From 37687f987017a220e66689464ae1a8c260403c3c Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 24 Jun 2026 06:11:15 -0400 Subject: [PATCH] Separate per-kind video/audio folders, defaulting to Documents\Video and Documents\Audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing installs had outputDir persisted to the Downloads folder (auto-filled by earlier versions), so the v0.3.0 per-kind routing never triggered — downloads kept going to Downloads. Replace the single outputDir setting with independent videoDir and audioDir settings: - Settings model: drop Settings.outputDir; add videoDir + audioDir (both blank by default). A blank value routes that kind into Documents\Video / Documents\Audio; a chosen folder overrides it. The stale outputDir key in old settings files is simply ignored, so existing users now get the Documents defaults. - buildCommand routes by kind: per-download override → per-kind folder → default. - The renderer no longer sends a global outputDir, so main always routes by kind. - Settings: the single "Download folder" field becomes separate "Video folder" and "Audio folder" pickers, each with a Browse + Reset and a Documents\… placeholder. Store gains chooseDir(target) / clearDir(target). - Onboarding: replace the folder picker with a short note about the two default folders (changeable in Settings). - Bump version to 0.3.1. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- src/main/download.ts | 8 ++-- src/main/settings.ts | 20 ++++++--- src/renderer/src/components/Onboarding.tsx | 35 ++++------------ src/renderer/src/components/SettingsView.tsx | 44 ++++++++++++++++---- src/renderer/src/main.tsx | 7 ++-- src/renderer/src/store/downloads.ts | 3 +- src/renderer/src/store/settings.ts | 16 ++++--- src/shared/ipc.ts | 6 ++- 9 files changed, 85 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 867ec26..c1a88d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aerofetch", - "version": "0.3.0", + "version": "0.3.1", "description": "A yt-dlp frontend for Windows", "main": "./out/main/index.js", "author": "AeroFetch", diff --git a/src/main/download.ts b/src/main/download.ts index 8e70e15..2e4940a 100644 --- a/src/main/download.ts +++ b/src/main/download.ts @@ -166,10 +166,10 @@ function resolveExtraArgs(opts: StartDownloadOptions, settings: Settings): strin export function buildCommand(opts: StartDownloadOptions): string[] { const settings = getSettings() // Output dir resolution: a per-download override wins, then the user's explicit - // global folder (Settings → Download folder), and finally — when both are blank — - // the per-kind default (Documents\Video for video, Documents\Audio for audio). - const outDir = - opts.outputDir?.trim() || settings.outputDir?.trim() || getDefaultMediaDir(opts.kind) + // per-kind folder (Settings → Video/Audio folder), and finally — when that's + // blank — the per-kind default (Documents\Video for video, Documents\Audio for audio). + const perKindDir = (opts.kind === 'audio' ? settings.audioDir : settings.videoDir)?.trim() + const outDir = opts.outputDir?.trim() || perKindDir || getDefaultMediaDir(opts.kind) // A collection (media-manager) download is filed into // // - folders; an ordinary download uses the flat filenameTemplate. const filenameTemplate = settings.filenameTemplate?.trim() || '%(title)s.%(ext)s' diff --git a/src/main/settings.ts b/src/main/settings.ts index ea0b813..f99a049 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -17,7 +17,10 @@ import { } from '@shared/ipc' const DEFAULTS: Settings = { - outputDir: '', // blank = route by kind into Documents\Video / Documents\Audio (see getDefaultMediaDir) + // Both blank by default → downloads land in Documents\Video / Documents\Audio + // (see getDefaultMediaDir). A non-empty value is an explicit per-kind override. + videoDir: '', + audioDir: '', defaultKind: 'video', defaultVideoQuality: 'Best available', defaultAudioQuality: 'Best (MP3)', @@ -126,9 +129,9 @@ export function getSettings(): Settings { // `set`, so only write when something actually changed — otherwise this churns // the settings file on every read. (audit P1) const cur = s.store - // outputDir is intentionally left blank by default — an empty value routes each - // download into Documents\Video / Documents\Audio by kind (see buildCommand). - // Only an explicit user choice (Settings → Download folder) overrides that. + // videoDir/audioDir are intentionally left blank by default — an empty value + // routes that kind into Documents\Video / Documents\Audio (see buildCommand). + // Only an explicit user choice (Settings → folders) overrides that. // Migrate settings files that predate downloadOptions (or hold a partial one), // but only persist when sanitizing actually altered the stored value. const sanitized = sanitizeOptions(cur.downloadOptions) @@ -212,8 +215,13 @@ export function setSettings(partial: Partial<Settings>): Settings { // group (it merges field changes locally before calling setSettings). s.set('downloadOptions', sanitizeOptions(value)) break - case 'outputDir': - if (typeof value === 'string' && isSafeOutputDir(value.trim())) s.set('outputDir', value.trim()) + case 'videoDir': + case 'audioDir': + // An empty string is allowed — it clears the override and restores the + // Documents\Video / Documents\Audio default for that kind. + if (typeof value === 'string' && (value.trim() === '' || isSafeOutputDir(value.trim()))) { + s.set(key, value.trim()) + } break case 'filenameTemplate': if (typeof value === 'string' && isSafeFilenameTemplate(value.trim())) { diff --git a/src/renderer/src/components/Onboarding.tsx b/src/renderer/src/components/Onboarding.tsx index 4e76623..82697da 100644 --- a/src/renderer/src/components/Onboarding.tsx +++ b/src/renderer/src/components/Onboarding.tsx @@ -4,7 +4,6 @@ import { Body1, Caption1, Field, - Input, Button, makeStyles, tokens, @@ -12,7 +11,6 @@ import { } from '@fluentui/react-components' import { ArrowDownloadFilled, - FolderRegular, ClipboardPasteRegular, HistoryRegular, OptionsRegular, @@ -56,13 +54,8 @@ const useStyles = makeStyles({ justifyContent: 'center', fontSize: '26px' }, - folderRow: { - display: 'flex', - gap: '8px', - alignItems: 'flex-end' - }, - folderInput: { - flexGrow: 1 + folderNote: { + color: tokens.colorNeutralForeground3 }, tips: { display: 'flex', @@ -99,8 +92,6 @@ const TIPS: { icon: React.JSX.Element; text: string }[] = [ export function Onboarding(): React.JSX.Element { const styles = useStyles() - const outputDir = useSettings((s) => s.outputDir) - const chooseOutputDir = useSettings((s) => s.chooseOutputDir) const update = useSettings((s) => s.update) return ( @@ -118,22 +109,12 @@ export function Onboarding(): React.JSX.Element { audio. yt-dlp and ffmpeg are bundled, so there's nothing else to install. </Body1> - <Field - label="Download folder" - hint="Leave blank to sort downloads into Documents\Video and Documents\Audio by type, or pick a folder for everything." - > - <div className={styles.folderRow}> - <Input - readOnly - className={styles.folderInput} - value={outputDir} - placeholder="Documents\Video and Documents\Audio (by type)" - contentBefore={<FolderRegular />} - /> - <Button icon={<FolderRegular />} onClick={chooseOutputDir}> - Browse - </Button> - </div> + <Field label="Where downloads go"> + <Caption1 className={styles.folderNote}> + Videos save to your <strong>Documents\Video</strong> folder and audio to{' '} + <strong>Documents\Audio</strong>. You can point each to a different folder any time + in Settings. + </Caption1> </Field> <div className={styles.tips}> diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index cd8e58d..889d490 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -170,8 +170,10 @@ const UPDATE_CHANNEL_OPTIONS = [ export function SettingsView(): React.JSX.Element { const styles = useStyles() - const outputDir = useSettings((s) => s.outputDir) - const chooseOutputDir = useSettings((s) => s.chooseOutputDir) + const videoDir = useSettings((s) => s.videoDir) + const audioDir = useSettings((s) => s.audioDir) + const chooseDir = useSettings((s) => s.chooseDir) + const clearDir = useSettings((s) => s.clearDir) const defaultKind = useSettings((s) => s.defaultKind) const defaultVideoQuality = useSettings((s) => s.defaultVideoQuality) const defaultAudioQuality = useSettings((s) => s.defaultAudioQuality) @@ -329,20 +331,48 @@ export function SettingsView(): React.JSX.Element { </div> <Field - label="Download folder" - hint="Leave blank to sort downloads by type into Documents\Video and Documents\Audio. Choose a folder to send everything there instead." + label="Video folder" + hint="Where video downloads are saved. Leave blank to use Documents\Video." > <div className={styles.folderRow}> <Input readOnly className={styles.folderInput} - value={outputDir} - placeholder="Documents\Video and Documents\Audio (by type)" + value={videoDir} + placeholder="Documents\Video (default)" contentBefore={<FolderRegular />} /> - <Button icon={<FolderRegular />} onClick={chooseOutputDir}> + <Button icon={<FolderRegular />} onClick={() => chooseDir('videoDir')}> Browse </Button> + {videoDir && ( + <Button appearance="subtle" onClick={() => clearDir('videoDir')}> + Reset + </Button> + )} + </div> + </Field> + + <Field + label="Audio folder" + hint="Where audio downloads are saved. Leave blank to use Documents\Audio." + > + <div className={styles.folderRow}> + <Input + readOnly + className={styles.folderInput} + value={audioDir} + placeholder="Documents\Audio (default)" + contentBefore={<FolderRegular />} + /> + <Button icon={<FolderRegular />} onClick={() => chooseDir('audioDir')}> + Browse + </Button> + {audioDir && ( + <Button appearance="subtle" onClick={() => clearDir('audioDir')}> + Reset + </Button> + )} </div> </Field> diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 524de57..5b57416 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -11,7 +11,8 @@ import App from './App' // In the real Electron app this branch is skipped — preload provides window.api. if (import.meta.env.DEV && !window.api) { const MOCK_SETTINGS: Settings = { - outputDir: 'C:\\Users\\you\\Downloads', + videoDir: 'C:\\Users\\you\\Documents\\Video', + audioDir: 'C:\\Users\\you\\Documents\\Audio', defaultKind: 'video', defaultVideoQuality: 'Best available', defaultAudioQuality: 'Best (MP3)', @@ -121,11 +122,11 @@ if (import.meta.env.DEV && !window.api) { previewCommand: async (opts) => { await new Promise((r) => setTimeout(r, 300)) // simulate the main-process round-trip const extra = opts.extraArgs ? ` ${opts.extraArgs}` : '' + const dir = opts.kind === 'audio' ? MOCK_SETTINGS.audioDir : MOCK_SETTINGS.videoDir return { ok: true, command: - `yt-dlp.exe -f "bv*+ba/b" -o "${MOCK_SETTINGS.outputDir}\\%(title)s.%(ext)s"` + - `${extra} -- ${opts.url}` + `yt-dlp.exe -f "bv*+ba/b" -o "${dir}\\%(title)s.%(ext)s"` + `${extra} -- ${opts.url}` } }, updateYtdlp: async (channel) => { diff --git a/src/renderer/src/store/downloads.ts b/src/renderer/src/store/downloads.ts index d9e800c..14ce7ca 100644 --- a/src/renderer/src/store/downloads.ts +++ b/src/renderer/src/store/downloads.ts @@ -247,7 +247,8 @@ export const useDownloads = create<DownloadState>((set, get) => { url: item.url, kind: item.kind, quality: item.quality, - outputDir: useSettings.getState().outputDir || undefined, + // No outputDir here: main routes each download into the user's per-kind + // folder (Settings → Video/Audio folder), or the Documents\… default. formatId: item.formatId, formatHasAudio: item.formatHasAudio, options: item.options, diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 22a3ad5..bd5f490 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -5,7 +5,8 @@ import { DEFAULT_DOWNLOAD_OPTIONS, type Settings } from '@shared/ipc' const PREVIEW = typeof window === 'undefined' || !window.electron const FALLBACK: Settings = { - outputDir: PREVIEW ? 'C:\\Users\\you\\Downloads' : '', + videoDir: PREVIEW ? 'C:\\Users\\you\\Documents\\Video' : '', + audioDir: PREVIEW ? 'C:\\Users\\you\\Documents\\Audio' : '', defaultKind: 'video', defaultVideoQuality: 'Best available', defaultAudioQuality: 'Best (MP3)', @@ -35,7 +36,10 @@ interface SettingsState extends Settings { /** true once persisted settings have loaded (always true in preview) */ loaded: boolean update: (partial: Partial<Settings>) => void - chooseOutputDir: () => void + /** open the OS folder picker and store the result as the video or audio folder */ + chooseDir: (target: 'videoDir' | 'audioDir') => void + /** clear a per-kind folder override, restoring its Documents\… default */ + clearDir: (target: 'videoDir' | 'audioDir') => void } export const useSettings = create<SettingsState>((set, get) => ({ @@ -47,12 +51,14 @@ export const useSettings = create<SettingsState>((set, get) => ({ if (!PREVIEW) window.api.setSettings(partial).catch(() => {}) }, - chooseOutputDir: () => { + chooseDir: (target) => { if (PREVIEW) return window.api.chooseFolder().then((dir) => { - if (dir) get().update({ outputDir: dir }) + if (dir) get().update({ [target]: dir }) }) - } + }, + + clearDir: (target) => get().update({ [target]: '' }) })) // Load persisted settings on startup. diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index f7689db..0dc5aef 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -340,8 +340,10 @@ export type DownloadEvent = /** Persisted user settings (electron-store). */ export interface Settings { - /** absolute output directory (empty string resolves to the OS Downloads folder) */ - outputDir: string + /** where video downloads are saved; empty string = the default Documents\Video folder */ + videoDir: string + /** where audio downloads are saved; empty string = the default Documents\Audio folder */ + audioDir: string defaultKind: MediaKind defaultVideoQuality: string defaultAudioQuality: string