M23: replaceMediaItems wrote [...newItems, ...others] and the JSON store sliced
the combined list to MAX_ITEMS, so re-indexing one large source could
silently evict ANOTHER source's tail. Now other sources are always kept in
full and only the source being (re)indexed is capped to the remaining
budget (MAX_ITEMS - others.length).
M27: enqueueItems forced settings.defaultKind for every item, so a channel
couldn't be queued as audio without flipping the global default. Added an
optional `kind` override (falls back to the default) and a video/audio
SegmentedControl in the Library action row, seeded from defaultKind so
existing behavior is unchanged until toggled.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audio quality preset "Best (MP3)" named a format that's wrong whenever the
audio format is opus/flac/wav. Renamed it to "Best" so the label never
contradicts the chosen audioFormat (which is picked separately). Bitrate presets
stay as bitrates and are already ignored for lossless formats (M21).
- AUDIO_QUALITY_OPTIONS[0]: 'Best (MP3)' -> 'Best' (buildArgs already had a
`case 'Best'` returning '0', so arg generation is unchanged).
- Updated the main + renderer defaults and the preview/mock/test fixtures.
- Added a one-line migration in getSettings() that rewrites a stored legacy
'Best (MP3)' to 'Best' so existing users' dropdowns match.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LibraryView mapped url -> status by last-write, so when a URL had multiple
queue entries ("Download anyway"), the row reflected whichever item was
iterated last. Added a STATUS_PRIORITY ranking (completed > downloading >
queued > saved > paused > error > canceled) and keep the highest-priority
status per URL, so a row shows the most meaningful state of that video.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The completion side-effect (add to history unless private + markDownloaded on
the source MediaItem) was duplicated in applyEvent('done') and the preview
fake-ticker, and had already drifted (the preview copy omitted formatId/
formatHasAudio). Extracted one recordCompletion(item) helper that both call.
Also tightened a latent bug: the old applyEvent ran markDownloaded for any
'done' event with a mediaItemId, even when the item was canceled (the reducer
keeps canceled items as 'canceled'). recordCompletion now runs only when
status === 'completed', so a canceled collection item is no longer wrongly
marked downloaded (and will be retried on the next re-sync).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L119: Standardized empty-state copy on the "No X yet." pattern. Terminal
"Output will appear here." -> "No output yet."; Diagnostics
"No errors logged." -> "No errors yet."
L151: Range-dash convention is ASCII hyphen everywhere. Changed the lone
en-dash prose range ("2-3 is a good balance") to a hyphen, matching the
time-range placeholders (1:30-2:00).
L153: The three "Dismiss" close buttons now name what they dismiss --
"Dismiss suggested link" (DownloadBar + Library suggestion banners) and
"Dismiss duplicate warning" (DownloadBar dup row).
L95 left open: the Cookies-card "sign in"/"sign-in" usage is already correct
English (verb vs modifier); no change warranted.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L106: Established the convention — ArrowClockwise = retry/redo a local action
(retry-all, re-download, retry queue item); ArrowSync = network sync/
update (sync sources, reindex, check/apply updates). Flipped the two
misclassified sites (LibraryView "check for new" sync, SettingsView
"Update yt-dlp") to ArrowSync and dropped the now-unused imports.
L107: QueueItem queued-row Spinner was size="extra-tiny"; every other spinner
is "tiny". Standardized to "tiny".
L122: The app-update and yt-dlp-update buttons showed "Checking…"/"Updating…"
in the label AND a separate sibling Spinner. Folded the spinner into the
button icon (icon swaps to Spinner while busy), matching DownloadBar/
LibraryView, and removed the redundant sibling Spinner.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addressing issues found reviewing this session's audit commits:
REGRESSIONS FIXED
- updater.ts (L52): reverted the spawn(detached) installer launch. Raw
CreateProcess/spawn fails with ERROR_ELEVATION_REQUIRED if the NSIS build
flips to perMachine (the config comment invites this), and it silently
dropped shell.openPath's launch-error detection. Restored shell.openPath
(ShellExecute honors the elevation manifest) and kept the L52 goal by
replacing the 1500ms timer with setImmediate(app.quit).
- LibraryView.tsx (M15): native <button> doesn't inherit color (UA sets
ButtonText), so the source-card chevron (currentColor) would render wrong
in dark mode. Added color: colorNeutralForeground1 to cardHead.
INEFFECTIVE CHANGE REVERTED
- base.css (W18): forced-color-adjust:auto on * is the CSS default (no-op),
and [class*="backdrop"] never matches Fluent/Griffel's hashed atomic class
names. Reverted; W18 unmarked (needs real High-Contrast testing).
WEAK FIX REVERTED
- Onboarding.tsx (L67): "Skip" and "Get started" called the identical handler
on a single-screen onboarding. Removed the duplicate button; L67 unmarked
(real ask is a "show tips again" revisit affordance).
STYLE / CORRECTNESS
- Stripped UTF-8 BOMs accidentally added to Select/CommandPalette/App/
Onboarding by the PowerShell sanitizer; left pre-existing BOMs untouched.
- DownloadBar.tsx: merged the duplicate @shared/ipc import; removed a stray
double blank line.
- qualityOptions.ts (L29): restored the original `satisfies Record<MediaKind,
readonly string[]>` constraint + the noUncheckedIndexedAccess rationale
comment (the extraction had dropped both for a weaker `as const`).
- downloads.ts: removed double blank line left by the QUALITY_OPTIONS move.
- binaries.ts: corrected YTDLP_MISSING_MSG doc comment ("at startup" -> the
actual call sites).
typecheck + 242 tests + eslint all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
firstUrl() in DownloadBar was a local duplicate of looksLikeUrl iteration
(scanning multi-line text for the first valid URL). Promoted to
firstUrlInText() in src/renderer/src/lib/urlHelpers.ts alongside the other
URL utilities, re-exported from useClipboardLink.ts. DownloadBar now imports
firstUrlInText from there instead of maintaining a local copy.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L67: Onboarding now has a 'Skip' button alongside 'Get started', both in a
right-aligned flex row. Both call update({hasCompletedOnboarding:true})
so the behavior is the same — Skip just reads as optional for users who
want to get started immediately without reading the tips.
L86: HistoryView now clears any selected IDs that are no longer visible when
the filter (query or kind) changes. A useEffect on `filtered` intersects
the current selection with the new visible set and calls setSelected only
if the size actually changed (avoids re-renders when the filter doesn't
affect the selection).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.claude/launch.json was committed but VS Code reads launch configs from
.vscode/launch.json, so no tool was consuming it. Moved to the correct
location and removed the misplaced file from git tracking.
Also added .vscode/settings.json to .gitignore so local VS Code user
preferences don't end up tracked.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W18: Added explicit forced-color-adjust:auto on the universal selector so
the browser's HC mode can override all custom token colors. Added an
@media(forced-colors:active) rule that replaces the CommandPalette
backdrop's rgba(0,0,0,0.32) with the Canvas system color + a ButtonText
border, so the palette remains perceivable under Windows High Contrast.
W19: Already fixed in the SR8 commit (window title now reflects download
state via mainWindow.setTitle in the taskbarProgress handler). Marking
done here with the companion CSS change.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cleanError() strips leading 'ERROR:' from yt-dlp stderr before sending
errors to the UI and error log. The browser-preview seed entry in
store/errorlog.ts had the raw 'ERROR: [youtube]...' prefix, making the
preview inconsistent with what production shows. Removed the prefix from
the seed string so the preview and production rendering match.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window title was always 'AeroFetch' regardless of activity, so screen
readers and taskbar previews couldn't tell whether downloads were running.
The taskbarProgress IPC handler (which already fires on each progress tick)
now calls mainWindow.setTitle() alongside setProgressBar/setOverlayIcon:
- Idle: 'AeroFetch'
- Downloading: 'AeroFetch -- N downloads active'
- Error: 'AeroFetch -- Error'
This reuses the same overlay badge label string so the text is consistent
between the taskbar tooltip and the title bar.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W8: Added Ctrl+, as a standard Settings accelerator alongside the existing
Ctrl+K palette shortcut. The key handler now handles both in one listener
so there's a single window.addEventListener lifecycle.
W9: Command palette now restores focus to the previously focused element when
it closes. prePaletteRef stores document.activeElement at open time;
onClose fires a requestAnimationFrame focus-restore after unmounting the
palette, so the focus returns to the trigger (a sidebar nav button, the
URL input, etc.) rather than leaving focus stranded at the document body.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both interactive headers in LibraryView used role="button" on divs, which is
invalid when they contain child interactive elements (group header had nested
All/None and Download buttons).
- Group header (groupHead): split into an outer flex div + a native <button>
(groupToggle style) for the toggle/expand action + sibling Fluent Buttons
for All/None and Download. The toggle button gets aria-expanded and a
composite aria-label (title + item count). e.stopPropagation() removed
from the sibling buttons since they are no longer inside the toggle.
- Source card header (cardHead): converted from a role="button" div to a
native <button> with width:100%, border:none, and background:transparent.
aria-label set to source.title. tabIndex/onKeyDown removed (native button
handles keyboard by default).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both DownloadBar (renderer, drag-drop) and deeplink.ts (main, argv) had their
own regex to extract URL= from a Windows Internet Shortcut file -- with subtly
different patterns (/^\s*URL\s*=\s*(\S+)/ vs /^URL=(.+)$/).
Added parseUrlShortcutContent() to @shared/ipc. Both callers now delegate to
it and apply their own http/https guard (looksLikeUrl in renderer, asHttpUrl
in main). The local parseUrlFile wrapper in DownloadBar is kept as the
validation compositing point; it's now a one-liner.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
which assembles it directly from @shared/ipc. store/downloads.ts no longer
imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
updated to import from the new module.
L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
so the IPC response returns to the renderer before the process exits.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L19: App.tsx replaced `setTimeout(() => focus(), 60)` in the "New download"
palette action with `requestAnimationFrame(() => requestAnimationFrame(...))`
to wait exactly one React render + paint cycle rather than an arbitrary 60ms
timer that could race on slow machines.
L73: DownloadBar's probe button label changed from "Fetch" to "Check URL" (hint
and aria-label both updated). Placeholder text updated to match. This
distinguishes it from LibraryView's "Index" action, which catalogs an entire
channel/playlist, making both labels clearly meaningful in context.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L46: Introduced YTDLP_MISSING_MSG constant in binaries.ts. All four callers
(download, indexer, probe, ytdlp) now emit the same user-facing string:
"yt-dlp.exe is missing. Open Settings -> Software update to re-download it."
This removes newline-embedded messages and four divergent phrasings.
L71: Removed readOnly from videoDir/audioDir folder inputs in SettingsView.
Added onChange handler so users can paste or type a path directly without
being forced to use the Browse button. The path still flows through
applySettings() in main, which validates it.
L74: Accent color swatches were triple-labeled (aria-pressed + aria-label +
title). Replaced with a single aria-label that embeds "(selected)" state,
plus title for the hover tooltip. Removed the incorrect aria-pressed
(these are selection buttons, not toggles).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L6: Select.tsx adds a `size` prop ('medium' | 'large'). When size='large',
the select is 40px tall (matching Fluent size='large' Input/Button) with
slightly larger padding and font. DownloadBar quality/format selects now
use size='large' so they align with the row's large Input and buttons.
L12: CommandPalette keyboard navigation now scrolls the highlighted item into
view on each ArrowUp/ArrowDown press. Uses a listRef + data-sel attribute
so no DOM imperative tracking of individual buttons is needed.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
L22: DownloadOptionsForm accepts optional `kind` prop. When set, hides
audio-specific fields (Audio format) for video downloads and vice versa
(Video container, Preferred codec) for audio downloads. Settings passes
defaultKind so the defaults card shows only relevant options.
L24: Replace lone window.open('htmlUrl', '_blank') with a new openUrl IPC
channel (shell:open-url) that calls shell.openExternal after validating
the protocol is http/https. Consistent with how all other external opens
work in the app. Preload + main handler + mock updated.
L27: DownloadBar URL-field Enter is now smart -- if formats/playlist are
already loaded (or probe errored), Enter triggers download; otherwise
it probes. Eliminates the keyboard dead-end where Enter never downloads.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L14: base.css gets * { box-sizing: border-box } (removes ad-hoc swatch rule),
:focus-visible outline using the Fluent brand token color, and
-webkit-font-smoothing / -moz-osx-font-smoothing antialiasing.
L20: youtubePlayerClient Input now has a datalist of known client names
(web, web_safari, web_embedded, mweb, tv, ios, android) so users
get autocomplete suggestions. Still accepts free text for future clients.
SR10: Sidebar tagline changed from 'yt-dlp frontend' to 'Video downloader'
(plain-English, not developer shorthand).
L150: Standardized 'PO Token' capitalization -- SettingsView label now says
'YouTube PO Token (advanced)' consistently.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L8: TerminalView lines now carry a stable auto-increment id (keyed by that
instead of array index) so front-trimming at MAX_LOG_LINES doesn't cause
React to reuse wrong DOM nodes. SettingsView error list keyed by id alone
(not id+occurredAt, which was redundant since IDs are already unique).
SR9: DownloadOptionsForm -- rewrote 'Container for merged video' to a plain
description; rewrote format-sorting hint to remove raw '-S string' /
'yt-dlp syntax' jargon.
M20: All ProgressBars now carry aria-label -- QueueItem (downloading +
paused rows, keyed on title/URL), DownloadsView overall summary bar,
SettingsView app-update download bar.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sidebar collapsed state was an orphan in localStorage -- not backed up,
lost on portable installs, and inconsistent with every other preference.
Added sidebarCollapsed: boolean to the Settings type and DEFAULT_SETTINGS,
wired it through applySettings' boolean branch, and updated the renderer
store / main.tsx mock defaults. App.tsx now drives the toggle via
updateSettings({ sidebarCollapsed }) instead of localStorage.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L7: Canceled status chip now uses 'subtle' color (neutral) instead of 'warning'
(amber) -- distinguishing it from paused which remains amber.
M13: Proxy URL and YouTube PO-token inputs now use type="password" to mask
credentials, matching updateToken. Proxy URLs can carry user:pass; PO tokens
are opaque secrets.
M37: Rewrite two dev-facing hints -- youtubePlayerClient now says 'how AeroFetch
identifies itself to YouTube' instead of citing yt-dlp internals; filename
template says 'Controls how saved files are named' instead of 'yt-dlp output
template'. Remaining hints (cookie browser, aria2c, etc.) were already clean.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L23: Diagnostics error list now shows "Showing 20 of N errors." when there
are more than 20 logged errors.
L78: DownloadsView empty-state copy updated from "Paste a URL above to get
started" to "Paste a URL above, then click Download" -- matches the UI.
L79: SponsorBlock default categories expanded from ['sponsor'] to
['sponsor', 'selfpromo', 'interaction'] -- sponsor-only was silently
covering almost nothing; the three new defaults are universally unwanted.
Existing SB tests updated to pass explicit categories instead of using defaults.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L16: relTime now caps at weeks/months instead of unbounded 'N d ago' (adds w/mo
buckets). formatWhen omits the year when the timestamp is in the current year.
L75: Update-token Field in the Software-update card is hidden by default; only
shown when a token is already configured or an update-check error occurred.
L77: Adds a Re-check button next to the ffmpeg/ffprobe version display so a
previously-not-found binary can be re-detected without restarting the app.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L17: TemplateManager urlPattern field now validates the regex on each keystroke;
an error message appears below the input and Save is blocked until fixed.
L18: Terminal nav item is hidden when customCommandEnabled is false -- no more
dead-end gated view. Sidebar gains showTerminal prop; App.tsx reads the
setting and passes it through.
L56: SponsorBlock Categories Field shows a 'warning' validationMessage when the
toggle is ON but no categories are selected, making the silent no-op visible.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L54: DownloadBar preview image now tracks failed src in state (same pattern as
MediaThumb); on load error shows the kind icon fallback instead of broken img.
L61: Convert taskbarProgress from ipcMain.on/ipcRenderer.send to ipcMain.handle/
ipcRenderer.invoke so all IPC channels use the same request-response idiom.
App.tsx calls void to explicitly discard the resolved promise.
L63: audioQuality() now has an explicit 'Best' case alongside the bitrate strings;
unknown labels still fall back to '0' but the default branch is documented.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L49: Remove newline from missing-binary error message in download.ts; inline
error spans render it as literal newline, now one sentence with period.
L53: backup.ts exportBackup/importBackup/showMessageBox no longer use win!
non-null assertions -- each uses a guarded ternary (same pattern index.ts
already used for chooseFolder).
L59: Extract PAGE_BACKGROUND to src/shared/theme.ts; main/index.ts and
renderer/src/theme.ts both import from it -- no more "keep in sync" comment.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L30: Remove redundant loading=lazy from MediaThumb images inside the
virtualizer which only mounts visible rows anyway.
L44: electron-builder.yml now excludes the actual eslint.config.js and
.prettierrc rather than non-existent legacy .eslintrc/.prettierrc.yaml.
L60: IpcChannels.externalUrl renamed from external-url to app:external-url
to match every other channel name category prefix.
L90: settings.ts sanitizeOptions now uses (ARR as readonly string[]).includes
pattern consistently with setSettings; removes the as-never hack.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H3: probe.ts now imports fmtBytes from lib/formatters instead of download.ts,
breaking the circular probe->download dependency direction.
M33: ipc.ts comment corrected to reflect that no batch cap exists; notes the
known M33 issue for future implementation.
L25: App.tsx taskbar subscriber skips IPC when fraction/mode/badge unchanged,
eliminating redundant roundtrips on every progress tick.
L32: paletteActions wrapped in useMemo so CommandPalette sees a stable array
reference; only rebuilds when isDark changes.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H5: HistoryEntry now stores formatId+formatHasAudio; redownload passes them
back to addFromUrl so the original yt-dlp format is reused, not guessed.
Compound quality labels stripped to the preset prefix for old history.
L72: thumbnail now passed in redownload so re-queued rows keep their art.
H6: TerminalView log capped at MAX_LOG_LINES=2000 to prevent unbounded growth.
M21: --audio-quality omitted for lossless audioFormats (flac/wav).
L166: fmtEta hour rollover fixed in all 3 locations -- 2h00:00 not 120:00.
L165: queueStats formatSpeed precision aligned with fmtBytes.
L55: QueueItem suppresses sizeLabel when already in probed-format quality label.
L47: probeMeta null sends empty meta event so Resolving clears via SR6.
L68: notifiedBackground resets on window show for subsequent close-while-downloading.
L76: dup warning falls back to URL when title is still a placeholder.
L91: SettingsView onFormatSelect uses indexOf instead of unsafe tuple cast.
L98: Embed chapters field gets a hint text.
L108: BrowserWindow created with explicit title AeroFetch.
L37: pure formatters extracted to src/main/lib/formatters.ts and unit-tested.
L38: buildArgs test covers webm thumbnail suppression.
L39: CROP_SQUARE_PPA test is structural not exact-string.
L40: looksLikeUrl/looksLikeSingleVideo extracted to src/renderer/src/lib/urlHelpers.ts.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reformat from a one-off security report into a living checklist.
Adds the 2026-06-29 architectural review findings and a second
polish pass; all original security findings marked completed.
Items carry stable IDs for tracking across sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Encrypt proxy / youtubePoToken / updateToken on disk via safeStorage (DPAPI on Windows), with a one-time launch migration for legacy plaintext. Decrypted before reaching callers/renderer; backup export still writes clear, as documented. Harden updater token handling to refuse cross-origin redirects on the authenticated REST check.
Extract the clipboard-link logic from DownloadBar into the shared useClipboardLink hook: add an optional filter (library skips single-video links), an offer() for external aerofetch:// / .url links, and a source field driving banner wording. Add looksLikeSingleVideo plus its unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closing the window no longer kills in-progress downloads, the library gains the
same copied-link suggestion the downloads tab has, and the in-app updater can now
authenticate to a sign-in-required Gitea.
Background / tray:
- The window's close handler now hides to the tray (instead of quitting) whenever
a download is in flight, even if "Keep running in the tray" is off — quitting
was killing the spawned yt-dlp processes. A one-time notification explains the
app is still running. (download.ts exposes hasActiveDownloads().)
- tray.ts now falls back to an embedded icon when no build/icon.ico ships, so the
tray actually appears — previously an empty icon meant the tray was skipped and
a minimized window could be stranded with no way back.
- New "Start with Windows" setting (launchAtStartup) wired to
app.setLoginItemSettings, synced at startup and on toggle. Useful with
auto-download so watched channels stay current in the background.
- The "Keep running in the tray" hint now explains it also enables background
auto-download of new uploads.
Library clipboard detection:
- Extracted the downloads tab's clipboard watcher into a shared useClipboardLink
hook and used it in the library's add-source field, so a copied channel/playlist
link is offered there too.
Updater fix (works on a private / sign-in-required instance):
- Added an optional updateToken setting. When set, the updater sends it as a Gitea
Authorization header on the release check, the checksum fetch, and the installer
download — so "Check for updates" works where anonymous access is blocked (the
previous "could not reach the update server" case). Blank = anonymous, unchanged.
No token is ever shipped; it's only ever sent to the host-pinned update host.
Settings gains a masked "Update access token" field.
Typecheck clean; 187 tests pass; electron-vite build clean. New UI verified in the
browser preview (tray/startup toggles, token field, library copied-link banner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Taskbar progress bar: renderer pushes summarizeQueue aggregate over a new
taskbar:progress channel (App store subscription); setProgressBar normal/error
- System tray (src/main/tray.ts) + minimize-to-tray (Settings.minimizeToTray);
close hides to tray, isQuitting guards real exits; icon via getAppIconPath()
(build/icon.ico added to extraResources)
- Jump list: app.setUserTasks "Open AeroFetch" (thumbnail toolbar deferred)
- Settings search: filters the ~11 SettingsView cards live by text match
Overlay badge deferred (needs a drawn NativeImage). typecheck + test (192) +
build all clean. Roadmap: Phase O COMPLETE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold Phases L-P into ROADMAP.md: media editing (trim/cut, split-chapters,
metadata editing), queue/daily-use UX, power-user surface, Windows-native
integration, and reliability/trust. Cross-link from ROADMAP-PINCHFLAT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Library channel cards now collapse their playlist groups by default
(single-group sources auto-expand), and each group header has a
Download button that queues just that playlist's pending videos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a source-wide "Select all" across all playlist groups, and make one
click queue every indexed video (the previous 100-per-click cap is gone).
Bulk download skips items that are already downloaded or in flight, so it
can't enqueue duplicates, and it includes failed/canceled ones so it also
retries them.
Make the UI hold up at channel scale (1000s of videos): virtualize both
the Downloads queue and the Library item list with @tanstack/react-virtual
so only on-screen rows hit the DOM, memoize the queue row so they don't all
reconcile on every progress tick, and batch the enqueue so queuing a whole
channel stays O(n) instead of O(n^2).
- VirtualList: reusable windowed list that owns its own scroll container
- DownloadsView: virtualized, flex-filled queue
- LibraryView: groups flattened to rows; virtualized panel above 100 rows,
inline below so small sources are unchanged
- QueueItem: React.memo
- downloads store: addMany (one state update + one pump); sources:
enqueue the whole selection via addMany, no cap
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yt-dlp is now a managed, auto-updating binary rather than a static bundled
one. On launch AeroFetch seeds a writable copy under userData from the
bundled seed, self-heals it if it goes missing, and — throttled to once a
day — self-updates it to the configured channel (default: nightly). This
keeps the binary current so YouTube changes don't silently cause 403s, and
an app reinstall (or portable re-extraction) can no longer roll it back.
Settings shows an auto-update toggle, the live version, last-checked time,
and the channel selector.
Also surface the bundled ffmpeg/ffprobe versions in Settings (display only:
they have no self-update path and, unlike yt-dlp, don't break as sites
change, so there is no auto-update for them).
- binaries: split the read-only bundled seed from the managed userData copy
- ytdlp: ensureManagedYtdlp (seed + self-heal), startup auto-update runner
- ytdlpPolicy: pure once-a-day throttle decision (unit-tested)
- ffmpeg: parse ffmpeg/ffprobe -version for the Settings panel
- settings/ipc/preload/renderer: new settings, IPC channels, types, and UI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>