mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Screenshots: recheck fixes
- In Control, a right-click on the viewer goes to the Frame only; the copy menu stays out of the way. - Thumbnails no longer hold up the next check: a save shows as saved straight away, and a new shot appears while older previews are still loading. - Refresh (or a save) during a background check reads again after it, so the answer is fresh. - A preview that failed is retried on Refresh, not by every background check. - Copy reports a failure if the app refuses the image, and if the browser can't copy text. - Windows: Show in File Explorer works when the path has spaces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
83d74548cd
commit
47a29afb4c
2 files changed
+30
-16
No files matched your search
+1
-1
@@ -159,7 +159,7 @@ def reveal_path(path):
|
||||
if MAC:
|
||||
cmd = ["open", "-R", str(path)]
|
||||
elif WINDOWS:
|
||||
cmd = ["explorer", f"/select,{path}"]
|
||||
cmd = f'explorer /select,"{path}"' # as one string: Explorer wants the quotes after the comma
|
||||
else:
|
||||
return open_path(path.parent)
|
||||
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
|
||||
+29
-15
@@ -2029,9 +2029,9 @@ $("saveBtn").onclick = () => lastShot ? download(lastShot.blob, lastShot.file) :
|
||||
const copyViewer = () => act("Copy image", () => copyImage(lastShot ? lastShot.blob
|
||||
: new Promise((ok, bad) => $("canvas").toBlob(b => b ? ok(b) : bad(new Error("couldn't encode the image")), "image/png"))));
|
||||
$("copyBtn").onclick = copyViewer;
|
||||
// Right-click the viewer to copy or save what it shows.
|
||||
// Right-click the viewer to copy or save what it shows (in Control, a right-click goes to the Frame).
|
||||
$("canvas").oncontextmenu = e => {
|
||||
if ($("saveBtn").disabled) return;
|
||||
if (ctrl.on || $("saveBtn").disabled) return;
|
||||
e.preventDefault();
|
||||
showMenu(e, [["Copy image", copyViewer], ["Save image", () => $("saveBtn").click()]]);
|
||||
};
|
||||
@@ -2039,7 +2039,7 @@ $("canvas").oncontextmenu = e => {
|
||||
// takes PNG only, and the blob is handed over as a promise so the click still counts.
|
||||
async function copyImage(blob) {
|
||||
if (window.frameApp?.writeImage) {
|
||||
await window.frameApp.writeImage(new Uint8Array(await (await blob).arrayBuffer()));
|
||||
if (!await window.frameApp.writeImage(new Uint8Array(await (await blob).arrayBuffer()))) throw new Error("the app refused the image");
|
||||
return { message: "Copied the image" };
|
||||
}
|
||||
if (!navigator.clipboard?.write || !window.ClipboardItem) throw new Error("This browser can't copy images here");
|
||||
@@ -3534,7 +3534,7 @@ api("/api/host").then(applyHostWording).catch(() => {});
|
||||
// ---- Steam screenshots from the headset ----
|
||||
// thumbs: id -> promise of an object URL, kept across reloads so a refresh
|
||||
// that finds a new shot fetches only that one's thumbnail.
|
||||
const shots = { list: [], sig: null, thumbs: new Map(), loading: null, gen: 0 };
|
||||
const shots = { list: [], sig: null, thumbs: new Map(), loading: null, gen: 0, fill: 0 };
|
||||
const STEAMVR_APPID = "250820";
|
||||
const SHOTS_POLL_MS = 8000;
|
||||
function shotApp(appid) {
|
||||
@@ -3559,15 +3559,18 @@ function shotThumb(id) {
|
||||
// quiet: a background check. It keeps what's shown if the Frame can't be read,
|
||||
// and redraws only when the shots (or whether they're saved here) changed.
|
||||
function loadShots(quiet) {
|
||||
if (!shots.loading) {
|
||||
const p = readShots(quiet === true).finally(() => { if (shots.loading === p) shots.loading = null; });
|
||||
shots.loading = p;
|
||||
if (shots.loading) {
|
||||
// A check already on its way will do for another check; Refresh, or a save
|
||||
// that just finished, reads again after it so the answer is a fresh one.
|
||||
const again = () => loadShots();
|
||||
return quiet === true ? shots.loading : shots.loading.then(again, again);
|
||||
}
|
||||
return shots.loading;
|
||||
const p = readShots(quiet === true).finally(() => { if (shots.loading === p) shots.loading = null; });
|
||||
return shots.loading = p;
|
||||
}
|
||||
// Forget the shots of a headset we've switched away from, and ignore its answers still on their way.
|
||||
function resetShots() {
|
||||
shots.gen++; shots.loading = null; shots.list = []; shots.sig = null;
|
||||
shots.gen++; shots.fill++; shots.loading = null; shots.list = []; shots.sig = null;
|
||||
for (const p of shots.thumbs.values()) p.then(URL.revokeObjectURL, () => {});
|
||||
shots.thumbs = new Map();
|
||||
}
|
||||
@@ -3585,11 +3588,18 @@ async function readShots(quiet) {
|
||||
({ shots: shots.list, folder: shots.folder } = got);
|
||||
const sig = JSON.stringify(shots.list.map(s => [s.id, s.saved, shotApp(s.appid)]));
|
||||
if (sig !== shots.sig) { shots.sig = sig; drawShots(); }
|
||||
// Thumbnails one at a time over the shared SSH connection; one that failed is tried again next time.
|
||||
for (const img of document.querySelectorAll("#shotGrid img[data-shot]:not([src])")) {
|
||||
fillThumbs(quiet); // not waited for: the next check, or a save, needn't sit behind the thumbnails
|
||||
}
|
||||
// Thumbnails one at a time over the shared SSH connection. One that failed is
|
||||
// tried again on Refresh, not by every background check.
|
||||
async function fillThumbs(quiet) {
|
||||
const run = ++shots.fill;
|
||||
for (const img of document.querySelectorAll("#shotGrid img[data-shot]:not([src])" + (quiet ? ":not([data-failed])" : ""))) {
|
||||
const s = shots.list[+img.dataset.shot];
|
||||
try { img.src = await shotThumb(s.id); } catch (e) { img.alt = "Preview failed"; }
|
||||
if (!img.isConnected || gen !== shots.gen) return; // redrawn, or another headset, meanwhile
|
||||
let url;
|
||||
try { url = await shotThumb(s.id); } catch (e) { img.alt = "Preview failed"; img.dataset.failed = 1; }
|
||||
if (run !== shots.fill) return; // a newer pass (after a check, a redraw or another headset) has taken over
|
||||
if (url) img.src = url;
|
||||
}
|
||||
}
|
||||
// "Sep 28, 10:37 PM": short enough to sit beside the card's buttons (the full date is its tooltip).
|
||||
@@ -3710,16 +3720,20 @@ $("shotGrid").oncontextmenu = e => {
|
||||
items.push([`Show in ${!HOST.fileManager || HOST.fileManager === "your file manager" ? "folder" : HOST.fileManager}`,
|
||||
() => act("Show screenshot", () => api("/api/open", { what: "shot", id: s.id }))]);
|
||||
items.push(["Copy file path", () => act("Copy file path", async () => {
|
||||
await navigator.clipboard.writeText(shots.folder + sep + s.file);
|
||||
await copyText(shots.folder + sep + s.file);
|
||||
return { message: "Copied the file path" };
|
||||
})]);
|
||||
}
|
||||
items.push(null, ["Copy file name", () => act("Copy file name", async () => {
|
||||
await navigator.clipboard.writeText(s.file);
|
||||
await copyText(s.file);
|
||||
return { message: "Copied " + s.file };
|
||||
})]);
|
||||
showMenu(e, items);
|
||||
};
|
||||
function copyText(text) {
|
||||
if (!navigator.clipboard?.writeText) throw new Error("This browser can't copy here");
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
$("shotsRefresh").onclick = () => loadShots();
|
||||
$("shotsSaveNew").onclick = e => saveShots(shots.list.filter(s => !s.saved), e.currentTarget);
|
||||
$("shotsFolder").onclick = e => act($("shotsFolder").textContent, () => api("/api/open", { what: "shots" }), e.currentTarget);
|
||||
|
||||
Reference in new issue
Block a user