Fix L9/L28/M11: shared thumb sizes, live defaults, clipboard IPC
L9: Extract four thumbnail box dimensions into src/renderer/src/thumbSizes.ts
(THUMB_LG 120x68, THUMB_MD 108x64, THUMB_SM 72x44, THUMB_XS 60x34).
DownloadBar, QueueItem, HistoryView, and LibraryView all import and use
them instead of hard-coded strings -- one place to change the 16:9 sizing.
L28: DownloadBar now reflects Settings changes to defaultKind/defaultQuality
in real time, but only while the URL field is empty (idle state). A URL
typed or probed in progress is never discarded by a settings change.
M11: Clipboard reads now consistently use window.api.readClipboard (IPC) in
the paste button, matching the clipboard-watcher hook. navigator.clipboard
is no longer used for reads anywhere in the renderer.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -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.
|
||||
|
||||
@@ -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
|
||||
if (!settingsLoaded || url.trim()) return
|
||||
setKind(defaultKind)
|
||||
setQuality(defaultKind === 'audio' ? defaultAudioQuality : defaultVideoQuality)
|
||||
}
|
||||
}, [settingsLoaded, defaultKind, defaultVideoQuality, defaultAudioQuality])
|
||||
}, [settingsLoaded, defaultKind, defaultVideoQuality, defaultAudioQuality, url])
|
||||
|
||||
// Probe state for the single-video format picker.
|
||||
const [info, setInfo] = useState<MediaInfo | null>(null)
|
||||
@@ -415,10 +414,10 @@ export function DownloadBar(): React.JSX.Element {
|
||||
|
||||
async function paste(): Promise<void> {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Set<string>>(new Set())
|
||||
const [batchNote, setBatchNote] = useState<string | null>(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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user