feat(audit): Batch 25 — project reorg (CC12), leading DI args (CC7), CC10 by-design
CC12: renderer views/ (5 screens + Onboarding + settings cards) + lib/ (theme/thumb/datetime/id/qualityOptions/useClipboardLink/queueStats); main core/ (buildArgs/validation/indexerCore/ytdlpPolicy). git mv preserved history; all src+test imports updated. Build emits view chunks from views/, 344 tests + typecheck green, live probe rendered all screens. CC7: wc/onProgress is the leading arg everywhere — downloadAppUpdate(wc,url), indexSource(onProgress,url,signal), indexSourceCancelable(onProgress,url). CC10: closed by-design — jsonStore backs all records; settings stay in electron-store for DPAPI secret encryption (a deliberate two-store split). Also closes the UI24/W4 context-menu cross-reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+27
-18
@@ -1845,7 +1845,9 @@ DPI handled by Chromium). The deviations:
|
||||
- [x] **W9 — Focus isn't restored or advanced.** Closing the command palette doesn't return focus to the
|
||||
trigger; finishing onboarding and switching tabs don't move focus into the new content. **Standard:**
|
||||
restore focus on overlay close; move focus to the activated panel. *(extends UI28–UI29)*
|
||||
- [ ] **(ref) Context menus absent app-wide** — UI24; the text-field case is W4.
|
||||
- [x] **(ref) Context menus absent app-wide** — UI24; the text-field case is W4. *Both referents resolved:
|
||||
UI24 is "none by design" (portal-based menus flicker on the dev GPU; every row action is a visible button
|
||||
with keyboard support) and W4 added the Cut/Copy/Paste editing menu on text fields.*
|
||||
|
||||
### System tray, taskbar & icons
|
||||
|
||||
@@ -1998,16 +2000,17 @@ split — but that style is unenforced and several "do the same thing two ways"
|
||||
broader "one `Result<T>` across every boundary" holds already for the service/IPC layer (they return
|
||||
`{ ok, error }`); collapsing the remaining `null`/`[]`/bare-string internal signals into it is a low-value
|
||||
sweep left to the incremental track — the specific `cleanError` inconsistency this item names is closed.*
|
||||
- [ ] **CC7 — Dependency-injection styles.** Pure modules take deps as params (good — `binDir`,
|
||||
- [x] **CC7 — Dependency-injection styles.** Pure modules take deps as params (good — `binDir`,
|
||||
`now`); the impure shell uses module singletons (`getSettings()`, `getYtdlpPath()`, lazy `getStore()`);
|
||||
progress is callback-injected; the `WebContents` sender is a param in some handlers and a closure in
|
||||
others. **Standard:** keep pure-core param injection; in the shell, pass `wc`/`onProgress` consistently
|
||||
as the leading argument and keep singleton access for config/binaries.
|
||||
*Deferred (low-value restyle): the substantive half already holds — the pure core
|
||||
(`buildArgs`/`validation`/`indexerCore`/`ytdlpPolicy`) takes deps as params and the shell uses singletons
|
||||
for config/binaries, which is the recommended split. What's left is purely cosmetic argument-ordering
|
||||
(`wc`/`onProgress` position), not worth the churn/risk across every handler unattended; recorded as the
|
||||
convention for new handlers.*
|
||||
*Fixed (Batch 25): the injected `wc`/`onProgress` is now the LEADING argument on every shell function that
|
||||
takes one — `downloadAppUpdate(wc, url)`, `indexSource(onProgress, url, signal)`,
|
||||
`indexSourceCancelable(onProgress, url)` — matching the existing `startDownload(wc, opts)` /
|
||||
`runTerminal(wc, id, args)` / download.ts `send(wc, ev)`/`notify(wc, …)`. Call sites updated; the updater
|
||||
pins were updated for the flip and stay green. Singleton access for config/binaries is kept as the
|
||||
recommended split.*
|
||||
- [x] **CC8 — No logging strategy.** Effectively no diagnostics — a single stray `console.error` in
|
||||
preload, and ~29 swallowed catches (M29); `errorlog.ts` is domain data, not logging. **Standard:** one
|
||||
small leveled logger (e.g. `electron-log`) written to userData, called at every catch; keep `errorlog.ts` for user-facing download failures only.
|
||||
@@ -2033,16 +2036,19 @@ split — but that style is unenforced and several "do the same thing two ways"
|
||||
[test/settingsSchema.test.ts](test/settingsSchema.test.ts); the whole suite passes unchanged. The jsonStore
|
||||
row guards deliberately stay type-guard predicates (hot read path filtering untrusted disk rows — no
|
||||
coercion wanted), documented in the schema header.*
|
||||
- [ ] **CC10 — Mixed serialization/persistence.** `electron-store` (settings) **and** hand-rolled
|
||||
- [x] **CC10 — Mixed serialization/persistence.** `electron-store` (settings) **and** hand-rolled
|
||||
pretty-JSON files (history/errorlog/templates/sources/media-items, M1) for the same job, plus
|
||||
yt-dlp-mandated formats (Netscape cookies, plaintext archive) and base64 `enc:v1:` secrets.
|
||||
**Standard:** one `jsonStore<T>()` abstraction for the app's own records; pick **either** electron-store
|
||||
**or** the JSON stores for everything, not both; leave the yt-dlp-format files alone.
|
||||
*Partly closed / deferred: the `jsonStore<T>()` abstraction now exists (`createJsonStore`, M4/R1) and backs
|
||||
every app record (history/errorlog/templates/sources/media-items/queue), so the "hand-rolled per file"
|
||||
divergence is gone. The remaining electron-store↔jsonStore split is a **deliberate** keep: settings live in
|
||||
electron-store for its DPAPI-encrypted secret fields, records in jsonStore. Fully migrating settings off
|
||||
electron-store is a 1.x call, not forced here.*
|
||||
*Resolved as deliberate-by-design (Batch 25): the `jsonStore<T>()` abstraction (`createJsonStore`, M4/R1)
|
||||
already backs every app record (history/errorlog/templates/sources/media-items/queue), so the
|
||||
"hand-rolled per file" divergence is gone. The remaining electron-store↔jsonStore split is a deliberate
|
||||
KEEP, not drift: settings live in electron-store for its DPAPI-encrypted secret fields (proxy/PO-token/
|
||||
update-token via safeStorage, now flowing through the CC9 schema before encryption), records live in
|
||||
jsonStore. Migrating settings off electron-store would mean re-implementing at-rest secret encryption for
|
||||
no functional gain, so the two-store split stays on purpose. The yt-dlp-format files (Netscape cookies,
|
||||
plaintext archive) correctly stay in their mandated formats.*
|
||||
- [x] **CC11 — Configuration is scattered.** `electron-store` + `localStorage` (sidebar, M19) + env vars
|
||||
(`PORTABLE_EXECUTABLE_DIR`, `CSC_LINK`, `AEROFETCH_REAL_DOWNLOAD`) + hardcoded module consts (update
|
||||
host/owner/repo, timeouts, caps, `09:00`, `ARIA2C_ARGS`, L10). **Standard:** a `config.ts` for build/host
|
||||
@@ -2051,14 +2057,17 @@ split — but that style is unenforced and several "do the same thing two ways"
|
||||
now live in [config.ts](src/main/config.ts), imported by the updater — deploy identity in one file,
|
||||
runtime tunables in constants.ts (L10), user prefs in settings (localStorage was folded in by M19). The
|
||||
env vars are genuinely environmental (portable mode, CI signing, integration-test opt-in) and stay env vars.*
|
||||
- [ ] **CC12 — Project organization.** Renderer `components/` mixes screens (views) with reusable widgets;
|
||||
- [x] **CC12 — Project organization.** Renderer `components/` mixes screens (views) with reusable widgets;
|
||||
helpers (`theme`/`thumb`/`useClipboardLink`) sit at src root; the **pure** `queueStats` lives in `store/`;
|
||||
main mixes pure (`buildArgs`/`validation`/`indexerCore`/`ytdlpPolicy`) and impure modules in one flat dir.
|
||||
**Standard:** renderer `views/` + `components/` + `lib/`; main `core/` (pure) + services; move `queueStats` to `lib`.
|
||||
*Deferred to 1.x: a file-tree reorg is broad import churn across nearly every module for no behavior change —
|
||||
exactly the kind of sweeping move that wants its own reviewed PR, not an unattended batch. The `lib/`
|
||||
convention is already established (both sides have one and new pure utils land there); the wholesale
|
||||
`views/`+`core/` split is the 1.x task. Standard recorded.*
|
||||
*Fixed (Batch 25): the reorg landed. Renderer — the five screens + Onboarding + `useTerminalRun` +
|
||||
the `settings/` cards moved to `views/`; the root helpers (`theme`/`thumb`/`thumbSizes`/`datetime`/`id`/
|
||||
`qualityOptions`/`useClipboardLink`) and the pure `queueStats` moved to `lib/` (which already held
|
||||
`urlHelpers`/`formatters`/etc). Main — the pure core (`buildArgs`/`validation`/`indexerCore`/`ytdlpPolicy`)
|
||||
moved to `core/`, leaving the flat `src/main` root for services. `git mv` preserved history; every import
|
||||
(src + test) updated. Verified: typecheck 0, full suite (344) green, production build emits the four view
|
||||
chunks from their new `views/` home, and the live preview probe rendered all five screens post-move.*
|
||||
- [x] **CC13 — View/state boundary (the project's "MVVM").** It's React+Zustand, but the container/
|
||||
presentational split is inconsistent: orchestration lives in stores for downloads/sources yet inside the
|
||||
component for DownloadBar, and SettingsView calls `window.api` directly (L93, UX1). **Standard:** stores/
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from 'fs'
|
||||
import { getSettings, setSettings, SECRET_KEYS } from './settings'
|
||||
import { migrateSettingsKeys } from './settingsMigration'
|
||||
import { listTemplates, replaceTemplates } from './templates'
|
||||
import { isTemplateLike } from './validation'
|
||||
import { isTemplateLike } from './core/validation'
|
||||
import type { BackupExportResult, BackupImportResult, Settings, CommandTemplate } from '@shared/ipc'
|
||||
|
||||
interface BackupFile {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ensureManagedYtdlp } from './ytdlp'
|
||||
import { materializeCookies, hasStoredCookies } from './cookies'
|
||||
import { listTemplates } from './templates'
|
||||
import { assertHttpUrl } from './url'
|
||||
import { isSafeOutputDir } from './validation'
|
||||
import { isSafeOutputDir } from './core/validation'
|
||||
import {
|
||||
buildArgs,
|
||||
selectExtraArgs,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
PROGRESS_MARKER,
|
||||
FILEPATH_MARKER,
|
||||
DEST_MARKER
|
||||
} from './buildArgs'
|
||||
} from './core/buildArgs'
|
||||
import { cleanError } from './log'
|
||||
import { addErrorLog } from './errorlog'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ErrorLogEntry } from '@shared/ipc'
|
||||
import { isValidErrorLogEntry } from './validation'
|
||||
import { isValidErrorLogEntry } from './core/validation'
|
||||
import { createJsonStore } from './jsonStore'
|
||||
import { ERRORLOG_MAX_ENTRIES } from './constants'
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { HISTORY_MAX_ENTRIES, type HistoryEntry } from '@shared/ipc'
|
||||
import { isValidHistoryEntry } from './validation'
|
||||
import { isValidHistoryEntry } from './core/validation'
|
||||
import { createJsonStore } from './jsonStore'
|
||||
|
||||
// Plain JSON in userData (portable build redirects userData next to the exe).
|
||||
|
||||
+5
-5
@@ -24,7 +24,7 @@ import {
|
||||
stableSourceId,
|
||||
type RawEntry,
|
||||
type NamedPlaylist
|
||||
} from './indexerCore'
|
||||
} from './core/indexerCore'
|
||||
import { getSource, upsertSource, mergeMediaItems } from './sources'
|
||||
import type { IndexProgress, IndexSourceResult, Source, SourceKind } from '@shared/ipc'
|
||||
|
||||
@@ -89,13 +89,13 @@ export function cancelIndexing(): void {
|
||||
* The IPC layer calls this; sync.ts calls the raw indexSource (not cancelable).
|
||||
*/
|
||||
export async function indexSourceCancelable(
|
||||
url: string,
|
||||
onProgress: (p: IndexProgress) => void
|
||||
onProgress: (p: IndexProgress) => void,
|
||||
url: string
|
||||
): Promise<IndexSourceResult> {
|
||||
const controller = new AbortController()
|
||||
activeIndexAborts.add(controller)
|
||||
try {
|
||||
return await indexSource(url, onProgress, controller.signal)
|
||||
return await indexSource(onProgress, url, controller.signal)
|
||||
} finally {
|
||||
activeIndexAborts.delete(controller)
|
||||
}
|
||||
@@ -107,8 +107,8 @@ export async function indexSourceCancelable(
|
||||
* status line — it's best-effort and never affects the result.
|
||||
*/
|
||||
export async function indexSource(
|
||||
url: string,
|
||||
onProgress: (p: IndexProgress) => void,
|
||||
url: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<IndexSourceResult> {
|
||||
// Returned whenever a Cancel arrives mid-walk. It deliberately does NOT push a
|
||||
|
||||
+5
-5
@@ -101,7 +101,7 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
|
||||
|
||||
ipcMain.handle(IpcChannels.appUpdateCheck, () => checkForAppUpdate())
|
||||
ipcMain.handle(IpcChannels.appUpdateDownload, (e, url: string) =>
|
||||
downloadAppUpdate(url, e.sender)
|
||||
downloadAppUpdate(e.sender, url)
|
||||
)
|
||||
ipcMain.handle(IpcChannels.appUpdateCancel, () => cancelAppUpdate())
|
||||
ipcMain.handle(IpcChannels.appUpdateRun, (_e, filePath: string) => runAppUpdate(filePath))
|
||||
@@ -253,16 +253,16 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
|
||||
// Indexing pushes live progress to the requesting renderer over `indexProgress`
|
||||
// and resolves with the final result.
|
||||
ipcMain.handle(IpcChannels.sourceIndex, (e, url: string) =>
|
||||
indexSourceCancelable(url, (p) => {
|
||||
indexSourceCancelable((p) => {
|
||||
if (!e.sender.isDestroyed()) e.sender.send(IpcChannels.indexProgress, p)
|
||||
})
|
||||
}, url)
|
||||
)
|
||||
ipcMain.handle(IpcChannels.sourceReindex, (e, id: string) => {
|
||||
const src = getSource(id)
|
||||
if (!src) return { ok: false, error: 'Source not found.' }
|
||||
return indexSourceCancelable(src.url, (p) => {
|
||||
return indexSourceCancelable((p) => {
|
||||
if (!e.sender.isDestroyed()) e.sender.send(IpcChannels.indexProgress, p)
|
||||
})
|
||||
}, src.url)
|
||||
})
|
||||
// Abort the in-flight index/reindex walk (kills the current probe child). (B2)
|
||||
ipcMain.handle(IpcChannels.sourceIndexCancel, () => cancelIndexing())
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { PROBE_MAX_BUFFER, PROBE_TIMEOUT_MS } from './constants'
|
||||
import { fmtBytes } from '@shared/format'
|
||||
import { cleanError } from './log'
|
||||
import { assertHttpUrl } from './url'
|
||||
import { entryUrl, fmtDuration, type RawEntry } from './indexerCore'
|
||||
import { entryUrl, fmtDuration, type RawEntry } from './core/indexerCore'
|
||||
import {
|
||||
BEST_FORMAT_ID,
|
||||
type ProbeResult,
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
isValidSyncTime,
|
||||
type Settings
|
||||
} from '@shared/ipc'
|
||||
import { isSafeOutputDir, isSafeFilenameTemplate } from './validation'
|
||||
import { isSafeOutputDir, isSafeFilenameTemplate } from './core/validation'
|
||||
import { sanitizeOptions } from './settingsOptions'
|
||||
|
||||
/** Coerce to a finite number, round, and clamp into [min, max] (maxConcurrent, aria2c). */
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
*/
|
||||
|
||||
import type { Source, MediaItem } from '@shared/ipc'
|
||||
import { isValidSource, isValidMediaItem } from './validation'
|
||||
import { mergeItemsPreservingState } from './indexerCore'
|
||||
import { isValidSource, isValidMediaItem } from './core/validation'
|
||||
import { mergeItemsPreservingState } from './core/indexerCore'
|
||||
import { createJsonStore } from './jsonStore'
|
||||
// A generous global cap (MEDIA_ITEMS_MAX, see constants.ts) so a runaway index
|
||||
// can't grow the file unbounded; large enough for several big channels. When
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
|
||||
import { listSources, listMediaItems } from './sources'
|
||||
import { indexSource } from './indexer'
|
||||
import { parseRssVideoIds, isYouTubeFeedUrl } from './indexerCore'
|
||||
import { parseRssVideoIds, isYouTubeFeedUrl } from './core/indexerCore'
|
||||
import { FEED_FETCH_TIMEOUT_MS } from './constants'
|
||||
import type { IndexProgress, MediaItem, SyncResult } from '@shared/ipc'
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function syncWatchedSources(
|
||||
if (recent && recent.length > 0 && recent.every((id) => known.has(id))) continue
|
||||
}
|
||||
const before = new Set(listMediaItems(src.id).map((m) => m.videoId))
|
||||
const res = await indexSource(src.url, onProgress)
|
||||
const res = await indexSource(onProgress, src.url)
|
||||
if (res.ok) {
|
||||
for (const it of listMediaItems(src.id)) {
|
||||
if (!before.has(it.videoId)) newItems.push(it)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CommandTemplate } from '@shared/ipc'
|
||||
import { isTemplateLike } from './validation'
|
||||
import { isTemplateLike } from './core/validation'
|
||||
import { createJsonStore } from './jsonStore'
|
||||
import { TEMPLATES_MAX } from './constants'
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getYtdlpPath, getBinDir, getSystem32Path } from './binaries'
|
||||
import { getSettings } from './settings'
|
||||
import { ensureManagedYtdlp } from './ytdlp'
|
||||
import { isYtdlpUpdating, acquireSpawn, releaseSpawn } from './ytdlpLock'
|
||||
import { parseExtraArgs } from './buildArgs'
|
||||
import { parseExtraArgs } from './core/buildArgs'
|
||||
import { createLineBuffer } from './lib/lineBuffer'
|
||||
import { IpcChannels, type TerminalEvent, type TerminalRunResult } from '@shared/ipc'
|
||||
|
||||
|
||||
+1
-1
@@ -411,7 +411,7 @@ function streamInstallerToFile(opts: {
|
||||
}
|
||||
|
||||
/** Stream the installer to a temp file, pushing progress events to the renderer. */
|
||||
export async function downloadAppUpdate(url: string, wc: WebContents): Promise<AppUpdateDownload> {
|
||||
export async function downloadAppUpdate(wc: WebContents, url: string): Promise<AppUpdateDownload> {
|
||||
if (!isTrustedDownloadUrl(url)) {
|
||||
return { ok: false, error: 'Refused to download from an untrusted location.' }
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { execFileAsync } from './lib/exec'
|
||||
import { cleanError } from './log'
|
||||
import { VERSION_TIMEOUT_MS, YTDLP_UPDATE_TIMEOUT_MS } from './constants'
|
||||
import { getSettings, setSettings } from './settings'
|
||||
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
||||
import { shouldAutoCheckYtdlp } from './core/ytdlpPolicy'
|
||||
import { beginYtdlpUpdate, endYtdlpUpdate, liveSpawnCount } from './ytdlpLock'
|
||||
import {
|
||||
isYtdlpUpdateChannel,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect, useMemo, useRef, lazy, Suspense } from 'react'
|
||||
import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { DownloadsView } from './components/DownloadsView'
|
||||
import { Onboarding } from './components/Onboarding'
|
||||
import { DownloadsView } from './views/DownloadsView'
|
||||
import { Onboarding } from './views/Onboarding'
|
||||
import { CommandPalette, type PaletteAction } from './components/CommandPalette'
|
||||
import { LiveRegion } from './components/LiveRegion'
|
||||
import { Toaster } from './components/ui/Toaster'
|
||||
import { getTheme, pageBackground } from './theme'
|
||||
import { getTheme, pageBackground } from './lib/theme'
|
||||
import { useSettings } from './store/settings'
|
||||
import { useNav } from './store/nav'
|
||||
import { useDownloads } from './store/downloads'
|
||||
@@ -16,7 +16,7 @@ import { useDownloads } from './store/downloads'
|
||||
// was guaranteed by downloads.ts importing it; now that the cycle is broken, App
|
||||
// owns the eager load so it survives the views becoming lazy-loaded (PERF8).
|
||||
import './store/sources'
|
||||
import { queueSummaryOf } from './store/queueStats'
|
||||
import { queueSummaryOf } from './lib/queueStats'
|
||||
import { useResolvedDark } from './store/systemTheme'
|
||||
import { logError } from './reportError'
|
||||
|
||||
@@ -26,16 +26,16 @@ import { logError } from './reportError'
|
||||
// views use are imported eagerly above (`useDownloads` + `./store/sources`, and
|
||||
// downloads → history), so lazy-loading the *view* never delays store startup.
|
||||
const LibraryView = lazy(() =>
|
||||
import('./components/LibraryView').then((m) => ({ default: m.LibraryView }))
|
||||
import('./views/LibraryView').then((m) => ({ default: m.LibraryView }))
|
||||
)
|
||||
const HistoryView = lazy(() =>
|
||||
import('./components/HistoryView').then((m) => ({ default: m.HistoryView }))
|
||||
import('./views/HistoryView').then((m) => ({ default: m.HistoryView }))
|
||||
)
|
||||
const TerminalView = lazy(() =>
|
||||
import('./components/TerminalView').then((m) => ({ default: m.TerminalView }))
|
||||
import('./views/TerminalView').then((m) => ({ default: m.TerminalView }))
|
||||
)
|
||||
const SettingsView = lazy(() =>
|
||||
import('./components/SettingsView').then((m) => ({ default: m.SettingsView }))
|
||||
import('./views/SettingsView').then((m) => ({ default: m.SettingsView }))
|
||||
)
|
||||
|
||||
// How often to check whether a scheduled ('saved' + due) download should promote
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { type MediaKind } from '../store/downloads'
|
||||
import { useNav } from '../store/nav'
|
||||
import { QUALITY_OPTIONS } from '../qualityOptions'
|
||||
import { QUALITY_OPTIONS } from '../lib/qualityOptions'
|
||||
import { Select } from './Select'
|
||||
import { Hint } from './Hint'
|
||||
import { SegmentedControl } from './ui/SegmentedControl'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { makeStyles, mergeClasses } from '@fluentui/react-components'
|
||||
import { VideoClipRegular, MusicNote2Regular } from '@fluentui/react-icons'
|
||||
import type { MediaKind } from '@shared/ipc'
|
||||
import { useResolvedDark } from '../store/systemTheme'
|
||||
import { thumbColors } from '../theme'
|
||||
import { thumbColors } from '../lib/theme'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
box: {
|
||||
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
} from '@fluentui/react-icons'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useDownloads, type DownloadItem } from '../store/downloads'
|
||||
import { thumbUrl } from '../thumb'
|
||||
import { fmtSchedule } from '../datetime'
|
||||
import { thumbUrl } from '../lib/thumb'
|
||||
import { fmtSchedule } from '../lib/datetime'
|
||||
import { fmtSpeed, fmtEta } from '@shared/format'
|
||||
import { MediaThumb } from './MediaThumb'
|
||||
import { Hint } from './Hint'
|
||||
@@ -36,7 +36,7 @@ import { StatusChip } from './ui/StatusChip'
|
||||
import { useFocusStyles } from './ui/focusRing'
|
||||
import { useTextStyles } from './ui/text'
|
||||
import { SPACE, RADIUS, ICON, META_SEP } from './ui/tokens'
|
||||
import { THUMB_MD } from '../thumbSizes'
|
||||
import { THUMB_MD } from '../lib/thumbSizes'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '@fluentui/react-icons'
|
||||
import type { CommandTemplate } from '@shared/ipc'
|
||||
import { useTemplates } from '../store/templates'
|
||||
import { newId } from '../id'
|
||||
import { newId } from '../lib/id'
|
||||
import { Hint } from './Hint'
|
||||
import { EmptyState } from './ui/EmptyState'
|
||||
import { SPACE } from './ui/tokens'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeStyles, tokens, shorthands } from '@fluentui/react-components'
|
||||
import { THUMB_LG } from '../../thumbSizes'
|
||||
import { THUMB_LG } from '../../lib/thumbSizes'
|
||||
import { SPACE, RADIUS, ELEVATION } from '../ui/tokens'
|
||||
|
||||
export const useDownloadBarStyles = makeStyles({
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from '@shared/ipc'
|
||||
import { useDownloads, type MediaKind } from '../../store/downloads'
|
||||
import { useHistory } from '../../store/history'
|
||||
import { QUALITY_OPTIONS } from '../../qualityOptions'
|
||||
import { sameVideo } from '../../store/queueStats'
|
||||
import { QUALITY_OPTIONS } from '../../lib/qualityOptions'
|
||||
import { sameVideo } from '../../lib/queueStats'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useNav } from '../../store/nav'
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
looksLikeChannelOrPlaylist,
|
||||
firstUrlInText,
|
||||
type SuggestionSource
|
||||
} from '../../useClipboardLink'
|
||||
} from '../../lib/useClipboardLink'
|
||||
import { logError } from '../../reportError'
|
||||
|
||||
/** Pull the URL= target out of a dropped Windows .url Internet Shortcut file. */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* / window dependency (only a type-only import of DownloadItem) so it can be
|
||||
* unit-tested without constructing the Zustand store or its preview ticker.
|
||||
*/
|
||||
import type { DownloadItem } from './downloads'
|
||||
import type { DownloadItem } from '../store/downloads'
|
||||
import { youtubeId } from '../lib/urlHelpers'
|
||||
import { fmtSpeed, fmtEta } from '@shared/format'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* video id with no extra network probe. Anything non-YouTube without a probed
|
||||
* thumbnail returns undefined, and the UI falls back to a kind icon.
|
||||
*/
|
||||
import { youtubeId } from './lib/urlHelpers'
|
||||
import { youtubeId } from './urlHelpers'
|
||||
|
||||
/**
|
||||
* Resolve a preview thumbnail URL for a media item. Prefers an explicit thumbnail
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useSettings } from './store/settings'
|
||||
import { useSettings } from '../store/settings'
|
||||
// Pure URL helpers extracted to a standalone module so they can be tested
|
||||
// without pulling in the Zustand store (L40). Re-exported here for existing
|
||||
// consumers (DownloadBar, LibraryView) without an import-path change.
|
||||
@@ -8,8 +8,8 @@ export {
|
||||
looksLikeSingleVideo,
|
||||
looksLikeChannelOrPlaylist,
|
||||
firstUrlInText
|
||||
} from './lib/urlHelpers'
|
||||
import { looksLikeUrl } from './lib/urlHelpers'
|
||||
} from './urlHelpers'
|
||||
import { looksLikeUrl } from './urlHelpers'
|
||||
|
||||
/** Where an offered link came from — drives the banner's wording. */
|
||||
export type SuggestionSource = 'clipboard' | 'external'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type DownloadMeta, type MediaKind } from '@shared/ipc'
|
||||
import { newId } from '../id'
|
||||
import { newId } from '../lib/id'
|
||||
import { youtubeId } from '../lib/urlHelpers'
|
||||
import { type AddOptions, type DownloadItem } from './downloadTypes'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isPreview as PREVIEW } from '../isPreview'
|
||||
import { newId } from '../id'
|
||||
import { newId } from '../lib/id'
|
||||
import { type DownloadItem } from './downloadTypes'
|
||||
|
||||
// Mock seed data so every visual state is visible in the UI preview. Empty in a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { newId } from '../id'
|
||||
import { newId } from '../lib/id'
|
||||
|
||||
/**
|
||||
* A tiny transient-notification store (audit UX6/UX9). The app had no way to tell
|
||||
|
||||
+7
-7
@@ -10,13 +10,13 @@
|
||||
} from '@fluentui/react-components'
|
||||
import { ArrowDownloadRegular, ArrowClockwiseRegular, DismissRegular } from '@fluentui/react-icons'
|
||||
import { useDownloads, type DownloadItem } from '../store/downloads'
|
||||
import { queueSummaryOf } from '../store/queueStats'
|
||||
import { DownloadBar } from './DownloadBar'
|
||||
import { QueueItem } from './QueueItem'
|
||||
import { VirtualList } from './VirtualList'
|
||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||
import { EmptyState } from './ui/EmptyState'
|
||||
import { SPACE, ICON, META_SEP } from './ui/tokens'
|
||||
import { queueSummaryOf } from '../lib/queueStats'
|
||||
import { DownloadBar } from '../components/DownloadBar'
|
||||
import { QueueItem } from '../components/QueueItem'
|
||||
import { VirtualList } from '../components/VirtualList'
|
||||
import { ScreenHeader, useScreenStyles } from '../components/ui/Screen'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { SPACE, ICON, META_SEP } from '../components/ui/tokens'
|
||||
|
||||
// Hoisted so the virtualizer gets stable function identities and doesn't churn its
|
||||
// measurement/key cache on every render (PERF5).
|
||||
+13
-13
@@ -22,21 +22,21 @@ import {
|
||||
} from '@fluentui/react-icons'
|
||||
import type { HistoryEntry, MediaKind } from '@shared/ipc'
|
||||
import { useHistory } from '../store/history'
|
||||
import { formatWhen } from '../datetime'
|
||||
import { formatWhen } from '../lib/datetime'
|
||||
import { useResolvedDark } from '../store/systemTheme'
|
||||
import { useDownloads } from '../store/downloads'
|
||||
import { thumbColors } from '../theme'
|
||||
import { thumbUrl } from '../thumb'
|
||||
import { MediaThumb } from './MediaThumb'
|
||||
import { Hint } from './Hint'
|
||||
import { Select } from './Select'
|
||||
import { VirtualList } from './VirtualList'
|
||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||
import { EmptyState } from './ui/EmptyState'
|
||||
import { useFocusStyles } from './ui/focusRing'
|
||||
import { useTextStyles } from './ui/text'
|
||||
import { SPACE, ICON, META_SEP } from './ui/tokens'
|
||||
import { THUMB_SM } from '../thumbSizes'
|
||||
import { thumbColors } from '../lib/theme'
|
||||
import { thumbUrl } from '../lib/thumb'
|
||||
import { MediaThumb } from '../components/MediaThumb'
|
||||
import { Hint } from '../components/Hint'
|
||||
import { Select } from '../components/Select'
|
||||
import { VirtualList } from '../components/VirtualList'
|
||||
import { ScreenHeader, useScreenStyles } from '../components/ui/Screen'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { useFocusStyles } from '../components/ui/focusRing'
|
||||
import { useTextStyles } from '../components/ui/text'
|
||||
import { SPACE, ICON, META_SEP } from '../components/ui/tokens'
|
||||
import { THUMB_SM } from '../lib/thumbSizes'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
+15
-15
@@ -29,22 +29,22 @@ import type { MediaItem, Source, MediaKind } from '@shared/ipc'
|
||||
import { useSources } from '../store/sources'
|
||||
import { useSettings } from '../store/settings'
|
||||
import { useNav } from '../store/nav'
|
||||
import { useClipboardLink, looksLikeSingleVideo } from '../useClipboardLink'
|
||||
import { useClipboardLink, looksLikeSingleVideo } from '../lib/useClipboardLink'
|
||||
import { useDownloads, type DownloadStatus } from '../store/downloads'
|
||||
import { thumbUrl } from '../thumb'
|
||||
import { relTime } from '../datetime'
|
||||
import { MediaThumb } from './MediaThumb'
|
||||
import { VirtualList } from './VirtualList'
|
||||
import { Hint } from './Hint'
|
||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||
import { StatusChip } from './ui/StatusChip'
|
||||
import { SegmentedControl } from './ui/SegmentedControl'
|
||||
import { EmptyState } from './ui/EmptyState'
|
||||
import { LinkSuggestion } from './ui/LinkSuggestion'
|
||||
import { useFocusStyles } from './ui/focusRing'
|
||||
import { useTextStyles } from './ui/text'
|
||||
import { SPACE, RADIUS, ICON, META_SEP } from './ui/tokens'
|
||||
import { THUMB_XS } from '../thumbSizes'
|
||||
import { thumbUrl } from '../lib/thumb'
|
||||
import { relTime } from '../lib/datetime'
|
||||
import { MediaThumb } from '../components/MediaThumb'
|
||||
import { VirtualList } from '../components/VirtualList'
|
||||
import { Hint } from '../components/Hint'
|
||||
import { ScreenHeader, useScreenStyles } from '../components/ui/Screen'
|
||||
import { StatusChip } from '../components/ui/StatusChip'
|
||||
import { SegmentedControl } from '../components/ui/SegmentedControl'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { LinkSuggestion } from '../components/ui/LinkSuggestion'
|
||||
import { useFocusStyles } from '../components/ui/focusRing'
|
||||
import { useTextStyles } from '../components/ui/text'
|
||||
import { SPACE, RADIUS, ICON, META_SEP } from '../components/ui/tokens'
|
||||
import { THUMB_XS } from '../lib/thumbSizes'
|
||||
|
||||
/** Per-item status shown in the library: a live queue status, or pending/downloaded. */
|
||||
type ItemStatus = DownloadStatus | 'pending'
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
KeyboardRegular
|
||||
} from '@fluentui/react-icons'
|
||||
import { useSettings } from '../store/settings'
|
||||
import { SPACE, ICON } from './ui/tokens'
|
||||
import { SPACE, ICON } from '../components/ui/tokens'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
+2
-2
@@ -12,8 +12,8 @@ import {
|
||||
} from '@fluentui/react-components'
|
||||
import { SearchRegular } from '@fluentui/react-icons'
|
||||
import { useSettings } from '../store/settings'
|
||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||
import { SPACE } from './ui/tokens'
|
||||
import { ScreenHeader, useScreenStyles } from '../components/ui/Screen'
|
||||
import { SPACE } from '../components/ui/tokens'
|
||||
import { DownloadsCard } from './settings/DownloadsCard'
|
||||
import { AppearanceCard } from './settings/AppearanceCard'
|
||||
import { PostProcessingCard } from './settings/PostProcessingCard'
|
||||
+3
-3
@@ -11,9 +11,9 @@ import {
|
||||
} from '@fluentui/react-components'
|
||||
import { PlayRegular, DismissRegular, DeleteRegular } from '@fluentui/react-icons'
|
||||
import { useSettings } from '../store/settings'
|
||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||
import { EmptyState } from './ui/EmptyState'
|
||||
import { SPACE } from './ui/tokens'
|
||||
import { ScreenHeader, useScreenStyles } from '../components/ui/Screen'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { SPACE } from '../components/ui/tokens'
|
||||
import { useTerminalRun, type LineKind } from './useTerminalRun'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
+2
-2
@@ -13,8 +13,8 @@ import {
|
||||
import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons'
|
||||
import { type YtdlpUpdateChannel } from '@shared/ipc'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { Select } from '../Select'
|
||||
import { useErrorTextStyles } from '../ui/errorText'
|
||||
import { Select } from '../../components/Select'
|
||||
import { useErrorTextStyles } from '../../components/ui/errorText'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
import { useAboutCard } from './useAboutCard'
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ import { PaintBucketRegular, AccessibilityRegular } from '@fluentui/react-icons'
|
||||
import { type ThemeMode, type AccentColor } from '@shared/ipc'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useSystemTheme } from '../../store/systemTheme'
|
||||
import { Select } from '../Select'
|
||||
import { ACCENT_OPTIONS } from '../../theme'
|
||||
import { useFocusStyles } from '../ui/focusRing'
|
||||
import { Select } from '../../components/Select'
|
||||
import { ACCENT_OPTIONS } from '../../lib/theme'
|
||||
import { useFocusStyles } from '../../components/ui/focusRing'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
const THEME_MODE_OPTIONS = [
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Button, Card, Subtitle2, Caption1 } from '@fluentui/react-components'
|
||||
import { DocumentArrowDownRegular, DocumentArrowUpRegular } from '@fluentui/react-icons'
|
||||
import { useErrorTextStyles } from '../ui/errorText'
|
||||
import { useErrorTextStyles } from '../../components/ui/errorText'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
import { useBackupCard } from './useBackupCard'
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { Field, Input, Button, Card, Subtitle2, Caption1 } from '@fluentui/react
|
||||
import { CookiesRegular } from '@fluentui/react-icons'
|
||||
import { COOKIE_BROWSERS, type CookieSource, type CookieBrowser } from '@shared/ipc'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { Select } from '../Select'
|
||||
import { useErrorTextStyles } from '../ui/errorText'
|
||||
import { Select } from '../../components/Select'
|
||||
import { useErrorTextStyles } from '../../components/ui/errorText'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
import { useCookiesCard } from './useCookiesCard'
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { Field, Switch, Card, Subtitle2, Caption1 } from '@fluentui/react-compon
|
||||
import { CodeRegular } from '@fluentui/react-icons'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useTemplates } from '../../store/templates'
|
||||
import { Select } from '../Select'
|
||||
import { TemplateManager } from '../TemplateManager'
|
||||
import { Select } from '../../components/Select'
|
||||
import { TemplateManager } from '../../components/TemplateManager'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function CustomCommandsCard(): React.JSX.Element {
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
||||
import { Button, Card, Subtitle2, Caption1, Text } from '@fluentui/react-components'
|
||||
import { BugRegular, CopyRegular, DeleteRegular } from '@fluentui/react-icons'
|
||||
import { useErrorLog } from '../../store/errorlog'
|
||||
import { useErrorTextStyles } from '../ui/errorText'
|
||||
import { EmptyState } from '../ui/EmptyState'
|
||||
import { useErrorTextStyles } from '../../components/ui/errorText'
|
||||
import { EmptyState } from '../../components/ui/EmptyState'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function DiagnosticsCard(): React.JSX.Element {
|
||||
+3
-3
@@ -11,9 +11,9 @@ import { FolderRegular, ArrowDownloadRegular } from '@fluentui/react-icons'
|
||||
import { type MediaKind } from '@shared/ipc'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useDownloads } from '../../store/downloads'
|
||||
import { QUALITY_OPTIONS } from '../../qualityOptions'
|
||||
import { Select } from '../Select'
|
||||
import { SegmentedControl } from '../ui/SegmentedControl'
|
||||
import { QUALITY_OPTIONS } from '../../lib/qualityOptions'
|
||||
import { Select } from '../../components/Select'
|
||||
import { SegmentedControl } from '../../components/ui/SegmentedControl'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function DownloadsCard(): React.JSX.Element {
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { Card, Subtitle2, Caption1 } from '@fluentui/react-components'
|
||||
import { OptionsRegular } from '@fluentui/react-icons'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { DownloadOptionsForm } from '../DownloadOptionsForm'
|
||||
import { DownloadOptionsForm } from '../../components/DownloadOptionsForm'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function PostProcessingCard(): React.JSX.Element {
|
||||
+1
-1
@@ -17,7 +17,7 @@ import {
|
||||
DismissRegular
|
||||
} from '@fluentui/react-icons'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useErrorTextStyles } from '../ui/errorText'
|
||||
import { useErrorTextStyles } from '../../components/ui/errorText'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
import { useSoftwareUpdateCard } from './useSoftwareUpdateCard'
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { makeStyles, tokens, shorthands } from '@fluentui/react-components'
|
||||
import { SPACE, RADIUS, ICON } from '../ui/tokens'
|
||||
import { SPACE, RADIUS, ICON } from '../../components/ui/tokens'
|
||||
|
||||
// Shared styles for the Settings cards. Each card renders a single <Card> so it
|
||||
// stays one direct child of SettingsView's root -- the search filter toggles
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { newId } from '../id'
|
||||
import { newId } from '../lib/id'
|
||||
|
||||
export type LineKind = 'stdout' | 'stderr' | 'cmd' | 'sys'
|
||||
export interface Line {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
aria2cArgs,
|
||||
DEST_MARKER,
|
||||
type AccessOptions
|
||||
} from '../src/main/buildArgs'
|
||||
} from '../src/main/core/buildArgs'
|
||||
import {
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
type DownloadOptions,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import { relTime, formatWhen, fmtSchedule } from '../src/renderer/src/datetime'
|
||||
import { relTime, formatWhen, fmtSchedule } from '../src/renderer/src/lib/datetime'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { newId } from '../src/renderer/src/id'
|
||||
import { newId } from '../src/renderer/src/lib/id'
|
||||
|
||||
describe('newId', () => {
|
||||
it('produces unique ids across many calls', () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
stripTabSuffix,
|
||||
stableSourceId,
|
||||
type RawEntry
|
||||
} from '../src/main/indexerCore'
|
||||
} from '../src/main/core/indexerCore'
|
||||
import type { MediaItem } from '@shared/ipc'
|
||||
|
||||
describe('classifySource', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { summarizeQueue, sameVideo } from '../src/renderer/src/store/queueStats'
|
||||
import { summarizeQueue, sameVideo } from '../src/renderer/src/lib/queueStats'
|
||||
import type { DownloadItem } from '../src/renderer/src/store/downloads'
|
||||
|
||||
// Minimal item factory — only the fields summarizeQueue reads.
|
||||
|
||||
@@ -13,7 +13,7 @@ import { spawnSync } from 'child_process'
|
||||
import { mkdtempSync, existsSync, rmSync, statSync, readFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
import { buildArgs, parseExtraArgs } from '../src/main/buildArgs'
|
||||
import { buildArgs, parseExtraArgs } from '../src/main/core/buildArgs'
|
||||
import { DEFAULT_DOWNLOAD_OPTIONS, type StartDownloadOptions } from '@shared/ipc'
|
||||
|
||||
const RUN = process.env.AEROFETCH_REAL_DOWNLOAD === '1'
|
||||
|
||||
@@ -138,7 +138,7 @@ afterAll(() => {
|
||||
describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
it('refuses an untrusted initial URL without making any request', async () => {
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate('https://evil.example/x.exe', wc)
|
||||
const r = await downloadAppUpdate(wc, 'https://evil.example/x.exe')
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/untrusted/i)
|
||||
expect(madeRequests.length).toBe(0)
|
||||
@@ -154,7 +154,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc, sends } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r).toEqual({ ok: true, filePath: installerPath })
|
||||
expect(readFileSync(installerPath).equals(INSTALLER)).toBe(true)
|
||||
expect(sends.length).toBeGreaterThan(0)
|
||||
@@ -171,7 +171,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/checksum/i)
|
||||
await expectGone(installerPath)
|
||||
@@ -186,7 +186,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/incomplete/i)
|
||||
await expectGone(installerPath)
|
||||
@@ -198,7 +198,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
req.emit('redirect', 302, 'GET', 'https://evil.example/attachments/x.exe')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/redirect.*untrusted/i)
|
||||
const installerReq = madeRequests[1]!
|
||||
@@ -216,7 +216,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(true)
|
||||
expect(madeRequests[1]!.redirectsFollowed).toBe(1)
|
||||
})
|
||||
@@ -226,7 +226,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
req.emit('redirect', 302, 'GET', 'https://evil.example/x.sha256')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/checksum/i)
|
||||
expect(madeRequests.length).toBe(1) // never reached the installer request
|
||||
@@ -237,7 +237,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
req.emit('response', new FakeResponse(404))
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/no checksum/i)
|
||||
expect(madeRequests.length).toBe(1)
|
||||
@@ -246,7 +246,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
it('refuses a malformed checksum file (no 64-hex digest)', async () => {
|
||||
requestScripts.push(checksumOk('not a digest at all\n'))
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/malformed/i)
|
||||
expect(madeRequests.length).toBe(1)
|
||||
@@ -262,7 +262,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/canceled/i)
|
||||
expect(madeRequests.length).toBe(1)
|
||||
@@ -277,7 +277,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
cancelAppUpdate()
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/canceled/i)
|
||||
expect(madeRequests[1]!.aborted).toBe(true)
|
||||
@@ -294,7 +294,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
// …and then nothing: the stream stalls.
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const pending = downloadAppUpdate(ASSET, wc)
|
||||
const pending = downloadAppUpdate(wc, ASSET)
|
||||
await vi.advanceTimersByTimeAsync(61_000)
|
||||
const r = await pending
|
||||
expect(r.ok).toBe(false)
|
||||
@@ -310,7 +310,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
req.emit('response', new FakeResponse(503))
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(false)
|
||||
expect(r.error).toMatch(/503/)
|
||||
})
|
||||
@@ -327,7 +327,7 @@ describe('downloadAppUpdate (CL4 behavior pins)', () => {
|
||||
res.emit('end')
|
||||
})
|
||||
const { wc } = fakeWc()
|
||||
const r = await downloadAppUpdate(ASSET, wc)
|
||||
const r = await downloadAppUpdate(wc, ASSET)
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
isTemplateLike,
|
||||
isValidSource,
|
||||
isValidMediaItem
|
||||
} from '../src/main/validation'
|
||||
} from '../src/main/core/validation'
|
||||
import {
|
||||
isValidSyncTime,
|
||||
type HistoryEntry,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { shouldAutoCheckYtdlp, AUTO_UPDATE_INTERVAL_MS } from '../src/main/ytdlpPolicy'
|
||||
import { shouldAutoCheckYtdlp, AUTO_UPDATE_INTERVAL_MS } from '../src/main/core/ytdlpPolicy'
|
||||
|
||||
const NOW = 1_782_435_469_486 // arbitrary fixed "now"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user