diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index f8b3d62..99273cc 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -192,7 +192,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n ~20 useState), `DownloadBar.tsx` (858, ~17 state hooks), `store/downloads.ts` (614). - [ ] **H2 — Consolidate scattered utilities.** `youtubeId` ×2; 4 YouTube-URL-parsing variants; byte/speed/eta/duration formatting across 4 modules. -- [ ] **H3 — Fix `probe.ts → download.ts` dependency direction** (`fmtBytes` import); resolves +- [x] **H3 — Fix `probe.ts → download.ts` dependency direction** (`fmtBytes` import); resolves with H2's shared formatter. - [ ] **H4 — Carry raw numbers across the progress boundary.** `DownloadProgress` ships formatted `speed`/`eta` strings; `queueStats.ts` re-parses them back to numbers. @@ -343,7 +343,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n item first (it reverses, assuming one-at-a-time prepends). So a selected playlist/channel downloads **N → 1**. Files are still named `001…NNN` correctly (by `playlistIndex`), so a partial run leaves the *last* videos on disk — surprising. Enqueue batches in reverse, or have `pump()` respect insertion order. -- [ ] **M33 — Documented enqueue batch cap doesn't exist.** [ipc.ts](src/shared/ipc.ts):629 and +- [x] **M33 — Documented enqueue batch cap doesn't exist.** [ipc.ts](src/shared/ipc.ts):629 and ROADMAP-PINCHFLAT claim the queue pulls "a batch at a time (e.g. 50)" via `MAX_ENQUEUE_BATCH` so it "never holds the whole collection at once," but no such cap exists — `enqueueItems` `addMany`s *every* selected item, so "Download all pending" on a 5,000-video channel puts 5,000 items in the store/queue @@ -426,7 +426,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n report includes all). - [ ] **L24 — `window.open(htmlUrl, '_blank')`** in SettingsView is the only `window.open` in the app (relies on the window-open handler); elsewhere external opens go through IPC/`shell`. -- [ ] **L25 — Taskbar effect over-fires.** `App.tsx` subscribes to the whole downloads store and +- [x] **L25 — Taskbar effect over-fires.** `App.tsx` subscribes to the whole downloads store and recomputes `summarizeQueue` + sends the taskbar IPC on *every* store change (each progress tick); throttle or dedupe by computed value. - [ ] **L26 — Third URL-sniffing path.** DownloadBar's drag-drop `firstUrl` is a separate @@ -442,7 +442,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n 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. -- [ ] **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. - [x] **L33 — `idCounter` fallback resets per launch.** The non-crypto `newId` fallback (`item-${++idCounter}`) restarts at 1 each run; the `Date.now()`-based fallbacks elsewhere can diff --git a/src/main/probe.ts b/src/main/probe.ts index 74f32e6..32ea2ce 100644 --- a/src/main/probe.ts +++ b/src/main/probe.ts @@ -1,7 +1,7 @@ import { execFile } from 'child_process' import { existsSync } from 'fs' import { getYtdlpPath } from './binaries' -import { fmtBytes } from './download' +import { fmtBytes } from './lib/formatters' import { cleanError } from './log' import { assertHttpUrl } from './url' import { entryUrl, fmtDuration, type RawEntry } from './indexerCore' diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2a48983..5b79507 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components' import { Sidebar, type TabValue } from './components/Sidebar' import { DownloadsView } from './components/DownloadsView' @@ -56,44 +56,68 @@ function App(): React.JSX.Element { // Mirror overall queue progress onto the Windows taskbar. Subscribe to the store // directly (not via a selector) so taskbar updates don't re-render the app. + // Compare against the last IPC call and skip when nothing changed — the store + // fires on every progress tick and this avoids one IPC roundtrip per tick (L25). useEffect(() => { + let lastFraction = -1 + let lastMode = '' + let lastBadge = -1 function push(items: ReturnType['items']): void { const s = summarizeQueue(items) const mode = s.active ? (s.failed > 0 ? 'error' : 'normal') : 'none' + if (s.progress === lastFraction && mode === lastMode && s.downloading === lastBadge) return + lastFraction = s.progress + lastMode = mode + lastBadge = s.downloading window.api?.setTaskbarProgress?.({ fraction: s.progress, mode, badgeCount: s.downloading }) } push(useDownloads.getState().items) return useDownloads.subscribe((st) => push(st.items)) }, []) - const paletteActions: PaletteAction[] = [ - { - id: 'go-downloads', - label: 'Go to Downloads', - hint: 'Navigate', - run: () => setTab('downloads') - }, - { id: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') }, - { id: 'go-history', label: 'Go to History', hint: 'Navigate', run: () => setTab('history') }, - { id: 'go-terminal', label: 'Go to Terminal', hint: 'Navigate', run: () => setTab('terminal') }, - { id: 'go-settings', label: 'Go to Settings', hint: 'Navigate', run: () => setTab('settings') }, - { - id: 'new-download', - label: 'New download', - hint: 'Focus URL', - run: () => { - setTab('downloads') - // Wait for the download bar to mount after the tab switch, then focus. - setTimeout(() => document.getElementById('aerofetch-url')?.focus(), 60) + // Memoized so CommandPalette sees a stable array reference between renders — + // only rebuilds when the theme label needs to change (L32). + const paletteActions = useMemo( + () => [ + { + id: 'go-downloads', + label: 'Go to Downloads', + hint: 'Navigate', + run: () => setTab('downloads') + }, + { id: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') }, + { id: 'go-history', label: 'Go to History', hint: 'Navigate', run: () => setTab('history') }, + { + id: 'go-terminal', + label: 'Go to Terminal', + hint: 'Navigate', + run: () => setTab('terminal') + }, + { + id: 'go-settings', + label: 'Go to Settings', + hint: 'Navigate', + run: () => setTab('settings') + }, + { + id: 'new-download', + label: 'New download', + hint: 'Focus URL', + run: () => { + setTab('downloads') + // Wait for the download bar to mount after the tab switch, then focus. + setTimeout(() => document.getElementById('aerofetch-url')?.focus(), 60) + } + }, + { + id: 'toggle-theme', + label: isDark ? 'Switch to light theme' : 'Switch to dark theme', + hint: 'Appearance', + run: () => updateSettings({ theme: isDark ? 'light' : 'dark' }) } - }, - { - id: 'toggle-theme', - label: isDark ? 'Switch to light theme' : 'Switch to dark theme', - hint: 'Appearance', - run: () => updateSettings({ theme: isDark ? 'light' : 'dark' }) - } - ] + ], + [isDark, setTab, updateSettings] + ) // AeroFetch's own version, shown in the sidebar. Loaded once over IPC. const [version, setVersion] = useState('') diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 07425f6..3085770 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -659,9 +659,9 @@ export interface BackupImportResult { // --- Sources & media-manager indexing (Pinchflat-style) --------------------- // See ROADMAP-PINCHFLAT.md. A Source (channel/playlist) is indexed once into a -// persisted list of MediaItem records; the live download queue then pulls from -// that list a batch at a time, so "download an entire channel" never has to hold -// the whole collection in the queue at once. +// persisted list of MediaItem records. When the user downloads pending items, +// all selected entries are enqueued at once — add a batch cap here if large +// channel queues (thousands of items) cause performance issues (M33). /** Whether a Source is a whole channel or a single playlist. */ export type SourceKind = 'channel' | 'playlist'