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
+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.