From 7b8539fb8dc3d2964d1c97cafb45281c1510e722 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 9 Sep 2026 22:34:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/screenshot):=200.6.4=20=E2=80=94?= =?UTF-8?q?=20Polaroid=20sounds,=20trash=20icon,=20centred=20cluster,=20fi?= =?UTF-8?q?lename=20footer=20+=20open-in-folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rolling every user report from the 0.6.3 rollout into one bundle: Sounds — the Web-Audio synth palette matches the metaphor now: - Screenshot: Polaroid shutter — sharp metallic tick + curtain-close click chained to a film-advance whir (band-passed noise sweeping 900→400 Hz). - Copy: printer "chika-chika-chika" — three descending percussive noise bursts pinned by short sine ticks. Reads as a print-head sweep. - Discard: paper crumple — three overlapping band-limited noise beds with per-sample random-amplitude crackle, descending centre freq. No more descending sine "boop". - Save: soft "photo dispensing" hiss (Polaroid ejects) + a small click. - Both the panel and editor share the design so nothing sounds different depending on which surface fired it. UI polish: - Discard button now carries a trash-can icon so it's obviously not the same as the close-sidebar X (they both used to be plain X's). - Toolbar drawing tools centre themselves via a new .tool-cluster wrapper (flex:1 1 auto, justify-content:center); the Copy/Save actions stay right-anchored via margin-left:auto on their own tgroup. Fixes the maximized-sidebar case where the drawing groups all crowded the left with a big empty gap before Copy/Save on the right. - Filename moves out of the topbar into a dedicated footer strip under the canvas board, alongside a new "Open in folder" button. The topbar is now flex-wrap:nowrap and holds only fixed-width window controls, so a long filename can never push discard / sound / max / close onto a second row (the filename ellipsises instead). - "Open in folder" invokes a new "openFolder" addon message that calls Electron's shell.showItemInFolder() to open the OS file explorer with the specific scratch PNG highlighted (falls back to shell.openPath() on the scratch dir when no capture is named). Version bump so the OTA update endpoint picks it up on the next tick. --- bundled-addons/screenshot/addon.json | 2 +- bundled-addons/screenshot/editor.css | 38 ++++++++-- bundled-addons/screenshot/editor.html | 28 +++++++- bundled-addons/screenshot/editor.js | 99 +++++++++++++++++++++------ bundled-addons/screenshot/index.js | 16 +++++ bundled-addons/screenshot/panel.html | 70 ++++++++++++++----- 6 files changed, 204 insertions(+), 49 deletions(-) diff --git a/bundled-addons/screenshot/addon.json b/bundled-addons/screenshot/addon.json index 0dc3e7c..ed8f36b 100644 --- a/bundled-addons/screenshot/addon.json +++ b/bundled-addons/screenshot/addon.json @@ -1,7 +1,7 @@ { "id": "screenshot", "name": "Screenshot", - "version": "0.6.3", + "version": "0.6.4", "description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, rect, ellipse, pen, text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.", "author": "Silent Mode", "icon": "📸", diff --git a/bundled-addons/screenshot/editor.css b/bundled-addons/screenshot/editor.css index 2e4e87a..7b6c12d 100644 --- a/bundled-addons/screenshot/editor.css +++ b/bundled-addons/screenshot/editor.css @@ -23,13 +23,24 @@ body { background: var(--bg); color: var(--ink); .topbar, .toolbar { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--line); background: var(--panel); user-select: none; flex-wrap: wrap; } -/* Toolbar layout: drawing groups pack left/centre, the Copy/Save actions - pin to the right. When the sidebar is too narrow to hold everything in - one row, each group wraps as a unit (never a single dot orphaned) and - the actions cluster wraps as its own row on the right. */ -.toolbar { justify-content: flex-start; gap: 10px; row-gap: 6px; } +/* The topbar has fixed-size window controls only; no reason to ever wrap + its row. Filename lives in the footer now, so nothing can grow the row + past what the icons need. */ +.topbar { flex-wrap: nowrap; } +/* Toolbar layout: drawing tools live inside `.tool-cluster` which is a + flex-grow container centring its own contents. Copy/Save (`.tgroup + .actions`) sit outside it and pin to the right edge. On a maximized + sidebar the cluster centres in the empty space between name-area and + the actions; at narrow widths the whole cluster shrinks and wraps by + category, and the actions can wrap to their own row on the right. */ +.toolbar { align-items: flex-start; gap: 8px; row-gap: 6px; } +.tool-cluster { + display: flex; flex-wrap: wrap; align-items: center; + gap: 10px; row-gap: 6px; + flex: 1 1 auto; justify-content: center; +} .tgroup { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; } -.tgroup.actions { margin-left: auto; } +.tgroup.actions { flex: 0 0 auto; align-self: center; } /* Close-sidebar button — reads as danger without shouting. */ #close-sidebar:hover, @@ -38,6 +49,21 @@ body { background: var(--bg); color: var(--ink); .btn.danger:hover { border-color: rgba(255,91,91,.55); } .topbar .name { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; margin: 0 4px; } + +/* Footer strip that carries the filename + "Open in folder" affordance, + sitting under the canvas board. The filename truncates first so the + button never falls off the edge; the button never wraps. */ +.footer { display: flex; align-items: center; gap: 8px; + padding: 6px 10px; border-top: 1px solid var(--line); + background: var(--panel); font-size: 11.5px; } +.footer .fname { color: var(--dim); flex: 1 1 auto; min-width: 0; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.footer .fbtn { border: 1px solid var(--line); background: var(--panel2); + color: var(--ink); border-radius: 6px; cursor: pointer; + padding: 4px 8px; display: inline-flex; gap: 6px; align-items: center; + flex: 0 0 auto; font: inherit; line-height: 1; } +.footer .fbtn svg { width: 14px; height: 14px; display: block; } +.footer .fbtn:hover { border-color: rgb(from var(--acid) r g b / .55); } .topbar .spacer, .toolbar .spacer { flex: 1; } .btn, .tool { diff --git a/bundled-addons/screenshot/editor.html b/bundled-addons/screenshot/editor.html index 4c43841..2513681 100644 --- a/bundled-addons/screenshot/editor.html +++ b/bundled-addons/screenshot/editor.html @@ -10,7 +10,6 @@ - screenshot + history) instead of one dot spilling to a lonely second row. + `.tool-cluster` centres itself so on a maximized sidebar the drawing + groups sit in the middle of the row and the Copy/Save actions + right-anchor cleanly against the edge, instead of the tools crowding + the left when there's a lot of empty space. -->
+
+
+ +
diff --git a/bundled-addons/screenshot/editor.js b/bundled-addons/screenshot/editor.js index 3e82c45..f305c19 100644 --- a/bundled-addons/screenshot/editor.js +++ b/bundled-addons/screenshot/editor.js @@ -44,17 +44,21 @@ function _tone(freq, dur, type, vol, at) { osc.start(t0); osc.stop(t0 + dur + 0.02); } -// A short filtered noise burst — the mechanical component of the shutter -// click and (paired lower down) the "paper feed" texture of the print -// sound. `bp` is the band-pass centre frequency, `q` how tight the band. -function _noise(dur, vol, bp, q, at) { +// Filtered noise burst. `bp` = band-pass centre freq, `q` = sharpness of +// the band. Optional `pan` gives a random-amplitude micro-modulation +// texture — used by the paper-crumple pass. +function _noise(dur, vol, bp, q, at, texture) { 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; + for (let i = 0; i < n; i++) { + let s = Math.random() * 2 - 1; + if (texture) s *= 0.4 + Math.random() * 0.6; // crackle + d[i] = s; + } const src = c.createBufferSource(); src.buffer = buf; const flt = c.createBiquadFilter(); flt.type = "bandpass"; flt.frequency.setValueAtTime(bp || 2000, t0); @@ -66,26 +70,64 @@ function _noise(dur, vol, bp, q, at) { src.start(t0); src.stop(t0 + dur + 0.02); } -// Photoshoot: two clicks — mirror-slap up, mirror-slap down. A short -// high-band noise burst is the metallic ping, a low-band burst is the -// mechanical thud, and a very short low tone fills in the body of the -// shutter closing. +// Slowly-sweeping band-pass noise — used as the film-advance "whir" tail +// under the shutter click. +function _sweep(dur, vol, fromHz, toHz, 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(fromHz, t0); + flt.frequency.exponentialRampToValueAtTime(toHz, t0 + dur); + flt.Q.setValueAtTime(q || 8, t0); + const g = c.createGain(); + g.gain.setValueAtTime(0.0001, t0); + g.gain.exponentialRampToValueAtTime(vol, t0 + dur * 0.2); + 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); +} +// Polaroid: sharp mechanical click (shutter mirror + curtain) followed by +// a short film-advance whir. Reads unmistakably as "camera taking a +// picture" rather than a UI beep. function playShutter() { - _noise(0.03, 0.28, 4200, 6, 0); // first click, up - _tone(180, 0.04, "square", 0.10, 0.005); - _noise(0.04, 0.32, 3600, 5, 0.08); // second click, down - _tone(140, 0.05, "square", 0.12, 0.09); + _noise(0.02, 0.32, 5200, 8, 0); // sharp metallic tick + _tone(160, 0.03, "square", 0.14, 0.003); // mirror thud + _noise(0.03, 0.24, 3200, 5, 0.03); // curtain close + _tone(120, 0.04, "square", 0.10, 0.035); + _sweep(0.28, 0.10, 900, 400, 12, 0.07); // film-advance whir tail } -// Copy: two-chirp "printer feed" — matches Firefox's Easy Screenshot copy -// feedback (staccato ascending pair, no residual hum). +// Printer chika-chika-chika — three descending percussive bursts modelled +// on a print head sweeping across a page. Fast and unmistakably "action +// happened", 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); + _noise(0.028, 0.22, 3800, 9, 0.00); + _tone(1200, 0.03, "sine", 0.10, 0.00); + _noise(0.028, 0.22, 3200, 9, 0.06); + _tone(1000, 0.03, "sine", 0.10, 0.06); + _noise(0.028, 0.22, 2600, 9, 0.12); + _tone(800, 0.03, "sine", 0.10, 0.12); +} +// Photo dispensing — a soft pneumatic hiss with a small final click, the +// way a Polaroid ejects its print. +function playSave() { + _sweep(0.18, 0.14, 3800, 1600, 3, 0); + _noise(0.02, 0.20, 2400, 8, 0.18); + _tone(900, 0.04, "sine", 0.12, 0.20); +} +// Paper crumple — a long band-limited noise with texture crackle and a +// descending centre frequency, reading as "a page being scrunched". +function playDiscard() { + _noise(0.16, 0.28, 2600, 3, 0.00, true); // main body + _noise(0.12, 0.22, 1800, 3, 0.06, true); // trailing scrunch + _noise(0.08, 0.16, 1200, 3, 0.12, true); } -function playSave() { _tone(700, 0.06, "sine", 0.16, 0); _tone(1100, 0.08, "sine", 0.16, 0.05); _tone(1500, 0.09, "sine", 0.16, 0.10); } -function playDiscard() { _tone(500, 0.06, "sine", 0.18, 0); _tone(280, 0.10, "sine", 0.14, 0.05); } function playCropApply() { _tone(1200, 0.05, "sine", 0.14, 0); _tone(1600, 0.07, "sine", 0.14, 0.04); } const base = $("base"); @@ -618,6 +660,21 @@ if (cancelCropBtn) cancelCropBtn.addEventListener("click", cancelCrop); // -- top-bar navigation ---------------------------------------------- $("back").addEventListener("click", () => { location.href = "panel.html"; }); +// "Open in folder" — sends the current capture's name to the addon's +// openFolder handler, which calls shell.showItemInFolder() so the file +// explorer opens with the exact scratch PNG highlighted. +const openFolderBtn = document.getElementById("open-folder"); +if (openFolderBtn) openFolderBtn.addEventListener("click", async () => { + try { + if (window.silentmode?.invoke) { + await window.silentmode.invoke("openFolder", { name: NAME || "" }); + } + } catch (e) { + console.warn("open-folder failed:", e); + toast("Couldn't open folder: " + (e && e.message || e), true); + } +}); + // Discard — throw away this capture entirely (delete it from the Recent // ring) and return to the panel. Back is non-destructive; Discard is not. const discardBtn = document.getElementById("discard"); diff --git a/bundled-addons/screenshot/index.js b/bundled-addons/screenshot/index.js index c165824..04f13e0 100644 --- a/bundled-addons/screenshot/index.js +++ b/bundled-addons/screenshot/index.js @@ -126,6 +126,22 @@ module.exports = { return { name: hit.name, dataUrl: readAsDataUrl(hit.path), bytes: hit.bytes, at: hit.at, mode: hit.mode }; }); + // Show the scratch dir (or a specific capture inside it) in the OS + // file explorer. `api.require("electron").shell.openPath` opens the + // folder; `showItemInFolder` opens the folder AND highlights the + // file — used when the panel/editor knows a specific capture. + api.onMessage("openFolder", (payload) => { + const { shell } = api.require("electron"); + const wantedName = payload && payload.name ? String(payload.name) : ""; + if (wantedName) { + const recent = api.storage.get("recent", []) || []; + const hit = recent.find((r) => r.id === wantedName); + if (hit) { shell.showItemInFolder(hit.path); return { ok: true, path: hit.path }; } + } + shell.openPath(scratchDir); + return { ok: true, path: scratchDir }; + }); + api.onMessage("clearRecent", (payload) => { const wantedName = payload && payload.name ? String(payload.name) : ""; let recent = api.storage.get("recent", []) || []; diff --git a/bundled-addons/screenshot/panel.html b/bundled-addons/screenshot/panel.html index 317ea11..cba5244 100644 --- a/bundled-addons/screenshot/panel.html +++ b/bundled-addons/screenshot/panel.html @@ -236,17 +236,21 @@ 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) { + // Filtered noise burst — camera clicks, printer thwacks, and (with + // texture=true) the crackle of crumpled paper. `bp` = band-pass centre + // freq, `q` = sharpness of the band. + function noise(dur, vol, bp, q, at, texture) { 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; + for (let i = 0; i < n; i++) { + let s = Math.random() * 2 - 1; + if (texture) s *= 0.4 + Math.random() * 0.6; + d[i] = s; + } const src = c.createBufferSource(); src.buffer = buf; const flt = c.createBiquadFilter(); flt.type = "bandpass"; flt.frequency.setValueAtTime(bp || 2000, t0); @@ -258,23 +262,53 @@ 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. + // Sweeping band-pass noise — used as the film-advance whir tail of the + // Polaroid shutter. + function sweep(dur, vol, fromHz, toHz, 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(fromHz, t0); + flt.frequency.exponentialRampToValueAtTime(toHz, t0 + dur); + flt.Q.setValueAtTime(q || 8, t0); + const g = c.createGain(); + g.gain.setValueAtTime(0.0001, t0); + g.gain.exponentialRampToValueAtTime(vol, t0 + dur * 0.2); + 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); + } + // Polaroid: mirror + curtain click, then a film-advance whir. 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); + noise(0.02, 0.32, 5200, 8, 0); + tone(160, 0.03, "square", 0.14, 0.003); + noise(0.03, 0.24, 3200, 5, 0.03); + tone(120, 0.04, "square", 0.10, 0.035); + sweep(0.28, 0.10, 900, 400, 12, 0.07); } - // Copy — two-chirp "printer feed", modeled on Firefox Easy Screenshot's - // copy feedback: fast staccato ascending pair, no residual hum. + // Printer chika-chika-chika — 3 descending percussive bursts. 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); + noise(0.028, 0.22, 3800, 9, 0.00); + tone(1200, 0.03, "sine", 0.10, 0.00); + noise(0.028, 0.22, 3200, 9, 0.06); + tone(1000, 0.03, "sine", 0.10, 0.06); + noise(0.028, 0.22, 2600, 9, 0.12); + tone(800, 0.03, "sine", 0.10, 0.12); + } + // Paper crumple — long band-limited noise with crackle texture and a + // descending centre frequency. + function playDiscard() { + noise(0.16, 0.28, 2600, 3, 0.00, true); + noise(0.12, 0.22, 1800, 3, 0.06, true); + noise(0.08, 0.16, 1200, 3, 0.12, true); } - 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