Four user reports from the 0.6.0 rollout: - Text tool never committed. openTextInput placed the box correctly but a couple of Chromium quirks stopped a normal type-Enter cycle: focus() called synchronously right after appendChild lost the race in some builds, and the input's own mousedown / click was bubbling through to #base and re-firing openTextInput on every subsequent keystroke click-through, so what looked like "nothing happens" was actually "a new empty box spawned on top of the last one every time". Now: focus after requestAnimationFrame, contain pointerdown / mousedown / click inside the input so they don't bubble to the canvas, track the font size on the state so commit uses the same one openTextInput measured against, and preventDefault on the base pointerdown so Chromium doesn't reset focus back to <body>. - Toolbar wrapped one dot at a time when the sidebar was narrow (a lonely thin/medium/thick width would jump to a second row while the swatches stayed above it). Toolbar items are now wrapped in `<div class="tgroup">` per category — tools / swatches / widths / undo-redo — with `flex: 0 0 auto`, so a whole row wraps as a unit and lands cleanly under the previous one. `gap: 10px / row-gap: 6px` keeps the visual grouping obvious. - No way to close the sidebar without hunting for the dock icon. Added an X button in the top-right of both the sidebar panel and the editor toolbar. Both wire through a new `silentmode.sidebar.close()` preload method that calls the existing `sidebar-close` IPC. - Tightened the pointerdown text branch so preventDefault + explicit focus-after-frame make the click-through races impossible. Bundled but not shipped separately — parent session signs and pushes.
472 lines
20 KiB
HTML
472 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Screenshot</title>
|
||
<style>
|
||
:root { color-scheme: light dark;
|
||
--bg:#0e131c; --panel:#141a24; --line:rgba(255,255,255,.09);
|
||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
||
--btn:#1c2432; --btn-h:#242e40; --err:#ff9081; }
|
||
@media (prefers-color-scheme: light) {
|
||
:root { --bg:#f8faff; --panel:#ffffff; --line:rgba(0,0,0,.10);
|
||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
|
||
--btn:#f0f3fa; --btn-h:#e3e8f2; --acid:#088A66; --err:#c81f0f; }
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; }
|
||
body { background: var(--bg); color: var(--ink);
|
||
font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||
display: flex; flex-direction: column; }
|
||
header { display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
||
background: var(--panel); gap: 8px; }
|
||
header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; min-width: 0; }
|
||
header .t .em { font-size: 15px; }
|
||
header .m { color: var(--dim); font-size: 11.5px; min-height: 15px; flex: 1; text-align: right; }
|
||
header .max {
|
||
border: 1px solid var(--line); border-radius: 6px; background: var(--btn);
|
||
color: var(--ink); cursor: pointer; padding: 4px 6px; font: inherit; line-height: 0;
|
||
}
|
||
header .max:hover { background: var(--btn-h); }
|
||
header .max svg { width: 14px; height: 14px; display: block; }
|
||
header .max.close:hover { border-color: rgba(255,91,91,.55); color: var(--err); }
|
||
main { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; }
|
||
.modes { display: flex; gap: 6px; }
|
||
button.mode {
|
||
flex: 1; min-width: 0;
|
||
padding: 10px 6px; border: 1px solid var(--line); border-radius: 8px;
|
||
background: var(--btn); color: var(--ink); cursor: pointer;
|
||
font: inherit; text-align: center;
|
||
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||
transition: background 120ms;
|
||
}
|
||
button.mode:hover:not(:disabled) { background: var(--btn-h); }
|
||
button.mode:disabled { opacity: .5; cursor: default; }
|
||
button.mode .em { font-size: 18px; line-height: 1; }
|
||
button.mode .lbl { font-size: 11.5px; }
|
||
.prev-wrap {
|
||
border: 1px dashed var(--line); border-radius: 8px;
|
||
background:
|
||
repeating-conic-gradient(rgba(255,255,255,.02) 0% 25%, transparent 0% 50%)
|
||
50%/16px 16px;
|
||
padding: 6px; min-height: 120px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: var(--dim); font-size: 12px; text-align: center;
|
||
}
|
||
.prev-wrap img {
|
||
max-width: 100%; max-height: 320px; border-radius: 4px; display: block;
|
||
background: #fff;
|
||
}
|
||
.meta { color: var(--dim); font-size: 11.5px; display: flex; justify-content: space-between; gap: 8px; }
|
||
.meta .host { color: var(--mut); }
|
||
.actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.actions .row { display: flex; gap: 6px; flex: 1 1 100%; }
|
||
button.act {
|
||
flex: 1; padding: 9px 8px; border: 1px solid var(--line); border-radius: 8px;
|
||
background: var(--btn); color: var(--ink); cursor: pointer; font: inherit;
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||
line-height: 1;
|
||
}
|
||
button.act svg { width: 14px; height: 14px; display: block; }
|
||
button.act.primary { background: var(--acid); color: #101418; border-color: transparent; font-weight: 600; }
|
||
button.act:hover:not(:disabled) { background: var(--btn-h); }
|
||
button.act.primary:hover:not(:disabled) { filter: brightness(1.05); background: var(--acid); }
|
||
button.act:disabled { opacity: .45; cursor: default; }
|
||
.confirm {
|
||
display: none; gap: 6px; align-items: center;
|
||
padding: 8px 10px; border: 1px solid rgba(255,91,91,.40); border-radius: 8px;
|
||
background: rgba(255,91,91,.06); font-size: 12px;
|
||
}
|
||
.confirm.on { display: flex; }
|
||
.confirm .msg { flex: 1; color: var(--mut); }
|
||
.confirm button {
|
||
padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px;
|
||
background: var(--btn); color: var(--ink); cursor: pointer; font: inherit;
|
||
}
|
||
.confirm button.yes { background: #ff5b5b; color: #101418; border-color: transparent; font-weight: 600; }
|
||
.confirm button:hover { background: var(--btn-h); }
|
||
.confirm button.yes:hover { filter: brightness(1.05); background: #ff5b5b; }
|
||
.status { color: var(--mut); font-size: 12px; min-height: 16px; }
|
||
.status.err { color: var(--err); }
|
||
.status.ok { color: var(--acid); }
|
||
.recent { display: none; }
|
||
.recent.on { display: block; }
|
||
.recent .lbl { color: var(--dim); font-size: 11.5px; text-transform: uppercase; letter-spacing: .5px; margin: 4px 0 6px; display: flex; justify-content: space-between; }
|
||
.recent .strip { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 4px; }
|
||
.recent .tile {
|
||
flex: 0 0 auto; width: 78px; height: 52px; border-radius: 4px;
|
||
border: 1px solid var(--line); background: var(--btn);
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer; overflow: hidden; position: relative;
|
||
}
|
||
.recent .tile img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
|
||
.recent .tile:hover { border-color: var(--acid); }
|
||
.recent .clear { background: none; border: 0; color: var(--dim); font: inherit; cursor: pointer; text-transform: none; letter-spacing: 0; }
|
||
.recent .clear:hover { color: var(--err); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="t"><span class="em">📸</span> <span>Screenshot</span></div>
|
||
<div class="m" id="hdr-status"></div>
|
||
<button class="max" id="btn-sound" title="Toggle sound" aria-label="Toggle sound"></button>
|
||
<button class="max" id="btn-max" title="Expand the sidebar to full window width" aria-label="Expand sidebar">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/>
|
||
</svg>
|
||
</button>
|
||
<button class="max close" id="btn-close" title="Close the sidebar" aria-label="Close sidebar">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||
<path d="M4 4l8 8M12 4l-8 8"/>
|
||
</svg>
|
||
</button>
|
||
</header>
|
||
<main>
|
||
<div class="modes">
|
||
<button class="mode" data-mode="visible" title="Capture the visible part of the current tab">
|
||
<span class="em">🖼️</span><span class="lbl">Visible</span>
|
||
</button>
|
||
<button class="mode" data-mode="full" title="Capture the entire scrollable page">
|
||
<span class="em">📄</span><span class="lbl">Full page</span>
|
||
</button>
|
||
<button class="mode" data-mode="region" title="Drag a rectangle on the page to select what to capture">
|
||
<span class="em">✂️</span><span class="lbl">Region</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="prev-wrap" id="prev-wrap">
|
||
<span id="prev-empty">No capture yet. Pick a mode above.</span>
|
||
<img id="prev-img" alt="" hidden>
|
||
</div>
|
||
|
||
<div class="meta">
|
||
<span id="meta-size">—</span>
|
||
<span class="host" id="meta-host"></span>
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<div class="row">
|
||
<button class="act" id="btn-copy" disabled title="Copy the raw capture to the clipboard">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="4" width="9" height="10"/><path d="M3 12V3h9"/></svg>
|
||
<span>Copy</span>
|
||
</button>
|
||
<button class="act" id="btn-save" disabled title="Save the raw capture straight to Downloads">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v9M4 7l4 4 4-4M2 14h12"/></svg>
|
||
<span>Save</span>
|
||
</button>
|
||
</div>
|
||
<div class="row">
|
||
<button class="act" id="btn-discard" disabled>Discard</button>
|
||
<button class="act primary" id="btn-edit" disabled title="Open the annotate editor — arrow, rect, ellipse, pen, text, undo, save, copy">Edit</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status" id="status"></div>
|
||
|
||
<div class="recent" id="recent">
|
||
<div class="lbl"><span>Recent</span>
|
||
<button class="clear" id="clear-recent" title="Delete every past capture">clear all</button>
|
||
</div>
|
||
<div class="strip" id="recent-strip"></div>
|
||
<div class="confirm" id="clear-confirm">
|
||
<span class="msg">Delete every past capture?</span>
|
||
<button id="clear-no">Cancel</button>
|
||
<button class="yes" id="clear-yes">Delete</button>
|
||
</div>
|
||
</div>
|
||
<a id="download-link" style="display:none"></a>
|
||
</main>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
const previewImg = $("prev-img");
|
||
const previewEmpty = $("prev-empty");
|
||
const status = $("status");
|
||
const metaSize = $("meta-size");
|
||
const metaHost = $("meta-host");
|
||
const btnEdit = $("btn-edit");
|
||
const btnDiscard = $("btn-discard");
|
||
const btnCopy = $("btn-copy");
|
||
const btnSave = $("btn-save");
|
||
const btnMax = $("btn-max");
|
||
const btnSound = $("btn-sound");
|
||
const dl = $("download-link");
|
||
|
||
// ---- audio ----------------------------------------------------------
|
||
// Synthesized in-page so the tarball stays small — no wav shipped.
|
||
// Preference is stored under silentmode.storage so the editor sees the
|
||
// same setting when it loads (default on).
|
||
let soundOn = true;
|
||
let audioCtx = null;
|
||
function actx() {
|
||
if (!audioCtx) {
|
||
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch {}
|
||
}
|
||
// A user gesture is needed to resume in some Chromium setups; the
|
||
// toggle button click satisfies that.
|
||
if (audioCtx && audioCtx.state === "suspended") { audioCtx.resume().catch(() => {}); }
|
||
return audioCtx;
|
||
}
|
||
function tone(freq, dur, type, vol, at) {
|
||
if (!soundOn) return;
|
||
const c = actx(); if (!c) return;
|
||
const t0 = c.currentTime + (at || 0);
|
||
const osc = c.createOscillator();
|
||
const g = c.createGain();
|
||
osc.type = type || "sine";
|
||
osc.frequency.setValueAtTime(freq, t0);
|
||
g.gain.setValueAtTime(vol || 0.15, t0);
|
||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||
osc.connect(g); g.connect(c.destination);
|
||
osc.start(t0);
|
||
osc.stop(t0 + dur + 0.02);
|
||
}
|
||
// Filtered noise burst — the metallic "click" of the shutter and the
|
||
// paper-feed texture of the print sound. Band-pass around `bp` with
|
||
// sharpness `q` picks a resonance so we don't just play white noise.
|
||
function noise(dur, vol, bp, q, at) {
|
||
if (!soundOn) return;
|
||
const c = actx(); if (!c) return;
|
||
const t0 = c.currentTime + (at || 0);
|
||
const n = Math.max(1, Math.floor(c.sampleRate * dur));
|
||
const buf = c.createBuffer(1, n, c.sampleRate);
|
||
const d = buf.getChannelData(0);
|
||
for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
|
||
const src = c.createBufferSource(); src.buffer = buf;
|
||
const flt = c.createBiquadFilter(); flt.type = "bandpass";
|
||
flt.frequency.setValueAtTime(bp || 2000, t0);
|
||
flt.Q.setValueAtTime(q || 4, t0);
|
||
const g = c.createGain();
|
||
g.gain.setValueAtTime(vol || 0.2, t0);
|
||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||
src.connect(flt); flt.connect(g); g.connect(c.destination);
|
||
src.start(t0);
|
||
src.stop(t0 + dur + 0.02);
|
||
}
|
||
// Photoshoot shutter — mirror-slap up (metallic ping + soft thud), then
|
||
// mirror-slap down. Reads as a real camera click rather than a beep.
|
||
function playShutter() {
|
||
noise(0.03, 0.28, 4200, 6, 0);
|
||
tone(180, 0.04, "square", 0.10, 0.005);
|
||
noise(0.04, 0.32, 3600, 5, 0.08);
|
||
tone(140, 0.05, "square", 0.12, 0.09);
|
||
}
|
||
// Copy — two-chirp "printer feed", modeled on Firefox Easy Screenshot's
|
||
// copy feedback: fast staccato ascending pair, no residual hum.
|
||
function playCopy() {
|
||
noise(0.02, 0.14, 3200, 8, 0);
|
||
tone(1400, 0.05, "sine", 0.14, 0);
|
||
noise(0.02, 0.14, 4200, 8, 0.06);
|
||
tone(2000, 0.06, "sine", 0.14, 0.06);
|
||
}
|
||
function playDiscard() { tone(500, 0.06, "sine", 0.18, 0); tone(280, 0.10, "sine", 0.14, 0.05); }
|
||
function paintSound() {
|
||
btnSound.title = soundOn ? "Sounds on — click to mute" : "Sounds off — click to enable";
|
||
btnSound.innerHTML = soundOn
|
||
? '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6v4h3l3.5 3V3L5.5 6z"/><path d="M11 5.5c1 .8 1 4.2 0 5"/><path d="M12.5 4c2 1.5 2 6.5 0 8"/></svg>'
|
||
: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6v4h3l3.5 3V3L5.5 6z"/><path d="M11 5l4 6M15 5l-4 6"/></svg>';
|
||
}
|
||
(async () => {
|
||
try {
|
||
const v = await window.silentmode.storage.get("soundOn", true);
|
||
soundOn = v !== false; // default on
|
||
} catch {}
|
||
paintSound();
|
||
})();
|
||
btnSound.addEventListener("click", async () => {
|
||
soundOn = !soundOn;
|
||
paintSound();
|
||
try { await window.silentmode.storage.set("soundOn", soundOn); } catch {}
|
||
if (soundOn) playShutter(); // audible confirmation
|
||
});
|
||
const hdrStatus = $("hdr-status");
|
||
const recentBox = $("recent");
|
||
const recentStrip = $("recent-strip");
|
||
const clearBtn = $("clear-recent");
|
||
|
||
let last = null; // { name, dataUrl, width, height, host, bytes, mode }
|
||
let busy = false;
|
||
|
||
const fmt = (n) => n >= 1e6 ? (n / 1e6).toFixed(1) + " MB"
|
||
: n >= 1e3 ? (n / 1e3).toFixed(0) + " KB"
|
||
: n + " B";
|
||
|
||
function setStatus(text, cls = "") {
|
||
status.textContent = text || "";
|
||
status.className = "status" + (cls ? " " + cls : "");
|
||
}
|
||
|
||
function renderPreview() {
|
||
const on = !!(last && last.dataUrl);
|
||
previewImg.hidden = !on;
|
||
previewImg.src = on ? last.dataUrl : "";
|
||
previewEmpty.hidden = on;
|
||
metaSize.textContent = on ? `${last.width}×${last.height} · ${fmt(last.bytes || 0)}` : "—";
|
||
metaHost.textContent = on ? (last.host || "") : "";
|
||
btnEdit.disabled = !on;
|
||
btnDiscard.disabled = !on;
|
||
btnCopy.disabled = !on;
|
||
btnSave.disabled = !on;
|
||
}
|
||
|
||
async function refreshRecent() {
|
||
try {
|
||
const list = await window.silentmode.invoke("listRecent", {});
|
||
if (!Array.isArray(list) || list.length === 0) {
|
||
recentBox.classList.remove("on");
|
||
recentStrip.innerHTML = "";
|
||
return;
|
||
}
|
||
recentBox.classList.add("on");
|
||
recentStrip.innerHTML = list.map((r) =>
|
||
`<div class="tile" data-name="${r.name.replace(/"/g,""")}" title="${r.name.replace(/"/g,""")} · ${fmt(r.bytes || 0)}"></div>`
|
||
).join("");
|
||
for (const tile of recentStrip.querySelectorAll(".tile")) {
|
||
const name = tile.dataset.name;
|
||
try {
|
||
const b = await window.silentmode.invoke("getBytes", { name });
|
||
if (b && b.dataUrl) tile.innerHTML = `<img src="${b.dataUrl}" alt="">`;
|
||
} catch {}
|
||
tile.addEventListener("click", async () => {
|
||
try {
|
||
const b = await window.silentmode.invoke("getBytes", { name });
|
||
if (!b) return;
|
||
last = { name: b.name, dataUrl: b.dataUrl, width: 0, height: 0, host: "", bytes: b.bytes };
|
||
const im = new Image();
|
||
im.onload = () => { last.width = im.naturalWidth; last.height = im.naturalHeight; renderPreview(); };
|
||
im.src = b.dataUrl;
|
||
renderPreview();
|
||
setStatus(`Loaded ${b.name}.`, "ok");
|
||
} catch (e) {
|
||
setStatus("Couldn't load: " + (e && e.message || e), "err");
|
||
}
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.warn("refreshRecent failed:", e);
|
||
}
|
||
}
|
||
|
||
async function doCapture(mode) {
|
||
if (busy) return;
|
||
busy = true;
|
||
hdrStatus.textContent = "capturing…";
|
||
setStatus("");
|
||
if (mode === "region") setStatus("Drag on the page. Esc to cancel.");
|
||
else if (mode === "full") setStatus("Rendering full page — this can take a moment.");
|
||
for (const b of document.querySelectorAll("button.mode")) b.disabled = true;
|
||
try {
|
||
const res = await window.silentmode.invoke("capture", { mode });
|
||
if (res && res.cancelled) {
|
||
setStatus("Region capture cancelled.");
|
||
} else if (res && res.dataUrl) {
|
||
last = res;
|
||
renderPreview();
|
||
playShutter();
|
||
setStatus("Captured. Click Edit to annotate.", "ok");
|
||
refreshRecent();
|
||
} else {
|
||
setStatus("Nothing captured.", "err");
|
||
}
|
||
} catch (e) {
|
||
console.warn("capture failed:", e);
|
||
setStatus(String(e && e.message || e), "err");
|
||
} finally {
|
||
busy = false;
|
||
hdrStatus.textContent = "";
|
||
for (const b of document.querySelectorAll("button.mode")) b.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function doEdit() {
|
||
if (!last || !last.name || busy) return;
|
||
// No IPC handshake — the editor pulls bytes on load via silentmode
|
||
// .invoke("getBytes", {name}) directly. That's idempotent, so a refresh
|
||
// or back-and-forth navigation always shows the image.
|
||
location.href = "editor.html?name=" + encodeURIComponent(last.name);
|
||
}
|
||
|
||
async function toggleMax() {
|
||
try { await window.silentmode.sidebar.toggleMax(); }
|
||
catch (e) { console.warn("toggleMax failed:", e); }
|
||
}
|
||
|
||
// Convert a base64 data URL into a Blob without a fetch round-trip.
|
||
function dataUrlToBlob(dataUrl) {
|
||
const m = /^data:([^;,]+);base64,(.+)$/.exec(String(dataUrl || ""));
|
||
if (!m) throw new Error("expected base64 data URL");
|
||
const bin = atob(m[2]);
|
||
const buf = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
||
return new Blob([buf], { type: m[1] });
|
||
}
|
||
async function doCopy() {
|
||
if (!last || !last.dataUrl || busy) return;
|
||
try {
|
||
const blob = dataUrlToBlob(last.dataUrl);
|
||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||
playCopy();
|
||
setStatus("Copied to clipboard.", "ok");
|
||
} catch (e) {
|
||
console.warn("copy failed:", e);
|
||
setStatus("Copy failed: " + (e && e.message || e), "err");
|
||
}
|
||
}
|
||
async function doSave() {
|
||
if (!last || !last.dataUrl || busy) return;
|
||
try {
|
||
const blob = dataUrlToBlob(last.dataUrl);
|
||
const url = URL.createObjectURL(blob);
|
||
dl.href = url;
|
||
dl.download = last.name || "screenshot.png";
|
||
dl.click();
|
||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||
setStatus(`Saved ${dl.download} (${(blob.size / 1024).toFixed(1)} KB)`, "ok");
|
||
} catch (e) {
|
||
console.warn("save failed:", e);
|
||
setStatus("Save failed: " + (e && e.message || e), "err");
|
||
}
|
||
}
|
||
|
||
for (const b of document.querySelectorAll("button.mode")) {
|
||
b.addEventListener("click", () => doCapture(b.dataset.mode));
|
||
}
|
||
btnEdit.addEventListener("click", doEdit);
|
||
btnDiscard.addEventListener("click", () => { playDiscard(); last = null; renderPreview(); setStatus(""); });
|
||
btnCopy.addEventListener("click", doCopy);
|
||
btnSave.addEventListener("click", doSave);
|
||
btnMax.addEventListener("click", toggleMax);
|
||
document.getElementById("btn-close").addEventListener("click", async () => {
|
||
// Explicit close from the sidebar itself — same IPC the toolbar dock
|
||
// icon toggles. No need to un-maximize first; setSidebar(false) on the
|
||
// main side flips visibility and the maximize state is remembered so
|
||
// the next open lands where it left off.
|
||
try { await window.silentmode.sidebar.close(); }
|
||
catch (e) { setStatus("Close failed: " + (e && e.message || e), "err"); }
|
||
});
|
||
|
||
// Inline confirmation for "clear all" — the native confirm() looks like an
|
||
// OS-modal from the last decade AND opens over the tab area (out of the
|
||
// sidebar's visual context). Toggle a styled strip inside the sidebar so
|
||
// the destructive action stays where the user is looking.
|
||
const confirmBox = $("clear-confirm");
|
||
clearBtn.addEventListener("click", () => {
|
||
confirmBox.classList.add("on");
|
||
clearBtn.disabled = true;
|
||
});
|
||
$("clear-no").addEventListener("click", () => {
|
||
confirmBox.classList.remove("on");
|
||
clearBtn.disabled = false;
|
||
});
|
||
$("clear-yes").addEventListener("click", async () => {
|
||
confirmBox.classList.remove("on");
|
||
clearBtn.disabled = false;
|
||
try { await window.silentmode.invoke("clearRecent", {}); await refreshRecent(); setStatus("Cleared past captures.", "ok"); }
|
||
catch (e) { setStatus("Clear failed: " + (e && e.message || e), "err"); }
|
||
});
|
||
|
||
refreshRecent();
|
||
</script>
|
||
</body>
|
||
</html>
|