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