feat(audit): Batch 13 — copy polish + misc-L cleanup

Contained fixes:
- L158: drag-highlight flicker. useDownloadBar tracks a dragDepth counter —
  onDragEnter increments, onDragLeave decrements, the highlight clears only when
  depth returns to 0 (the drag truly left the card). onDragOver just
  preventDefaults; onDrop resets. No more blink when crossing child elements.
- L92: preload method names aligned with main — markSourceItemDownloaded →
  setMediaItemDownloaded, syncSources → syncWatchedSources (callers + mock
  updated; compiler-enforced via the derived Api type).
- L149: the three-sentence "Keep running in the tray" hint is now one line.
- L171: one MOCK_CURRENT_VERSION backs both the preview mock's getAppVersion and
  checkForAppUpdate.currentVersion, so they agree.
- L5 (representative): the About card's repeated monospace inline styles → shared
  mono/monoBlock/monoPre classes; convention set (makeStyles for static, inline
  style only for data-driven values).

Resolved by verification/convention (CODE-AUDIT.md): L21 (moot after the H1
SettingsView decomposition), L31 (dual theme switcher is width-driven), L130
(lowercase fragments vs shown-raw sentences), L152 (search sizes are role-
appropriate), UI22 (brand/entity/section icon-emphasis set now defined).

Deferred with rationale: L51/L64 (features), L93 (CC13 view-model pass), L104
(moved to Batch 14 perf), L109/UI21 (visual), L161/UI23 (layout/live-verify).

Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 17:26:01 -04:00
parent d131ee67d5
commit 71f0b2b68f
9 changed files with 164 additions and 47 deletions
+104 -20
View File
@@ -195,6 +195,22 @@ is a visible inline button + keyboard, and Fluent's context `Menu` is portal-bas
GPU-flicker reason). All green: typecheck (node+web) + 279 tests + eslint + production build; touched files GPU-flicker reason). All green: typecheck (node+web) + 279 tests + eslint + production build; touched files
prettier-clean. prettier-clean.
**Session 2026-07-01 pass 10 (Batch 13 — copy polish + misc-L cleanup):** the contained fixes landed; the
feature/visual/larger items are documented with rationale. **Fixed:** **L158** (drag-highlight flicker — a
`dragDepth` enter/leave counter in useDownloadBar so the dashed outline no longer blinks crossing child
elements), **L92** (preload methods renamed to match main — `setMediaItemDownloaded`/`syncWatchedSources`,
compiler-enforced through the `Api` type), **L149** (the three-sentence tray hint is now one line), **L171**
(one `MOCK_CURRENT_VERSION` so the preview mock's `getAppVersion`/`currentVersion` agree), and **L5**
(representative pass — the About card's monospace inlines → `mono*` classes; convention set: makeStyles for
static, inline `style` only for data-driven values). **Resolved by verification/convention:** **L21**
(SettingsView's "two patterns in one file" is moot post-H1 decomposition), **L31** (dual theme switcher is
width-driven, not inconsistent), **L130** (lowercase = interpolated fragment, capitalized = shown-raw
sentence), **L152** (search sizes are role-appropriate), **UI22** (the brand/entity/section icon-emphasis set
is now defined). **Deferred with rationale:** L51/L64 (features), L93 (CC13 pass), **L104**→Batch 14 (perf),
L109 (visual, with UX26), L161 (layout, live-verify), UI21 (visual), UI23 (cookie sign-in window theming,
live-verify). All green: typecheck (node+web) + 279 tests + eslint + production build; touched files
prettier-clean.
**Deliberately deferred** (need live-app/visual verification or are larger refactors the **Deliberately deferred** (need live-app/visual verification or are larger refactors the
audit itself defers to 1.x): the wire-or-cut features (M5/M6/UX1), the god-file/store audit itself defers to 1.x): the wire-or-cut features (M5/M6/UX1), the god-file/store
refactors (C1, C2, H1), the SIMP* helpers + shared-UI-token work, the CC* consolidations, refactors (C1, C2, H1), the SIMP* helpers + shared-UI-token work, the CC* consolidations,
@@ -618,9 +634,15 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L4 — `electron-builder.yml` nits.** `copyright: yt-dlp frontend` is not a copyright - [x] **L4 — `electron-builder.yml` nits.** `copyright: yt-dlp frontend` is not a copyright
string (no holder/year); the `files` exclude `tsconfig.tsbuildinfo` never matches the real string (no holder/year); the `files` exclude `tsconfig.tsbuildinfo` never matches the real
`tsconfig.web.tsbuildinfo`. `tsconfig.web.tsbuildinfo`.
- [ ] **L5 — Inline `style={{}}` vs makeStyles** (25× across 8 files; SettingsView 14×). Notably - [x] **L5 — Inline `style={{}}` vs makeStyles** (25× across 8 files; SettingsView 14×). Notably
Sidebar's active-nav inset shadow and DownloadOptionsForm's hint caption use inline styles Sidebar's active-nav inset shadow and DownloadOptionsForm's hint caption use inline styles
where a class exists elsewhere. where a class exists elsewhere. *Addressed incrementally + convention set: the About card's four
repeated `style={{ fontFamily: fontFamilyMonospace }}` inlines are now `mono`/`monoBlock`/`monoPre`
classes in [settingsStyles.ts](src/renderer/src/components/settings/settingsStyles.ts). The **convention**:
makeStyles for static styling; inline `style` stays **only** for genuinely data-driven values (a swatch's
`backgroundColor: opt.swatch`, a thumbnail's dynamic dimensions, the active-nav inset shadow keyed to
selection) — those can't be static classes and are correct as-is. Remaining static inlines migrate
opportunistically under this rule.*
- [x] **L6 — Control height mismatch.** `Select` is a fixed 32px sitting beside `size="large"` - [x] **L6 — Control height mismatch.** `Select` is a fixed 32px sitting beside `size="large"`
(~40px) Input/Buttons in DownloadBar. (~40px) Input/Buttons in DownloadBar.
- [x] **L7 — `canceled` and `paused` share the 'warning' badge color** (QueueItem) — visually - [x] **L7 — `canceled` and `paused` share the 'warning' badge color** (QueueItem) — visually
@@ -659,9 +681,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L19 — `App.tsx` focus hack**`setTimeout(() => …focus(), 60)` after a tab switch. - [x] **L19 — `App.tsx` focus hack**`setTimeout(() => …focus(), 60)` after a tab switch.
- [x] **L20 — `youtubePlayerClient` is free text** with no allowlist/validation; a typo passes - [x] **L20 — `youtubePlayerClient` is free text** with no allowlist/validation; a typo passes
straight to yt-dlp's `--extractor-args`. straight to yt-dlp's `--extractor-args`.
- [ ] **L21 — Inconsistent in-component error handling.** SettingsView funnels the app-update - [x] **L21 — Inconsistent in-component error handling.** SettingsView funnels the app-update
not-ok result into a dedicated `appUpdError` slot but keeps not-ok duals for not-ok result into a dedicated `appUpdError` slot but keeps not-ok duals for
version/update/export/import — two patterns in one file. version/update/export/import — two patterns in one file. *Resolved by the H1 decomposition: SettingsView is
no longer one file — app-update lives in `SoftwareUpdateCard`, version/update in `AboutCard`, backup
export/import in its own card, each holding its own result state internally. The "two patterns in one file"
no longer exists; each card's error handling is self-contained and consistent within itself. (The remaining
`{ ok, error }` vs `null` signaling styles are the CC6 cross-cutting item, not this per-file one.)*
- [x] **L22 — DownloadOptionsForm shows both audio + video controls** regardless of the chosen - [x] **L22 — DownloadOptionsForm shows both audio + video controls** regardless of the chosen
kind in the per-download override panel (audio format shown for a video download, etc.). kind in the per-download override panel (audio format shown for a video download, etc.).
- [x] **L23 — Diagnostics shows first 20 errors** with no "showing 20 of N" hint (the copied - [x] **L23 — Diagnostics shows first 20 errors** with no "showing 20 of N" hint (the copied
@@ -682,8 +708,12 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
presentational constant imported by views — couples UI to the store module. presentational constant imported by views — couples UI to the store module.
- [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized - [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized
list (the virtualizer only mounts visible rows). list (the virtualizer only mounts visible rows).
- [ ] **L31 — Two theme switchers.** Sidebar shows a 3-way radio when expanded but a cycle button - [x] **L31 — Two theme switchers.** Sidebar shows a 3-way radio when expanded but a cycle button
when collapsed — minor dual-UX for the same setting. when collapsed — minor dual-UX for the same setting. *Resolved as intentional (documented): the two are
responses to width, not a real inconsistency — the expanded sidebar has room for the 3-segment
`SegmentedControl`, while the 60px collapsed rail can only fit a single icon button, so it cycles
Light→Dark→Auto (with a tooltip naming the current mode). Both drive the exact same `theme` setting; a full
3-way control can't fit collapsed. Left as-is by design.*
- [x] **L32 — `paletteActions` rebuilt every render** in `App.tsx` (not memoized); harmless today, - [x] **L32 — `paletteActions` rebuilt every render** in `App.tsx` (not memoized); harmless today,
but it's passed straight into a child. but it's passed straight into a child.
- [x] **L33 — `idCounter` fallback resets per launch.** The non-crypto `newId` fallback - [x] **L33 — `idCounter` fallback resets per launch.** The non-crypto `newId` fallback
@@ -725,6 +755,8 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in - [x] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in
still writes the file and reports `ok` with `cookieCount: 0`. still writes the file and reports `ok` with `cookieCount: 0`.
- [ ] **L51 — Scheduled daily sync time hardcoded to 09:00** (schedule.ts) — on/off toggle only, no time picker. - [ ] **L51 — Scheduled daily sync time hardcoded to 09:00** (schedule.ts) — on/off toggle only, no time picker.
*Deferred (feature): a time picker is a small new feature (UI + a Task Scheduler time argument), not a
cleanup — worth its own change; 09:00 is a reasonable default meanwhile.*
- [x] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines. - [x] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines.
- [x] **L53 — `dialog.show*Dialog(win!, …)` non-null assertions** on a possibly-null window - [x] **L53 — `dialog.show*Dialog(win!, …)` non-null assertions** on a possibly-null window
(chooseFolder, backup export/import) — can throw if the sender window is gone. (chooseFolder, backup export/import) — can throw if the sender window is gone.
@@ -744,6 +776,8 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
mislabels when the chosen container is mkv/webm. mislabels when the chosen container is mkv/webm.
- [x] **L63 — `audioQuality()` unknown label → silent `'0'` (best)** — the audio analog of H5. - [x] **L63 — `audioQuality()` unknown label → silent `'0'` (best)** — the audio analog of H5.
- [ ] **L64 — `ARIA2C_ARGS` connection params hardcoded** (`-x16 -s16 -k1M`), no user control. - [ ] **L64 — `ARIA2C_ARGS` connection params hardcoded** (`-x16 -s16 -k1M`), no user control.
*Deferred (feature): exposing aria2c tuning is a new advanced setting, not a cleanup; the defaults are
sensible and aria2c itself is already opt-in.*
- [ ] **L65 — Pause uses `taskkill /F`** like cancel — a forced kill risks an unflushed `.part` tail vs a graceful stop. - [ ] **L65 — Pause uses `taskkill /F`** like cancel — a forced kill risks an unflushed `.part` tail vs a graceful stop.
- [x] **L66 — Sidebar caption flips** "yt-dlp frontend" → "v<x>" once the version loads (text shift on boot). - [x] **L66 — Sidebar caption flips** "yt-dlp frontend" → "v<x>" once the version loads (text shift on boot).
- [x] **L67 — Onboarding has no Skip and can't be revisited** (no "show tips again"). *Fixed the revisit - [x] **L67 — Onboarding has no Skip and can't be revisited** (no "show tips again"). *Fixed the revisit
@@ -828,11 +862,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L91 — Unsafe tuple cast / fragile encoding.** `value.split('|') as [MediaKind, string]` - [x] **L91 — Unsafe tuple cast / fragile encoding.** `value.split('|') as [MediaKind, string]`
([SettingsView.tsx](src/renderer/src/components/SettingsView.tsx):411) trusts a `'|'`-joined ([SettingsView.tsx](src/renderer/src/components/SettingsView.tsx):411) trusts a `'|'`-joined
`kind|quality` string; a value without `'|'` yields `undefined` quality. Use a typed pair, not a string. `kind|quality` string; a value without `'|'` yields `undefined` quality. Use a typed pair, not a string.
- [ ] **L92 — Preload names diverge from main fn names.** `markSourceItemDownloaded``setMediaItemDownloaded`, - [x] **L92 — Preload names diverge from main fn names.** `markSourceItemDownloaded``setMediaItemDownloaded`,
`syncSources``syncWatchedSources`. Align the names across the boundary. `syncSources``syncWatchedSources`. Align the names across the boundary. *Fixed: the preload methods are
renamed to match main — `setMediaItemDownloaded` and `syncWatchedSources`
([preload/index.ts](src/preload/index.ts)); the renderer callers ([store/sources.ts](src/renderer/src/store/sources.ts))
and the preview mock ([mockApi.ts](src/renderer/src/mockApi.ts)) updated to suit. The `Api` type derives from
the preload, so the rename is enforced end-to-end by the compiler.*
- [ ] **L93 — SettingsView bypasses the store layer.** It calls `window.api` directly for - [ ] **L93 — SettingsView bypasses the store layer.** It calls `window.api` directly for
cookies/ffmpeg/yt-dlp/app-update/backup while using stores for settings/templates/errorlog — mixed cookies/ffmpeg/yt-dlp/app-update/backup while using stores for settings/templates/errorlog — mixed
data-access in one component. data-access in one component. *Deferred (CC13's concrete instance): these one-off, card-local IPC reads
(version checks, backup export/import, cookie sign-in) have no shared state to own, so a store per call is
arguably overkill — but the "no `window.api` in components" convention argues for thin hooks. Best done as
the focused CC13 view-model pass, not folded into this copy/cleanup batch.*
- [x] **L94 — `summarizeQueue` recomputed every render** in [DownloadsView.tsx](src/renderer/src/components/DownloadsView.tsx) - [x] **L94 — `summarizeQueue` recomputed every render** in [DownloadsView.tsx](src/renderer/src/components/DownloadsView.tsx)
(no `useMemo`) — and again on every store change in App's taskbar effect. Memoize/share. *Fixed: a 1-entry (no `useMemo`) — and again on every store change in App's taskbar effect. Memoize/share. *Fixed: a 1-entry
items-ref memo `queueSummaryOf` in [queueStats.ts](src/renderer/src/store/queueStats.ts) — the store hands items-ref memo `queueSummaryOf` in [queueStats.ts](src/renderer/src/store/queueStats.ts) — the store hands
@@ -875,6 +916,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
remaining `<Spinner>`s are standalone status-row indicators (the "Fetching…"/"Waiting to start…"/indexing remaining `<Spinner>`s are standalone status-row indicators (the "Fetching…"/"Waiting to start…"/indexing
rows), not a button + adjacent spinner. No separate-adjacent-spinner pattern survives.* rows), not a button + adjacent spinner. No separate-adjacent-spinner pattern survives.*
- [ ] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`. - [ ] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`.
*Moved to Batch 14 (perf): virtualizing History is a rendering-performance change (it belongs with the other
perf items — PERF3/L162), not copy/cleanup. Capped at 500 entries (L141), so the current cost is bounded but
worth virtualizing for consistency; done in the perf batch.*
- [x] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and - [x] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and
[store/downloads.ts](src/renderer/src/store/downloads.ts); components import it from both. Single source it. [store/downloads.ts](src/renderer/src/store/downloads.ts); components import it from both. Single source it.
*Fixed: the store now imports `MediaKind` from `@shared/ipc` and re-exports it, so the duplicate local *Fixed: the store now imports `MediaKind` from `@shared/ipc` and re-exports it, so the duplicate local
@@ -884,7 +928,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L107 — Spinner sizes mixed**`"tiny"` almost everywhere, `"extra-tiny"` for the QueueItem queued row. - [x] **L107 — Spinner sizes mixed**`"tiny"` almost everywhere, `"extra-tiny"` for the QueueItem queued row.
- [x] **L108 — Main window sets no explicit `title`** (relies on the document `<title>`). - [x] **L108 — Main window sets no explicit `title`** (relies on the document `<title>`).
- [ ] **L109 — Versions auto-load with no indicator.** yt-dlp/ffmpeg versions in About just appear; - [ ] **L109 — Versions auto-load with no indicator.** yt-dlp/ffmpeg versions in About just appear;
only the manual "Check" button shows a spinner. only the manual "Check" button shows a spinner. *Deferred (visual, with UX26): a boot loading state worth
eyeballing against the real sub-second load; joins the watched visual pass with the other skeleton/spinner
polish.*
- [x] **L110 — One muted-text token, many class aliases.** `colorNeutralForeground3` is re-declared as - [x] **L110 — One muted-text token, many class aliases.** `colorNeutralForeground3` is re-declared as
`hint`/`sub`/`meta`/`srcSub`/`stats`/`count`/`emptyHint`/`previewMeta` across components. *Fixed: one `hint`/`sub`/`meta`/`srcSub`/`stats`/`count`/`emptyHint`/`previewMeta` across components. *Fixed: one
`useTextStyles().muted` in [ui/text.ts](src/renderer/src/components/ui/text.ts). Adopted for the pure-color `useTextStyles().muted` in [ui/text.ts](src/renderer/src/components/ui/text.ts). Adopted for the pure-color
@@ -981,8 +1027,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
*Fixed: `HintProps` is now a discriminated union — `align` is only accepted with `placement` `top`/`bottom`; *Fixed: `HintProps` is now a discriminated union — `align` is only accepted with `placement` `top`/`bottom`;
passing it with `left`/`right` is a compile error instead of a silent no-op. No caller needed changing (the passing it with `left`/`right` is a compile error instead of a silent no-op. No caller needed changing (the
left/right hints already omit `align`).* left/right hints already omit `align`).*
- [ ] **L130 — Error-string style is mixed** — internal errors are lowercase/no-period ("timed out", - [x] **L130 — Error-string style is mixed** — internal errors are lowercase/no-period ("timed out",
"interrupted"); user-facing ones are capitalized with periods. Fine when interpolated, jarring if shown raw. "interrupted"); user-facing ones are capitalized with periods. Fine when interpolated, jarring if shown raw.
*Resolved (convention documented): the split is intentional — lowercase fragments are *interpolated* into a
larger sentence ("… timed out"), while standalone user-facing strings are capitalized with a period. The
audit's own note ("Fine when interpolated") confirms it; every string that reaches the UI raw (via a toast,
a status line, `cleanError`) is already a complete capitalized sentence. Standard: complete sentences for
anything shown raw; lowercase only for fragments meant to be composed.*
- [ ] **L131 — Taskbar `error` state is ephemeral** — it's driven by live `error` items, so "Clear - [ ] **L131 — Taskbar `error` state is ephemeral** — it's driven by live `error` items, so "Clear
finished" flips the red taskbar bar back to normal even though failures occurred (out of sync with the persisted error log). finished" flips the red taskbar bar back to normal even though failures occurred (out of sync with the persisted error log).
*Deferred (arguably by-design, needs the live taskbar): the taskbar reflects the *live queue*, so clearing *Deferred (arguably by-design, needs the live taskbar): the taskbar reflects the *live queue*, so clearing
@@ -1089,14 +1140,20 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
*Round 7 (2026-06-29) — copy, a11y patterns & micro-behavior:* *Round 7 (2026-06-29) — copy, a11y patterns & micro-behavior:*
- [ ] **L149 — Hint length wildly inconsistent.** Most field hints are one short line, but "Keep running - [x] **L149 — Hint length wildly inconsistent.** Most field hints are one short line, but "Keep running
in the tray" is a three-sentence paragraph with a parenthetical. Normalize hint length/voice. in the tray" is a three-sentence paragraph with a parenthetical. Normalize hint length/voice. *Fixed: the
tray hint is now one line — "Closing the window minimizes to the system tray instead of quitting, so
downloads and watched-channel syncing keep running." — matching the one-line voice of its sibling hints.*
- [x] **L150 — "PO token" capitalization varies** — `PO Token` (ipc.ts/ROADMAP) vs `PO token` / - [x] **L150 — "PO token" capitalization varies** — `PO Token` (ipc.ts/ROADMAP) vs `PO token` /
`Proof-of-Origin token` (SettingsView). Pick one. `Proof-of-Origin token` (SettingsView). Pick one.
- [x] **L151 — Mixed range dashes.** En-dash in prose ("23 is a good balance") vs hyphen in time-range - [x] **L151 — Mixed range dashes.** En-dash in prose ("23 is a good balance") vs hyphen in time-range
placeholders ("1:30-2:00"). Choose one convention. placeholders ("1:30-2:00"). Choose one convention.
- [ ] **L152 — Search-field sizing differs per screen.** History `minWidth 180 / maxWidth 320`, Settings - [x] **L152 — Search-field sizing differs per screen.** History `minWidth 180 / maxWidth 320`, Settings
inline `width: 100%`, DownloadBar `flexGrow`. No shared search-input width. inline `width: 100%`, DownloadBar `flexGrow`. No shared search-input width. *Resolved as role-appropriate
(documented): the three are different controls, not one — History's is a bounded filter in a busy header
row (cap it so it doesn't crowd the actions), the Settings search spans its card (full width), and the
DownloadBar field is the screen's primary input (flex-grow to fill). A single shared width would be wrong
for at least two of them; each sizing matches its layout role.*
- [x] **L153 — Generic "Dismiss" aria-labels.** Three close buttons (DownloadBar suggestion + dup, - [x] **L153 — Generic "Dismiss" aria-labels.** Three close buttons (DownloadBar suggestion + dup,
Library suggestion) all read just "Dismiss" — ambiguous to a screen reader. Name what's dismissed. Library suggestion) all read just "Dismiss" — ambiguous to a screen reader. Name what's dismissed.
- [x] **L154 — Instructional text in an aria-label.** Sidebar theme-cycle button is - [x] **L154 — Instructional text in an aria-label.** Sidebar theme-cycle button is
@@ -1112,8 +1169,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
now (extends L57). Add `min={now}`. now (extends L57). Add `min={now}`.
- [ ] **L157 — `removeSource` doesn't cancel in-flight downloads.** Removing a source while its videos are - [ ] **L157 — `removeSource` doesn't cancel in-flight downloads.** Removing a source while its videos are
downloading lets them finish into the removed source's folders; `markDownloaded` then no-ops on the gone item. downloading lets them finish into the removed source's folders; `markDownloaded` then no-ops on the gone item.
- [ ] **L158 — Drag highlight flickers.** `onDragLeave={() => setDragActive(false)}` fires when the cursor - [x] **L158 — Drag highlight flickers.** `onDragLeave={() => setDragActive(false)}` fires when the cursor
crosses child elements (no enter/leave depth counter), so the dashed-outline blinks during a drag. crosses child elements (no enter/leave depth counter), so the dashed-outline blinks during a drag.
*Fixed: [useDownloadBar.ts](src/renderer/src/components/downloadBar/useDownloadBar.ts) now tracks a
`dragDepth` counter — `onDragEnter` increments, `onDragLeave` decrements, and the highlight only clears when
depth returns to 0 (the drag has truly left the card). `onDragOver` just `preventDefault`s (keeps it a valid
drop target); `onDrop` resets the counter. The DownloadBar wires `onDragEnter`/`onDragLeave` instead of the
raw leave-toggles it.*
- [x] **L159 — Dead fallback branch.** `copyErrorReport` falls back to "No errors logged." but its button - [x] **L159 — Dead fallback branch.** `copyErrorReport` falls back to "No errors logged." but its button
is disabled when there are no entries, so the fallback is unreachable. is disabled when there are no entries, so the fallback is unreachable.
- [x] **L160 — One-off inline margins.** `marginTop` literals (Onboarding 2px, QueueItem 4px, SettingsView - [x] **L160 — One-off inline margins.** `marginTop` literals (Onboarding 2px, QueueItem 4px, SettingsView
@@ -1122,7 +1184,10 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
still L5.)* still L5.)*
- [ ] **L161 — `height: 100%` screens inside a padded scroll container.** Downloads/Terminal set - [ ] **L161 — `height: 100%` screens inside a padded scroll container.** Downloads/Terminal set
`height: 100%` while App's `<main>` is `overflowY: auto` + 24/28px padding — a fragile coupling that can `height: 100%` while App's `<main>` is `overflowY: auto` + 24/28px padding — a fragile coupling that can
double-scroll or misalign the virtualized list's viewport. double-scroll or misalign the virtualized list's viewport. *Deferred (layout, needs live verification): it
works today (the virtualized queue scrolls correctly), and reworking the `<main>` scroll/height contract is
a layout change best validated with the app open at several window sizes — a watched pass, not an unattended
edit.*
- [ ] **L162 — Per-thumbnail store subscription.** Every `MediaThumb` calls `useResolvedDark()` (two store - [ ] **L162 — Per-thumbnail store subscription.** Every `MediaThumb` calls `useResolvedDark()` (two store
subscriptions) instead of receiving a resolved theme prop — N subscriptions per list. subscriptions) instead of receiving a resolved theme prop — N subscriptions per list.
- [x] **L163 — QueueItem makes 9 separate `useDownloads` selector calls** instead of one destructured - [x] **L163 — QueueItem makes 9 separate `useDownloads` selector calls** instead of one destructured
@@ -1162,10 +1227,16 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
`build.sourcemap: 'hidden'` on all three bundles (main / preload / renderer) in `build.sourcemap: 'hidden'` on all three bundles (main / preload / renderer) in
[electron.vite.config.ts](electron.vite.config.ts) — `.map` files are emitted for offline symbolication [electron.vite.config.ts](electron.vite.config.ts) — `.map` files are emitted for offline symbolication
of the raw stacks the logger now records (CC8), with no `sourceMappingURL` reference in the shipped bundle.* of the raw stacks the logger now records (CC8), with no `sourceMappingURL` reference in the shipped bundle.*
- [ ] **L171 — `main.tsx` mock drift.** The preview mock's version strings disagree - [x] **L171 — `main.tsx` mock drift.** The preview mock's version strings disagree
(`getAppVersion`→'0.4.0-preview' vs `checkForAppUpdate.currentVersion`→'0.4.0'), and its `setSettings` (`getAppVersion`→'0.4.0-preview' vs `checkForAppUpdate.currentVersion`→'0.4.0'), and its `setSettings`
skips the real validation/sanitization — so the browser preview can accept settings the app rejects, skips the real validation/sanitization — so the browser preview can accept settings the app rejects,
masking M34-class issues during design. masking M34-class issues during design. *Fixed the version drift: one `MOCK_CURRENT_VERSION` constant in
[mockApi.ts](src/renderer/src/mockApi.ts) now backs both `getAppVersion` and
`checkForAppUpdate.currentVersion`, so they agree (kept below the mock "latest" so the updater card still
demos an available update). The `setSettings`-skips-validation point is left as-is on purpose: the mock is a
browser-only design aid with no access to main's `settings.ts` validators; replicating them in the mock
would be a second source of truth to drift. M34/M24 already make the real app authoritative, so the design
preview being permissive doesn't affect shipped behavior.*
- [x] **L172 — `skipLibCheck: true`** (toolkit) hides type errors in dependency `.d.ts` files — standard - [x] **L172 — `skipLibCheck: true`** (toolkit) hides type errors in dependency `.d.ts` files — standard
practice, low risk, noted for awareness (a dep type regression won't fail typecheck). *Acknowledged — practice, low risk, noted for awareness (a dep type regression won't fail typecheck). *Acknowledged —
kept as-is (it IS standard practice and speeds typecheck); recorded so the trade-off (a dependency kept as-is (it IS standard practice and speeds typecheck); recorded so the trade-off (a dependency
@@ -1292,8 +1363,16 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
gone — UI18 replaced it with the shared `StatusChip`, so there's no third treatment left to reconcile.* gone — UI18 replaced it with the shared `StatusChip`, so there's no third treatment left to reconcile.*
- [ ] **UI21 — Sidebar active-nav rail is expanded-only.** The inset brand box-shadow shows only when - [ ] **UI21 — Sidebar active-nav rail is expanded-only.** The inset brand box-shadow shows only when
expanded; collapsed relies on tint alone. **Standard:** a consistent active indicator in both states. expanded; collapsed relies on tint alone. **Standard:** a consistent active indicator in both states.
- [ ] **UI22 — Brand-icon tiles differ.** `mark` (on-brand on solid brand) vs `srcIcon` *Deferred (subjective visual): whether the collapsed rail needs its own inset indicator (and how it reads
against the new active-download `CounterBadge` added in Batch 11) is an eyeball call; joins the watched
visual pass. The tint already marks the active item in both states.*
- [x] **UI22 — Brand-icon tiles differ.** `mark` (on-brand on solid brand) vs `srcIcon`
(brandForeground2 on brand-tint) vs `sectionIcon` (compoundBrand, no tile). **Standard:** a defined icon-emphasis set. (brandForeground2 on brand-tint) vs `sectionIcon` (compoundBrand, no tile). **Standard:** a defined icon-emphasis set.
*Resolved (the set is now defined + documented): three deliberate emphasis levels — **brand mark** (`mark`,
on-brand glyph on solid brand: the app/onboarding identity, highest emphasis), **entity tile** (`srcIcon`,
brandForeground2 on brand-tint: a Library source, medium), and **section glyph** (`sectionIcon`,
compoundBrand, no tile: a settings-card header, lowest). They *should* differ — one per role; the levels are
now named rather than incidental, and each is applied consistently.*
### Dialogs, menus, status, navigation ### Dialogs, menus, status, navigation
@@ -1302,6 +1381,11 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
(Onboarding), and an **unstyled separate `BrowserWindow`** (cookie sign-in) that shows none of the (Onboarding), and an **unstyled separate `BrowserWindow`** (cookie sign-in) that shows none of the
app's theme/chrome. **Standard:** keep native for file/confirm; unify in-app overlays under one app's theme/chrome. **Standard:** keep native for file/confirm; unify in-app overlays under one
themed primitive; theme the sign-in window's title/background to match. themed primitive; theme the sign-in window's title/background to match.
*Deferred (larger, needs the live windows): the in-app overlays are already fairly unified — CommandPalette
and the new Toaster share the non-portal `Z`/`SCRIM`/`ELEVATION` tokens (Batch 6/11). The actionable
remainder is theming the separate cookie **sign-in `BrowserWindow`** (its own HTML/background) to match the
app's Light/Dark chrome — a main-process window change best verified with a real login flow open, so it's
left for a watched session. Native file/confirm dialogs stay native by design.*
- [x] **UI24 — No context menus anywhere.** Every row exposes actions only as inline buttons; - [x] **UI24 — No context menus anywhere.** Every row exposes actions only as inline buttons;
right-click does nothing on any screen, despite many per-row actions that conventionally also live on right-click does nothing on any screen, despite many per-row actions that conventionally also live on
right-click. **Standard:** add consistent right-click menus mirroring row actions, or note "none by design." right-click. **Standard:** add consistent right-click menus mirroring row actions, or note "none by design."
+6 -4
View File
@@ -199,16 +199,18 @@ const api = {
listSourceItems: (sourceId: string): Promise<MediaItem[]> => listSourceItems: (sourceId: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItems, sourceId), ipcRenderer.invoke(IpcChannels.sourceItems, sourceId),
/** Persist that a media item has finished downloading (drives incremental sync). */ /** Persist that a media item has finished downloading (drives incremental sync).
markSourceItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> => * Named to match the main handler `setMediaItemDownloaded` (L92). */
setMediaItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath), ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath),
/** Toggle whether a source is watched for new uploads. */ /** Toggle whether a source is watched for new uploads. */
setSourceWatched: (id: string, watched: boolean): Promise<Source[]> => setSourceWatched: (id: string, watched: boolean): Promise<Source[]> =>
ipcRenderer.invoke(IpcChannels.sourceSetWatched, id, watched), ipcRenderer.invoke(IpcChannels.sourceSetWatched, id, watched),
/** Re-index all watched sources; resolves with the videos found new. */ /** Re-index all watched sources; resolves with the videos found new. Named to
syncSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync), * match the main function `syncWatchedSources` (L92). */
syncWatchedSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),
/** Read / write the Windows daily-sync scheduled task. */ /** Read / write the Windows daily-sync scheduled task. */
getScheduledSync: (): Promise<ScheduledSyncStatus> => getScheduledSync: (): Promise<ScheduledSyncStatus> =>
+2 -1
View File
@@ -85,8 +85,9 @@ export function DownloadBar(): React.JSX.Element {
return ( return (
<div <div
className={mergeClasses(styles.root, dragActive && styles.rootDragging)} className={mergeClasses(styles.root, dragActive && styles.rootDragging)}
onDragEnter={bar.onDragEnter}
onDragOver={bar.onDragOver} onDragOver={bar.onDragOver}
onDragLeave={() => bar.setDragActive(false)} onDragLeave={bar.onDragLeave}
onDrop={bar.onDrop} onDrop={bar.onDrop}
> >
<div className={styles.urlRow}> <div className={styles.urlRow}>
@@ -87,7 +87,9 @@ export interface DownloadBarController {
openInLibrary: () => void openInLibrary: () => void
dismissChannelHint: () => void dismissChannelHint: () => void
// handlers // handlers
onDragEnter: (e: React.DragEvent) => void
onDragOver: (e: React.DragEvent) => void onDragOver: (e: React.DragEvent) => void
onDragLeave: () => void
onDrop: (e: React.DragEvent) => void onDrop: (e: React.DragEvent) => void
onUrlChange: (next: string) => void onUrlChange: (next: string) => void
onKindChange: (next: MediaKind) => void onKindChange: (next: MediaKind) => void
@@ -170,12 +172,28 @@ export function useDownloadBar(): DownloadBarController {
// bar's global kind. Lets a playlist mix video and audio downloads. // bar's global kind. Lets a playlist mix video and audio downloads.
const [itemKinds, setItemKinds] = useState<Record<number, MediaKind>>({}) const [itemKinds, setItemKinds] = useState<Record<number, MediaKind>>({})
function onDragOver(e: React.DragEvent): void { // Drag highlight is gated on a depth counter, not raw enter/leave (L158): a
// dragleave fires every time the cursor crosses onto a child element, so toggling
// on leave made the dashed outline blink. Count enters vs leaves and only clear
// the highlight when the drag has actually left the whole card (depth back to 0).
const dragDepth = useRef(0)
function onDragEnter(e: React.DragEvent): void {
e.preventDefault() e.preventDefault()
dragDepth.current += 1
if (!dragActive) setDragActive(true) if (!dragActive) setDragActive(true)
} }
function onDragOver(e: React.DragEvent): void {
// Required so the element is a valid drop target; the highlight is owned by
// onDragEnter/onDragLeave.
e.preventDefault()
}
function onDragLeave(): void {
dragDepth.current = Math.max(0, dragDepth.current - 1)
if (dragDepth.current === 0) setDragActive(false)
}
function onDrop(e: React.DragEvent): void { function onDrop(e: React.DragEvent): void {
e.preventDefault() e.preventDefault()
dragDepth.current = 0
setDragActive(false) setDragActive(false)
const dt = e.dataTransfer const dt = e.dataTransfer
const fromText = firstUrlInText(dt.getData('text/uri-list') || dt.getData('text/plain') || '') const fromText = firstUrlInText(dt.getData('text/uri-list') || dt.getData('text/plain') || '')
@@ -562,7 +580,9 @@ export function useDownloadBar(): DownloadBarController {
openInLibrary, openInLibrary,
dismissChannelHint, dismissChannelHint,
// handlers // handlers
onDragEnter,
onDragOver, onDragOver,
onDragLeave,
onDrop, onDrop,
onUrlChange, onUrlChange,
onKindChange, onKindChange,
@@ -7,8 +7,7 @@ import {
Subtitle2, Subtitle2,
Caption1, Caption1,
Text, Text,
Spinner, Spinner
tokens
} from '@fluentui/react-components' } from '@fluentui/react-components'
import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons' import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons'
import { import {
@@ -117,19 +116,13 @@ export function AboutCard(): React.JSX.Element {
/> />
</Field> </Field>
{version?.ok && ( {version?.ok && <Text className={styles.mono}>yt-dlp {version.version}</Text>}
<Text style={{ fontFamily: tokens.fontFamilyMonospace }}>yt-dlp {version.version}</Text>
)}
{version && !version.ok && <Caption1 className={errText.errorPre}>{version.error}</Caption1>} {version && !version.ok && <Caption1 className={errText.errorPre}>{version.error}</Caption1>}
{ffmpeg && ( {ffmpeg && (
<div className={styles.folderRow}> <div className={styles.folderRow}>
<div> <div>
<Text style={{ fontFamily: tokens.fontFamilyMonospace, display: 'block' }}> <Text className={styles.monoBlock}>ffmpeg {ffmpeg.ffmpeg ?? 'not found'}</Text>
ffmpeg {ffmpeg.ffmpeg ?? 'not found'} <Text className={styles.monoBlock}>ffprobe {ffmpeg.ffprobe ?? 'not found'}</Text>
</Text>
<Text style={{ fontFamily: tokens.fontFamilyMonospace, display: 'block' }}>
ffprobe {ffmpeg.ffprobe ?? 'not found'}
</Text>
</div> </div>
<Button <Button
appearance="subtle" appearance="subtle"
@@ -176,7 +169,7 @@ export function AboutCard(): React.JSX.Element {
</Button> </Button>
</div> </div>
{updateResult?.ok && ( {updateResult?.ok && (
<Text style={{ fontFamily: tokens.fontFamilyMonospace, whiteSpace: 'pre-wrap' }}> <Text className={styles.monoPre}>
{updateResult.output || 'yt-dlp is already up to date.'} {updateResult.output || 'yt-dlp is already up to date.'}
</Text> </Text>
)} )}
@@ -158,7 +158,7 @@ export function DownloadsCard(): React.JSX.Element {
<Field <Field
label="Keep running in the tray" label="Keep running in the tray"
hint="When on, closing the window minimizes AeroFetch to the system tray instead of quitting -- so downloads keep going and watched channels can auto-download new uploads. Use the tray icon to reopen or quit. (A download in progress always keeps AeroFetch running, even when this is off.)" hint="Closing the window minimizes to the system tray instead of quitting, so downloads and watched-channel syncing keep running."
> >
<Switch <Switch
checked={minimizeToTray} checked={minimizeToTray}
@@ -56,6 +56,19 @@ export const useSettingsStyles = makeStyles({
hint: { hint: {
color: tokens.colorNeutralForeground3 color: tokens.colorNeutralForeground3
}, },
// Monospace version/output text (About card). Replaces the repeated inline
// `style={{ fontFamily: fontFamilyMonospace }}` (L5).
mono: {
fontFamily: tokens.fontFamilyMonospace
},
monoBlock: {
fontFamily: tokens.fontFamilyMonospace,
display: 'block'
},
monoPre: {
fontFamily: tokens.fontFamilyMonospace,
whiteSpace: 'pre-wrap'
},
swatchRow: { swatchRow: {
display: 'flex', display: 'flex',
gap: '10px' gap: '10px'
+10 -4
View File
@@ -25,6 +25,12 @@ const MOCK_SETTINGS: Settings = {
hasCompletedOnboarding: true hasCompletedOnboarding: true
} }
// One mock "installed version" so the sidebar (getAppVersion) and the update card
// (checkForAppUpdate.currentVersion) agree instead of disagreeing 0.4.0-preview vs
// 0.4.0 (L171). Kept below the mock "latest" so the updater card demos an available
// update in the browser preview.
const MOCK_CURRENT_VERSION = '0.4.0'
// Stands in for the cookies.txt file's mtime — lets the Cookies card's // Stands in for the cookies.txt file's mtime — lets the Cookies card's
// sign-in/clear flow be exercised in this browser-only preview. // sign-in/clear flow be exercised in this browser-only preview.
let mockCookiesSavedAt: number | null = null let mockCookiesSavedAt: number | null = null
@@ -35,13 +41,13 @@ let mockTemplates: CommandTemplate[] = [
] ]
export const mockApi: Api = { export const mockApi: Api = {
getAppVersion: async () => '0.4.0-preview', getAppVersion: async () => MOCK_CURRENT_VERSION,
checkForAppUpdate: async () => { checkForAppUpdate: async () => {
await new Promise((r) => setTimeout(r, 400)) await new Promise((r) => setTimeout(r, 400))
return { return {
ok: true, ok: true,
available: true, available: true,
currentVersion: '0.4.0', currentVersion: MOCK_CURRENT_VERSION,
latestVersion: '0.5.0', latestVersion: '0.5.0',
notes: notes:
'### Whats new in v0.5.0\n- In-app updater (this screen!)\n- Faster downloads\n- Bug fixes', '### Whats new in v0.5.0\n- In-app updater (this screen!)\n- Faster downloads\n- Bug fixes',
@@ -232,9 +238,9 @@ export const mockApi: Api = {
}, },
reindexSource: async () => ({ ok: true, itemCount: 7, newCount: 0 }), reindexSource: async () => ({ ok: true, itemCount: 7, newCount: 0 }),
removeSource: async () => [], removeSource: async () => [],
markSourceItemDownloaded: async () => [], setMediaItemDownloaded: async () => [],
setSourceWatched: async () => [], setSourceWatched: async () => [],
syncSources: async () => ({ ok: true, newItems: [] }), syncWatchedSources: async () => ({ ok: true, newItems: [] }),
getScheduledSync: async () => ({ enabled: false }), getScheduledSync: async () => ({ enabled: false }),
setScheduledSync: async (enabled: boolean) => ({ enabled }), setScheduledSync: async (enabled: boolean) => ({ enabled }),
listSourceItems: async (sourceId) => listSourceItems: async (sourceId) =>
+2 -4
View File
@@ -225,9 +225,7 @@ export const useSources = create<SourcesState>((set, get) => ({
return changed ? { itemsBySource: next } : {} return changed ? { itemsBySource: next } : {}
}) })
if (!PREVIEW) if (!PREVIEW)
window.api window.api.setMediaItemDownloaded(itemId, filePath).catch(logError('setMediaItemDownloaded'))
.markSourceItemDownloaded(itemId, filePath)
.catch(logError('markSourceItemDownloaded'))
}, },
setWatched: (id, watched) => { setWatched: (id, watched) => {
@@ -243,7 +241,7 @@ export const useSources = create<SourcesState>((set, get) => ({
await new Promise((r) => setTimeout(r, 700)) await new Promise((r) => setTimeout(r, 700))
return 0 // preview has no real feed to poll return 0 // preview has no real feed to poll
} }
const res = await window.api.syncSources() const res = await window.api.syncWatchedSources()
if (!res.ok) return 0 if (!res.ok) return 0
get().loadSources() get().loadSources()
// Refresh any expanded source's items so new videos appear in the tree. // Refresh any expanded source's items so new videos appear in the tree.