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