mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 04:04:21 +02:00
Screenshots: fixes from review
- A right-click menu open when the headset changes closes, so it can't act on the other headset's shot. - A preview being retried by Refresh is no longer dropped when a background check lands first. - A late failure from a headset switched away from no longer drops the new headset's preview. - Tab and Escape close the menu and give focus back to where it was. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
47a29afb4c
commit
6cf01729e2
1 file changed
+16
-6
+16
-6
@@ -3549,12 +3549,13 @@ async function shotBlob(id, thumb) {
|
|||||||
return r.blob();
|
return r.blob();
|
||||||
}
|
}
|
||||||
function shotThumb(id) {
|
function shotThumb(id) {
|
||||||
if (!shots.thumbs.has(id)) {
|
const thumbs = shots.thumbs; // this headset's: a late failure must not touch the next one's
|
||||||
|
if (!thumbs.has(id)) {
|
||||||
const p = shotBlob(id, true).then(b => URL.createObjectURL(b));
|
const p = shotBlob(id, true).then(b => URL.createObjectURL(b));
|
||||||
p.catch(() => shots.thumbs.delete(id)); // try again next time
|
p.catch(() => { if (thumbs.get(id) === p) thumbs.delete(id); }); // try again next time
|
||||||
shots.thumbs.set(id, p);
|
thumbs.set(id, p);
|
||||||
}
|
}
|
||||||
return shots.thumbs.get(id);
|
return thumbs.get(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.
|
||||||
@@ -3570,6 +3571,7 @@ function loadShots(quiet) {
|
|||||||
}
|
}
|
||||||
// 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() {
|
||||||
|
closeMenu(); // its items were about the other headset's shot
|
||||||
shots.gen++; shots.fill++; 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();
|
||||||
@@ -3596,6 +3598,7 @@ async function fillThumbs(quiet) {
|
|||||||
const run = ++shots.fill;
|
const run = ++shots.fill;
|
||||||
for (const img of document.querySelectorAll("#shotGrid img[data-shot]:not([src])" + (quiet ? ":not([data-failed])" : ""))) {
|
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];
|
||||||
|
delete img.dataset.failed; // being tried again, so a background pass taking over keeps it
|
||||||
let url;
|
let url;
|
||||||
try { url = await shotThumb(s.id); } catch (e) { img.alt = "Preview failed"; img.dataset.failed = 1; }
|
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 (run !== shots.fill) return; // a newer pass (after a check, a redraw or another headset) has taken over
|
||||||
@@ -3669,12 +3672,19 @@ $("shotGrid").onclick = e => {
|
|||||||
if (b) saveShots([shots.list[+b.dataset.shotSave]], b);
|
if (b) saveShots([shots.list[+b.dataset.shotSave]], b);
|
||||||
};
|
};
|
||||||
// A small right-click menu. items: [label, fn] pairs, null for a divider.
|
// A small right-click menu. items: [label, fn] pairs, null for a divider.
|
||||||
function closeMenu() { document.querySelector(".ctx-menu")?.remove(); }
|
// back: the keyboard closed it, so focus returns to where it was.
|
||||||
|
function closeMenu(back) {
|
||||||
|
const menu = document.querySelector(".ctx-menu");
|
||||||
|
if (!menu) return;
|
||||||
|
menu.remove();
|
||||||
|
if (back === true && menu.opener?.isConnected) menu.opener.focus();
|
||||||
|
}
|
||||||
function showMenu(e, items) {
|
function showMenu(e, items) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
menu.className = "ctx-menu";
|
menu.className = "ctx-menu";
|
||||||
menu.setAttribute("role", "menu");
|
menu.setAttribute("role", "menu");
|
||||||
|
menu.opener = document.activeElement;
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
if (!it) { menu.append(document.createElement("hr")); continue; }
|
if (!it) { menu.append(document.createElement("hr")); continue; }
|
||||||
const b = document.createElement("button");
|
const b = document.createElement("button");
|
||||||
@@ -3693,7 +3703,7 @@ document.addEventListener("pointerdown", e => { if (!e.target.closest(".ctx-menu
|
|||||||
document.addEventListener("keydown", e => {
|
document.addEventListener("keydown", e => {
|
||||||
const menu = document.querySelector(".ctx-menu");
|
const menu = document.querySelector(".ctx-menu");
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
if (e.key === "Escape") return closeMenu();
|
if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); return closeMenu(true); } // Tab doesn't wander off behind it
|
||||||
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
|
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
|
||||||
e.preventDefault(); // arrows move through the menu, not the page
|
e.preventDefault(); // arrows move through the menu, not the page
|
||||||
const items = [...menu.querySelectorAll("button")];
|
const items = [...menu.querySelectorAll("button")];
|
||||||
|
|||||||
Reference in new issue
Block a user