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
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
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,
@@ -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
string (no holder/year); the `files` exclude `tsconfig.tsbuildinfo` never matches the real
`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
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"`
(~40px) Input/Buttons in DownloadBar.
- [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] **L20 — `youtubePlayerClient` is free text** with no allowlist/validation; a typo passes
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
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
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
@@ -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.
- [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized
list (the virtualizer only mounts visible rows).
- [ ] **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.
- [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. *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,
but it's passed straight into a child.
- [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
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.
*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] **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.
@@ -744,6 +776,8 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
mislabels when the chosen container is mkv/webm.
- [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.
*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.
- [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
@@ -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]`
([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.
- [ ] **L92 — Preload names diverge from main fn names.** `markSourceItemDownloaded``setMediaItemDownloaded`,
`syncSources``syncWatchedSources`. Align the names across the boundary.
- [x] **L92 — Preload names diverge from main fn names.** `markSourceItemDownloaded``setMediaItemDownloaded`,
`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
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)
(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
@@ -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
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`.
*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
[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
@@ -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] **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;
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
`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
@@ -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`;
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`).*
- [ ] **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.
*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
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
@@ -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:*
- [ ] **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.
- [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. *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` /
`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
placeholders ("1:30-2:00"). Choose one convention.
- [ ] **L152 — Search-field sizing differs per screen.** History `minWidth 180 / maxWidth 320`, Settings
inline `width: 100%`, DownloadBar `flexGrow`. No shared search-input width.
- [x] **L152 — Search-field sizing differs per screen.** History `minWidth 180 / maxWidth 320`, Settings
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,
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
@@ -1112,8 +1169,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
now (extends L57). Add `min={now}`.
- [ ] **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.
- [ ] **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.
*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
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
@@ -1122,7 +1184,10 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
still L5.)*
- [ ] **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
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
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
@@ -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
[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.*
- [ ] **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`
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
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
@@ -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.*
- [ ] **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.
- [ ] **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.
*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
@@ -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
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.
*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;
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."
+6 -4
View File
@@ -199,16 +199,18 @@ const api = {
listSourceItems: (sourceId: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItems, sourceId),
/** Persist that a media item has finished downloading (drives incremental sync). */
markSourceItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
/** Persist that a media item has finished downloading (drives incremental sync).
* Named to match the main handler `setMediaItemDownloaded` (L92). */
setMediaItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath),
/** Toggle whether a source is watched for new uploads. */
setSourceWatched: (id: string, watched: boolean): Promise<Source[]> =>
ipcRenderer.invoke(IpcChannels.sourceSetWatched, id, watched),
/** Re-index all watched sources; resolves with the videos found new. */
syncSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),
/** Re-index all watched sources; resolves with the videos found new. Named to
* match the main function `syncWatchedSources` (L92). */
syncWatchedSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),
/** Read / write the Windows daily-sync scheduled task. */
getScheduledSync: (): Promise<ScheduledSyncStatus> =>
+2 -1
View File
@@ -85,8 +85,9 @@ export function DownloadBar(): React.JSX.Element {
return (
<div
className={mergeClasses(styles.root, dragActive && styles.rootDragging)}
onDragEnter={bar.onDragEnter}
onDragOver={bar.onDragOver}
onDragLeave={() => bar.setDragActive(false)}
onDragLeave={bar.onDragLeave}
onDrop={bar.onDrop}
>
<div className={styles.urlRow}>
@@ -87,7 +87,9 @@ export interface DownloadBarController {
openInLibrary: () => void
dismissChannelHint: () => void
// handlers
onDragEnter: (e: React.DragEvent) => void
onDragOver: (e: React.DragEvent) => void
onDragLeave: () => void
onDrop: (e: React.DragEvent) => void
onUrlChange: (next: string) => 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.
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()
dragDepth.current += 1
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 {
e.preventDefault()
dragDepth.current = 0
setDragActive(false)
const dt = e.dataTransfer
const fromText = firstUrlInText(dt.getData('text/uri-list') || dt.getData('text/plain') || '')
@@ -562,7 +580,9 @@ export function useDownloadBar(): DownloadBarController {
openInLibrary,
dismissChannelHint,
// handlers
onDragEnter,
onDragOver,
onDragLeave,
onDrop,
onUrlChange,
onKindChange,
@@ -7,8 +7,7 @@ import {
Subtitle2,
Caption1,
Text,
Spinner,
tokens
Spinner
} from '@fluentui/react-components'
import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons'
import {
@@ -117,19 +116,13 @@ export function AboutCard(): React.JSX.Element {
/>
</Field>
{version?.ok && (
<Text style={{ fontFamily: tokens.fontFamilyMonospace }}>yt-dlp {version.version}</Text>
)}
{version?.ok && <Text className={styles.mono}>yt-dlp {version.version}</Text>}
{version && !version.ok && <Caption1 className={errText.errorPre}>{version.error}</Caption1>}
{ffmpeg && (
<div className={styles.folderRow}>
<div>
<Text style={{ fontFamily: tokens.fontFamilyMonospace, display: 'block' }}>
ffmpeg {ffmpeg.ffmpeg ?? 'not found'}
</Text>
<Text style={{ fontFamily: tokens.fontFamilyMonospace, display: 'block' }}>
ffprobe {ffmpeg.ffprobe ?? 'not found'}
</Text>
<Text className={styles.monoBlock}>ffmpeg {ffmpeg.ffmpeg ?? 'not found'}</Text>
<Text className={styles.monoBlock}>ffprobe {ffmpeg.ffprobe ?? 'not found'}</Text>
</div>
<Button
appearance="subtle"
@@ -176,7 +169,7 @@ export function AboutCard(): React.JSX.Element {
</Button>
</div>
{updateResult?.ok && (
<Text style={{ fontFamily: tokens.fontFamilyMonospace, whiteSpace: 'pre-wrap' }}>
<Text className={styles.monoPre}>
{updateResult.output || 'yt-dlp is already up to date.'}
</Text>
)}
@@ -158,7 +158,7 @@ export function DownloadsCard(): React.JSX.Element {
<Field
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
checked={minimizeToTray}
@@ -56,6 +56,19 @@ export const useSettingsStyles = makeStyles({
hint: {
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: {
display: 'flex',
gap: '10px'
+10 -4
View File
@@ -25,6 +25,12 @@ const MOCK_SETTINGS: Settings = {
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
// sign-in/clear flow be exercised in this browser-only preview.
let mockCookiesSavedAt: number | null = null
@@ -35,13 +41,13 @@ let mockTemplates: CommandTemplate[] = [
]
export const mockApi: Api = {
getAppVersion: async () => '0.4.0-preview',
getAppVersion: async () => MOCK_CURRENT_VERSION,
checkForAppUpdate: async () => {
await new Promise((r) => setTimeout(r, 400))
return {
ok: true,
available: true,
currentVersion: '0.4.0',
currentVersion: MOCK_CURRENT_VERSION,
latestVersion: '0.5.0',
notes:
'### 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 }),
removeSource: async () => [],
markSourceItemDownloaded: async () => [],
setMediaItemDownloaded: async () => [],
setSourceWatched: async () => [],
syncSources: async () => ({ ok: true, newItems: [] }),
syncWatchedSources: async () => ({ ok: true, newItems: [] }),
getScheduledSync: async () => ({ enabled: false }),
setScheduledSync: async (enabled: boolean) => ({ enabled }),
listSourceItems: async (sourceId) =>
+2 -4
View File
@@ -225,9 +225,7 @@ export const useSources = create<SourcesState>((set, get) => ({
return changed ? { itemsBySource: next } : {}
})
if (!PREVIEW)
window.api
.markSourceItemDownloaded(itemId, filePath)
.catch(logError('markSourceItemDownloaded'))
window.api.setMediaItemDownloaded(itemId, filePath).catch(logError('setMediaItemDownloaded'))
},
setWatched: (id, watched) => {
@@ -243,7 +241,7 @@ export const useSources = create<SourcesState>((set, get) => ({
await new Promise((r) => setTimeout(r, 700))
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
get().loadSources()
// Refresh any expanded source's items so new videos appear in the tree.