Fix H3/M33/L25/L32: fix probe dependency, correct ipc comment, throttle taskbar, memoize palette
H3: probe.ts now imports fmtBytes from lib/formatters instead of download.ts,
breaking the circular probe->download dependency direction.
M33: ipc.ts comment corrected to reflect that no batch cap exists; notes the
known M33 issue for future implementation.
L25: App.tsx taskbar subscriber skips IPC when fraction/mode/badge unchanged,
eliminating redundant roundtrips on every progress tick.
L32: paletteActions wrapped in useMemo so CommandPalette sees a stable array
reference; only rebuilds when isDark changes.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -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).
|
~20 useState), `DownloadBar.tsx` (858, ~17 state hooks), `store/downloads.ts` (614).
|
||||||
- [ ] **H2 — Consolidate scattered utilities.** `youtubeId` ×2; 4 YouTube-URL-parsing variants;
|
- [ ] **H2 — Consolidate scattered utilities.** `youtubeId` ×2; 4 YouTube-URL-parsing variants;
|
||||||
byte/speed/eta/duration formatting across 4 modules.
|
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.
|
with H2's shared formatter.
|
||||||
- [ ] **H4 — Carry raw numbers across the progress boundary.** `DownloadProgress` ships
|
- [ ] **H4 — Carry raw numbers across the progress boundary.** `DownloadProgress` ships
|
||||||
formatted `speed`/`eta` strings; `queueStats.ts` re-parses them back to numbers.
|
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
|
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
|
**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.
|
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
|
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*
|
"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
|
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).
|
report includes all).
|
||||||
- [ ] **L24 — `window.open(htmlUrl, '_blank')`** in SettingsView is the only `window.open` in the
|
- [ ] **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`.
|
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
|
recomputes `summarizeQueue` + sends the taskbar IPC on *every* store change (each progress
|
||||||
tick); throttle or dedupe by computed value.
|
tick); throttle or dedupe by computed value.
|
||||||
- [ ] **L26 — Third URL-sniffing path.** DownloadBar's drag-drop `firstUrl` is a separate
|
- [ ] **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).
|
list (the virtualizer only mounts visible rows).
|
||||||
- [ ] **L31 — Two theme switchers.** Sidebar shows a 3-way radio when expanded but a cycle button
|
- [ ] **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.
|
||||||
- [ ] **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
|
||||||
(`item-${++idCounter}`) restarts at 1 each run; the `Date.now()`-based fallbacks elsewhere can
|
(`item-${++idCounter}`) restarts at 1 each run; the `Date.now()`-based fallbacks elsewhere can
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { getYtdlpPath } from './binaries'
|
import { getYtdlpPath } from './binaries'
|
||||||
import { fmtBytes } from './download'
|
import { fmtBytes } from './lib/formatters'
|
||||||
import { cleanError } from './log'
|
import { cleanError } from './log'
|
||||||
import { assertHttpUrl } from './url'
|
import { assertHttpUrl } from './url'
|
||||||
import { entryUrl, fmtDuration, type RawEntry } from './indexerCore'
|
import { entryUrl, fmtDuration, type RawEntry } from './indexerCore'
|
||||||
|
|||||||
+52
-28
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components'
|
import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components'
|
||||||
import { Sidebar, type TabValue } from './components/Sidebar'
|
import { Sidebar, type TabValue } from './components/Sidebar'
|
||||||
import { DownloadsView } from './components/DownloadsView'
|
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
|
// 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.
|
// 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(() => {
|
useEffect(() => {
|
||||||
|
let lastFraction = -1
|
||||||
|
let lastMode = ''
|
||||||
|
let lastBadge = -1
|
||||||
function push(items: ReturnType<typeof useDownloads.getState>['items']): void {
|
function push(items: ReturnType<typeof useDownloads.getState>['items']): void {
|
||||||
const s = summarizeQueue(items)
|
const s = summarizeQueue(items)
|
||||||
const mode = s.active ? (s.failed > 0 ? 'error' : 'normal') : 'none'
|
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 })
|
window.api?.setTaskbarProgress?.({ fraction: s.progress, mode, badgeCount: s.downloading })
|
||||||
}
|
}
|
||||||
push(useDownloads.getState().items)
|
push(useDownloads.getState().items)
|
||||||
return useDownloads.subscribe((st) => push(st.items))
|
return useDownloads.subscribe((st) => push(st.items))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const paletteActions: PaletteAction[] = [
|
// Memoized so CommandPalette sees a stable array reference between renders —
|
||||||
{
|
// only rebuilds when the theme label needs to change (L32).
|
||||||
id: 'go-downloads',
|
const paletteActions = useMemo<PaletteAction[]>(
|
||||||
label: 'Go to Downloads',
|
() => [
|
||||||
hint: 'Navigate',
|
{
|
||||||
run: () => setTab('downloads')
|
id: 'go-downloads',
|
||||||
},
|
label: 'Go to Downloads',
|
||||||
{ id: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') },
|
hint: 'Navigate',
|
||||||
{ id: 'go-history', label: 'Go to History', hint: 'Navigate', run: () => setTab('history') },
|
run: () => setTab('downloads')
|
||||||
{ 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: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') },
|
||||||
{
|
{ id: 'go-history', label: 'Go to History', hint: 'Navigate', run: () => setTab('history') },
|
||||||
id: 'new-download',
|
{
|
||||||
label: 'New download',
|
id: 'go-terminal',
|
||||||
hint: 'Focus URL',
|
label: 'Go to Terminal',
|
||||||
run: () => {
|
hint: 'Navigate',
|
||||||
setTab('downloads')
|
run: () => setTab('terminal')
|
||||||
// Wait for the download bar to mount after the tab switch, then focus.
|
},
|
||||||
setTimeout(() => document.getElementById('aerofetch-url')?.focus(), 60)
|
{
|
||||||
|
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' })
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
{
|
[isDark, setTab, updateSettings]
|
||||||
id: 'toggle-theme',
|
)
|
||||||
label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
|
|
||||||
hint: 'Appearance',
|
|
||||||
run: () => updateSettings({ theme: isDark ? 'light' : 'dark' })
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// AeroFetch's own version, shown in the sidebar. Loaded once over IPC.
|
// AeroFetch's own version, shown in the sidebar. Loaded once over IPC.
|
||||||
const [version, setVersion] = useState('')
|
const [version, setVersion] = useState('')
|
||||||
|
|||||||
+3
-3
@@ -659,9 +659,9 @@ export interface BackupImportResult {
|
|||||||
|
|
||||||
// --- Sources & media-manager indexing (Pinchflat-style) ---------------------
|
// --- Sources & media-manager indexing (Pinchflat-style) ---------------------
|
||||||
// See ROADMAP-PINCHFLAT.md. A Source (channel/playlist) is indexed once into a
|
// 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
|
// persisted list of MediaItem records. When the user downloads pending items,
|
||||||
// that list a batch at a time, so "download an entire channel" never has to hold
|
// all selected entries are enqueued at once — add a batch cap here if large
|
||||||
// the whole collection in the queue at once.
|
// channel queues (thousands of items) cause performance issues (M33).
|
||||||
|
|
||||||
/** Whether a Source is a whole channel or a single playlist. */
|
/** Whether a Source is a whole channel or a single playlist. */
|
||||||
export type SourceKind = 'channel' | 'playlist'
|
export type SourceKind = 'channel' | 'playlist'
|
||||||
|
|||||||
Reference in New Issue
Block a user