mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 04:04:21 +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:
|
if MAC:
|
||||||
cmd = ["open", "-R", str(path)]
|
cmd = ["open", "-R", str(path)]
|
||||||
elif WINDOWS:
|
elif WINDOWS:
|
||||||
cmd = ["explorer", f"/select,{path}"]
|
cmd = f'explorer /select,"{path}"' # as one string: Explorer wants the quotes after the comma
|
||||||
else:
|
else:
|
||||||
return open_path(path.parent)
|
return open_path(path.parent)
|
||||||
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
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
|
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"))));
|
: new Promise((ok, bad) => $("canvas").toBlob(b => b ? ok(b) : bad(new Error("couldn't encode the image")), "image/png"))));
|
||||||
$("copyBtn").onclick = copyViewer;
|
$("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 => {
|
$("canvas").oncontextmenu = e => {
|
||||||
if ($("saveBtn").disabled) return;
|
if (ctrl.on || $("saveBtn").disabled) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showMenu(e, [["Copy image", copyViewer], ["Save image", () => $("saveBtn").click()]]);
|
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.
|
// takes PNG only, and the blob is handed over as a promise so the click still counts.
|
||||||
async function copyImage(blob) {
|
async function copyImage(blob) {
|
||||||
if (window.frameApp?.writeImage) {
|
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" };
|
return { message: "Copied the image" };
|
||||||
}
|
}
|
||||||
if (!navigator.clipboard?.write || !window.ClipboardItem) throw new Error("This browser can't copy images here");
|
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 ----
|
// ---- Steam screenshots from the headset ----
|
||||||
// thumbs: id -> promise of an object URL, kept across reloads so a refresh
|
// thumbs: id -> promise of an object URL, kept across reloads so a refresh
|
||||||
// that finds a new shot fetches only that one's thumbnail.
|
// 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 STEAMVR_APPID = "250820";
|
||||||
const SHOTS_POLL_MS = 8000;
|
const SHOTS_POLL_MS = 8000;
|
||||||
function shotApp(appid) {
|
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,
|
// 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.
|
// and redraws only when the shots (or whether they're saved here) changed.
|
||||||
function loadShots(quiet) {
|
function loadShots(quiet) {
|
||||||
if (!shots.loading) {
|
if (shots.loading) {
|
||||||
const p = readShots(quiet === true).finally(() => { if (shots.loading === p) shots.loading = null; });
|
// A check already on its way will do for another check; Refresh, or a save
|
||||||
shots.loading = p;
|
// 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.
|
// Forget the shots of a headset we've switched away from, and ignore its answers still on their way.
|
||||||
function resetShots() {
|
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, () => {});
|
for (const p of shots.thumbs.values()) p.then(URL.revokeObjectURL, () => {});
|
||||||
shots.thumbs = new Map();
|
shots.thumbs = new Map();
|
||||||
}
|
}
|
||||||
@@ -3585,11 +3588,18 @@ async function readShots(quiet) {
|
|||||||
({ shots: shots.list, folder: shots.folder } = got);
|
({ shots: shots.list, folder: shots.folder } = got);
|
||||||
const sig = JSON.stringify(shots.list.map(s => [s.id, s.saved, shotApp(s.appid)]));
|
const sig = JSON.stringify(shots.list.map(s => [s.id, s.saved, shotApp(s.appid)]));
|
||||||
if (sig !== shots.sig) { shots.sig = sig; drawShots(); }
|
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.
|
fillThumbs(quiet); // not waited for: the next check, or a save, needn't sit behind the thumbnails
|
||||||
for (const img of document.querySelectorAll("#shotGrid img[data-shot]:not([src])")) {
|
}
|
||||||
|
// 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];
|
const s = shots.list[+img.dataset.shot];
|
||||||
try { img.src = await shotThumb(s.id); } catch (e) { img.alt = "Preview failed"; }
|
let url;
|
||||||
if (!img.isConnected || gen !== shots.gen) return; // redrawn, or another headset, meanwhile
|
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).
|
// "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}`,
|
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 }))]);
|
() => act("Show screenshot", () => api("/api/open", { what: "shot", id: s.id }))]);
|
||||||
items.push(["Copy file path", () => act("Copy file path", async () => {
|
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" };
|
return { message: "Copied the file path" };
|
||||||
})]);
|
})]);
|
||||||
}
|
}
|
||||||
items.push(null, ["Copy file name", () => act("Copy file name", async () => {
|
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 };
|
return { message: "Copied " + s.file };
|
||||||
})]);
|
})]);
|
||||||
showMenu(e, items);
|
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();
|
$("shotsRefresh").onclick = () => loadShots();
|
||||||
$("shotsSaveNew").onclick = e => saveShots(shots.list.filter(s => !s.saved), e.currentTarget);
|
$("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);
|
$("shotsFolder").onclick = e => act($("shotsFolder").textContent, () => api("/api/open", { what: "shots" }), e.currentTarget);
|
||||||
|
|||||||
Reference in new issue
Block a user