feat(win): W2/UX19 persist window placement, W10 taskbar flash; tick W11

- W2/UX19: windowState.ts persists the window's normal bounds + maximized flag to
  <userData>/window-state.json (no new dep); createWindow restores from it with a
  screen.getAllDisplays() visibility guard so it never reopens off a disconnected
  monitor. Debounced save on resize/move + save on close (which may hide to tray).
- W10: notify() flashFrame(true)s the taskbar when a completion/failure lands while
  the window is unfocused/minimized (Windows clears the flash on focus).
- W11: already implemented (badge.ts overlay dot + setOverlayIcon) — ticked.

typecheck + 268 tests + eslint + prettier green. (Window restore + flash want a
live quit/relaunch confirm.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 11:33:11 -04:00
parent 64af608596
commit ab825488b8
4 changed files with 132 additions and 10 deletions
+21 -8
View File
@@ -1175,8 +1175,9 @@ root cause is already filed.
- [ ] **UX17 — Icon-only Fetch/Paste buttons** rely on hover tooltips for meaning (UI a11y; new users on touch/keyboard miss them). **Fix:** labels or `aria`-described affordances.
- [ ] **UX18 — The quality control morphs after Fetch** (preset list → real formats; label "Quality" →
"Quality / format") in place — a surprising transform. **Fix:** keep a stable control with a clear "formats loaded" state.
- [ ] **UX19 — Window size/position isn't remembered.** [index.ts](src/main/index.ts) opens a fixed
920×700 every launch (no bounds persistence). **Fix:** persist + restore window bounds.
- [x] **UX19 — Window size/position isn't remembered.** [index.ts](src/main/index.ts) opens a fixed
920×700 every launch (no bounds persistence). **Fix:** persist + restore window bounds. *Fixed with W2 —
[windowState.ts](src/main/windowState.ts) persists + restores bounds/maximized with an off-screen guard.*
- [ ] **UX20 — "App is still running" surprise.** Closing with a download active hides to tray and
notifies once per process (L68); later closes give no hint, so the window "won't close" reads as a
bug. **Fix:** a persistent tray hint / first-close explainer.
@@ -1207,9 +1208,14 @@ DPI handled by Chromium). The deviations:
- [x] **W1 — No minimum window size.** `createWindow` sets `width/height` but no `minWidth`/`minHeight`
([index.ts](src/main/index.ts)), so the window can be dragged down to a few pixels and the layout
(212px sidebar + content) breaks. **Standard:** set a sensible min (e.g. 640×480).
- [ ] **W2 — Window placement isn't persisted** (extends UX19): size, position, **maximized state**, and
- [x] **W2 — Window placement isn't persisted** (extends UX19): size, position, **maximized state**, and
**which monitor** are all forgotten — it reopens 920×700 on the primary display every launch.
**Standard:** persist & restore window placement per Windows app convention (e.g. `electron-window-state`).
*Fixed (no new dep): [windowState.ts](src/main/windowState.ts) persists the window's normal bounds +
maximized flag to `<userData>/window-state.json`; `createWindow` opens from the saved state (debounced
save on resize/move + a save on close, which may only hide to tray). A `screen.getAllDisplays()`
visibility check falls back to the default centered size when the saved monitor is gone, so the window
can never restore off-screen. (The restore cycle itself wants a live quit→relaunch confirm.)*
- [x] **W3 — Title bar doesn't follow the in-app theme.** `nativeTheme.themeSource` is deliberately never
set (index.ts:97), so the OS-drawn caption follows the *OS* theme. Choosing an explicit in-app **dark**
theme on a **light** OS leaves a light title bar on a dark app (and vice-versa). **Standard:** set
@@ -1244,12 +1250,19 @@ DPI handled by Chromium). The deviations:
### System tray, taskbar & icons
- [ ] **W10 — No taskbar attention flash.** When a background download completes while the window is
- [x] **W10 — No taskbar attention flash.** When a background download completes while the window is
minimized/hidden, the app doesn't `flashFrame` the taskbar button. **Standard:** flash for attention on
background completion (pairs with the existing notification).
- [ ] **W11 — No taskbar overlay badge** (deferred in roadmap): a download manager conventionally shows an
background completion (pairs with the existing notification). *Fixed: `notify()` in
[download.ts](src/main/download.ts) calls `win.flashFrame(true)` when the completion/failure event lands
while the window is unfocused/minimized/hidden (Windows stops the flash on focus). Gated on the same
`notifyOnComplete` as the toast, so it pairs with it. (The flash itself wants a quick live confirm.)*
- [x] **W11 — No taskbar overlay badge** (deferred in roadmap): a download manager conventionally shows an
active-count / error overlay icon via `setOverlayIcon`. **Standard:** add it (the progress bar's error
mode is not a substitute).
mode is not a substitute). *Done: [badge.ts](src/main/badge.ts) draws a green (active) / red (error) dot
`NativeImage` with pure Node/zlib (no dep), and the `taskbarProgress` handler in
[ipc.ts](src/main/ipc.ts) calls `win.setOverlayIcon(badge, label)` with an accessible count label
("N downloads active" / "Download error"), cleared to `null` when the queue goes idle. (Numeric count
lives in the tooltip label — a 16px overlay can't legibly render digits.)*
- [ ] **W12 — Fallback tray icon is single-resolution.** The embedded fallback is a 32×32 PNG, so on
150%/200% DPI the tray glyph is blurry when the real multi-size `.ico` is absent. **Standard:** a
multi-size fallback (or rely only on the `.ico`).
@@ -1769,7 +1782,7 @@ Security & correctness audit (2026-06-23):
- Migrate JSON stores to better-sqlite3 if a user indexes many large channels.
- Metadata editing (`--parse-metadata`/`--replace-in-metadata`) — held pending a live recipe.
- PO-token WebView auto-minting — only the `--extractor-args` plumbing shipped.
- Taskbar overlay badge (needs a drawn `NativeImage`).
- ~~Taskbar overlay badge (needs a drawn `NativeImage`)~~ — done (W11): [badge.ts](src/main/badge.ts).
- i18n — deliberately deferred (note: UI strings are hard-coded throughout).
- Code signing — **non-goal**: no certificate will be purchased, so builds ship unsigned and the SmartScreen
prompt is accepted. The build remains signing-ready via `CSC_LINK`/`CSC_KEY_PASSWORD` env vars if this ever