diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index e737ce0..4ad973f 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -248,7 +248,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n [`datetime.ts`](src/renderer/src/datetime.ts) and imported by their views; unit-tested in `test/datetime.test.ts`.* - [ ] **M10 — `.url` shortcut parsing duplicated** — `parseUrlFile` (DownloadBar) vs `readUrlShortcut` (main/deeplink). Different `URL=` extractors for the same file format. -- [ ] **M11 — Inconsistent clipboard access.** Reads use `navigator.clipboard.readText` (paste +- [x] **M11 — Inconsistent clipboard access.** Reads use `navigator.clipboard.readText` (paste button) *and* `window.api.readClipboard` (suggestion watcher); writes use `navigator.clipboard.writeText` (copy report). Pick one strategy. - [x] **M12 — Shared `errorText` style.** `tokens.colorPaletteRedForeground1` is applied inline @@ -392,7 +392,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n ambiguous. - [x] **L8 — Index/compound list keys.** TerminalView keys log lines by array index; Settings Diagnostics keys by `id + occurredAt` while every other list keys by `id` alone. -- [ ] **L9 — Thumbnail box sizes not shared.** Four hand-tuned 16:9 boxes (120×68, 108×64, 72×44, +- [x] **L9 — Thumbnail box sizes not shared.** Four hand-tuned 16:9 boxes (120×68, 108×64, 72×44, 60×34) with no shared aspect/size constant. - [ ] **L10 — Scattered magic numbers.** Timeouts/caps spread across modules (probe 60s, indexer 180s, probeMeta 30s, update-idle 60s; MAX_ENTRIES 500/200, MAX_TEMPLATES 100, MAX_ITEMS 20000). @@ -434,7 +434,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n - [x] **L27 — DownloadBar Enter probes, not downloads.** Enter in the URL field triggers `fetchFormats`; there's no keyboard path to actually start the download (LibraryView's Enter runs its primary action). Inconsistent. -- [ ] **L28 — Default kind/quality applied once.** DownloadBar seeds from settings via a one-shot +- [x] **L28 — Default kind/quality applied once.** DownloadBar seeds from settings via a one-shot ref; changing the default in Settings while on the Downloads tab isn't reflected until reload. - [ ] **L29 — UI constant in the store.** `QUALITY_OPTIONS` lives in `store/downloads.ts` yet is a presentational constant imported by views — couples UI to the store module. diff --git a/src/renderer/src/components/DownloadBar.tsx b/src/renderer/src/components/DownloadBar.tsx index a5a2b47..ec1f09e 100644 --- a/src/renderer/src/components/DownloadBar.tsx +++ b/src/renderer/src/components/DownloadBar.tsx @@ -37,6 +37,7 @@ import { SegmentedControl } from './ui/SegmentedControl' import { IconButton } from './ui/IconButton' import { useClipboardLink, looksLikeUrl } from '../useClipboardLink' import { logError } from '../reportError' +import { THUMB_LG } from '../thumbSizes' /** First http(s) URL in a blob of dropped text (one per line; '#' comments skipped). */ function firstUrl(text: string): string | null { @@ -104,8 +105,8 @@ const useStyles = makeStyles({ }, previewThumb: { flexShrink: 0, - width: '120px', - height: '68px', + width: `${THUMB_LG.w}px`, + height: `${THUMB_LG.h}px`, objectFit: 'cover', display: 'flex', alignItems: 'center', @@ -315,15 +316,13 @@ export function DownloadBar(): React.JSX.Element { } } - // Apply the saved default format once, when persisted settings first arrive. - const appliedDefaults = useRef(false) + // When settings load or change, update kind/quality -- but only while the bar + // is idle (URL empty) so an in-progress setup isn't unexpectedly reset. useEffect(() => { - if (settingsLoaded && !appliedDefaults.current) { - appliedDefaults.current = true - setKind(defaultKind) - setQuality(defaultKind === 'audio' ? defaultAudioQuality : defaultVideoQuality) - } - }, [settingsLoaded, defaultKind, defaultVideoQuality, defaultAudioQuality]) + if (!settingsLoaded || url.trim()) return + setKind(defaultKind) + setQuality(defaultKind === 'audio' ? defaultAudioQuality : defaultVideoQuality) + }, [settingsLoaded, defaultKind, defaultVideoQuality, defaultAudioQuality, url]) // Probe state for the single-video format picker. const [info, setInfo] = useState(null) @@ -415,10 +414,10 @@ export function DownloadBar(): React.JSX.Element { async function paste(): Promise { try { - const text = await navigator.clipboard.readText() + const text = (await window.api?.readClipboard?.()) ?? '' if (text) onUrlChange(text.trim()) } catch { - /* clipboard blocked -- ignore in preview */ + /* ignore in preview */ } } diff --git a/src/renderer/src/components/HistoryView.tsx b/src/renderer/src/components/HistoryView.tsx index a466db9..7fc627f 100644 --- a/src/renderer/src/components/HistoryView.tsx +++ b/src/renderer/src/components/HistoryView.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { Text, Caption1, @@ -33,6 +33,7 @@ import { Hint } from './Hint' import { Select } from './Select' import { ScreenHeader, useScreenStyles } from './ui/Screen' import { useFocusStyles } from './ui/focusRing' +import { THUMB_SM } from '../thumbSizes' const useStyles = makeStyles({ root: { @@ -78,8 +79,8 @@ const useStyles = makeStyles({ }, thumb: { flexShrink: 0, - width: '72px', - height: '44px', + width: `${THUMB_SM.w}px`, + height: `${THUMB_SM.h}px`, display: 'flex', alignItems: 'center', justifyContent: 'center', diff --git a/src/renderer/src/components/LibraryView.tsx b/src/renderer/src/components/LibraryView.tsx index d8374d6..ea2ae5d 100644 --- a/src/renderer/src/components/LibraryView.tsx +++ b/src/renderer/src/components/LibraryView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Body1, Caption1, @@ -41,6 +41,7 @@ import { VirtualList } from './VirtualList' import { ScreenHeader, useScreenStyles } from './ui/Screen' import { StatusChip } from './ui/StatusChip' import { useFocusStyles } from './ui/focusRing' +import { THUMB_XS } from '../thumbSizes' // True in the standalone browser preview (no Electron preload). const PREVIEW = typeof window === 'undefined' || !window.electron @@ -178,8 +179,8 @@ const useStyles = makeStyles({ }, plainList: { display: 'flex', flexDirection: 'column' }, rowThumb: { - width: '60px', - height: '34px', + width: `${THUMB_XS.w}px`, + height: `${THUMB_XS.h}px`, ...shorthands.borderRadius(tokens.borderRadiusSmall) }, rowMain: { display: 'flex', flexDirection: 'column', minWidth: 0, flexGrow: 1 }, @@ -242,7 +243,7 @@ export function LibraryView(): React.JSX.Element { setConfirmRemoveId(null) }, [selectedSourceId]) // Offer a freshly-copied link the way the Downloads tab does, but skip single - // videos — a library source is a channel/playlist to sync, not a one-off. + // videos -- a library source is a channel/playlist to sync, not a one-off. const clip = useClipboardLink(url, (u) => !looksLikeSingleVideo(u)) const [selected, setSelected] = useState>(new Set()) const [batchNote, setBatchNote] = useState(null) @@ -372,7 +373,7 @@ export function LibraryView(): React.JSX.Element { setSelected(on ? new Set(actionableItems.map((it) => it.id)) : new Set()) } - // One click queues every chosen item — maxConcurrent gates how many actually + // One click queues every chosen item -- maxConcurrent gates how many actually // run, the rest wait in the queue. Selection clears since nothing is held back. function downloadSelected(): void { if (!selectedSourceId || selectedActionable.length === 0) return @@ -397,7 +398,7 @@ export function LibraryView(): React.JSX.Element { setBatchNote(`Queued ${n} download${n === 1 ? '' : 's'}.`) } - // One row of the item list — a playlist header or a video — shared by the + // One row of the item list -- a playlist header or a video -- shared by the // inline (small source) and virtualized (large source) render paths. function rowKey(row: LibRow): string { return row.kind === 'header' ? `h:${row.title}` : row.item.id @@ -406,7 +407,7 @@ export function LibraryView(): React.JSX.Element { function renderRow(row: LibRow): React.JSX.Element { if (row.kind === 'header') { const open = isGroupOpen(row.title) - // "All on" is judged over the ACTIONABLE rows only — downloaded rows aren't + // "All on" is judged over the ACTIONABLE rows only -- downloaded rows aren't // selectable, so they must not keep the group from reading as fully selected (M36). const groupActionableItems = row.items.filter(actionable) const groupActionable = groupActionableItems.length diff --git a/src/renderer/src/components/QueueItem.tsx b/src/renderer/src/components/QueueItem.tsx index 07b3137..803b14c 100644 --- a/src/renderer/src/components/QueueItem.tsx +++ b/src/renderer/src/components/QueueItem.tsx @@ -32,6 +32,7 @@ import { MediaThumb } from './MediaThumb' import { Hint } from './Hint' import { StatusChip } from './ui/StatusChip' import { useFocusStyles } from './ui/focusRing' +import { THUMB_MD } from '../thumbSizes' const useStyles = makeStyles({ root: { @@ -44,8 +45,8 @@ const useStyles = makeStyles({ }, thumb: { flexShrink: 0, - width: '108px', - height: '64px', + width: `${THUMB_MD.w}px`, + height: `${THUMB_MD.h}px`, display: 'flex', alignItems: 'center', justifyContent: 'center', diff --git a/src/renderer/src/thumbSizes.ts b/src/renderer/src/thumbSizes.ts new file mode 100644 index 0000000..4911e53 --- /dev/null +++ b/src/renderer/src/thumbSizes.ts @@ -0,0 +1,5 @@ +/** Shared 16:9 thumbnail box dimensions used across queue, history, and library views. */ +export const THUMB_LG = { w: 120, h: 68 } as const +export const THUMB_MD = { w: 108, h: 64 } as const +export const THUMB_SM = { w: 72, h: 44 } as const +export const THUMB_XS = { w: 60, h: 34 } as const