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:
@@ -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> =>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
'### What’s 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) =>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user