feat(audit): Batch 23 — settings key renames with migration (CC1) + config.ts (CC11)
CC1: customCommandEnabled→enableCustomCommands, downloadArchive→ useDownloadArchive, clipboardWatch→watchClipboard, sidebarCollapsed→ isSidebarCollapsed, hardwareAcceleration→useHardwareAcceleration, videoDir/audioDir→videoFolder/audioFolder (+ chooseDir/clearDir store actions → chooseFolder/clearFolder). Rename map in settingsMigration.ts (pure, unit-tested incl. pre-rename backup fixture), applied as an idempotent on-disk shim at store open and on backup import so old settings.json and old backups both keep working. CC11: update host/owner/repo + release API moved to src/main/config.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ function App(): React.JSX.Element {
|
||||
const theme = useSettings((s) => s.theme)
|
||||
const accentColor = useSettings((s) => s.accentColor)
|
||||
const updateSettings = useSettings((s) => s.update)
|
||||
const showTerminal = useSettings((s) => s.customCommandEnabled)
|
||||
const showTerminal = useSettings((s) => s.enableCustomCommands)
|
||||
const isDark = useResolvedDark()
|
||||
const tab = useNav((s) => s.tab)
|
||||
const setTab = useNav((s) => s.setTab)
|
||||
@@ -214,9 +214,9 @@ function App(): React.JSX.Element {
|
||||
window.api?.getAppVersion?.().then(setVersion).catch(logError('getAppVersion'))
|
||||
}, [])
|
||||
|
||||
const collapsed = useSettings((s) => s.sidebarCollapsed)
|
||||
const collapsed = useSettings((s) => s.isSidebarCollapsed)
|
||||
function toggleCollapsed(): void {
|
||||
updateSettings({ sidebarCollapsed: !collapsed })
|
||||
updateSettings({ isSidebarCollapsed: !collapsed })
|
||||
}
|
||||
// Gate on `loaded` so a returning user's real settings never get clobbered
|
||||
// by a one-frame flash of the (default-false) onboarding state.
|
||||
|
||||
@@ -139,9 +139,9 @@ const TIPS: { icon: React.JSX.Element; text: string }[] = [
|
||||
export function Onboarding(): React.JSX.Element {
|
||||
const styles = useStyles()
|
||||
const update = useSettings((s) => s.update)
|
||||
const videoDir = useSettings((s) => s.videoDir)
|
||||
const audioDir = useSettings((s) => s.audioDir)
|
||||
const chooseDir = useSettings((s) => s.chooseDir)
|
||||
const videoFolder = useSettings((s) => s.videoFolder)
|
||||
const audioFolder = useSettings((s) => s.audioFolder)
|
||||
const chooseFolder = useSettings((s) => s.chooseFolder)
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -167,11 +167,15 @@ export function Onboarding(): React.JSX.Element {
|
||||
<VideoClipRegular className={styles.folderIcon} />
|
||||
<div className={styles.folderCol}>
|
||||
<Caption1 className={styles.folderLabel}>Video</Caption1>
|
||||
<Caption1 className={styles.folderPath} title={videoDir || 'Documents\\Video'}>
|
||||
{videoDir || 'Documents\\Video'}
|
||||
<Caption1 className={styles.folderPath} title={videoFolder || 'Documents\\Video'}>
|
||||
{videoFolder || 'Documents\\Video'}
|
||||
</Caption1>
|
||||
</div>
|
||||
<Button size="small" icon={<FolderRegular />} onClick={() => chooseDir('videoDir')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FolderRegular />}
|
||||
onClick={() => chooseFolder('videoFolder')}
|
||||
>
|
||||
Choose…
|
||||
</Button>
|
||||
</div>
|
||||
@@ -179,11 +183,15 @@ export function Onboarding(): React.JSX.Element {
|
||||
<MusicNote2Regular className={styles.folderIcon} />
|
||||
<div className={styles.folderCol}>
|
||||
<Caption1 className={styles.folderLabel}>Audio</Caption1>
|
||||
<Caption1 className={styles.folderPath} title={audioDir || 'Documents\\Audio'}>
|
||||
{audioDir || 'Documents\\Audio'}
|
||||
<Caption1 className={styles.folderPath} title={audioFolder || 'Documents\\Audio'}>
|
||||
{audioFolder || 'Documents\\Audio'}
|
||||
</Caption1>
|
||||
</div>
|
||||
<Button size="small" icon={<FolderRegular />} onClick={() => chooseDir('audioDir')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FolderRegular />}
|
||||
onClick={() => chooseFolder('audioFolder')}
|
||||
>
|
||||
Choose…
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -61,13 +61,13 @@ const useStyles = makeStyles({
|
||||
|
||||
/**
|
||||
* Built-in yt-dlp terminal (Phase N): type raw yt-dlp args, run the bundled
|
||||
* binary, and watch its output stream. Gated on the customCommandEnabled consent
|
||||
* binary, and watch its output stream. Gated on the enableCustomCommands consent
|
||||
* flag (enforced again in main -- see src/main/terminal.ts).
|
||||
*/
|
||||
export function TerminalView(): React.JSX.Element {
|
||||
const styles = useStyles()
|
||||
const screen = useScreenStyles()
|
||||
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
||||
const enableCustomCommands = useSettings((s) => s.enableCustomCommands)
|
||||
const updateSettings = useSettings((s) => s.update)
|
||||
|
||||
// View-model hook owns the run/stream/stop orchestration + IPC (L93/CC13).
|
||||
@@ -101,7 +101,7 @@ export function TerminalView(): React.JSX.Element {
|
||||
}
|
||||
/>
|
||||
|
||||
{!customCommandEnabled && (
|
||||
{!enableCustomCommands && (
|
||||
<div className={styles.gate}>
|
||||
<Body1>
|
||||
The terminal runs the bundled yt-dlp with your own arguments — part of custom
|
||||
@@ -110,7 +110,7 @@ export function TerminalView(): React.JSX.Element {
|
||||
<Button
|
||||
appearance="primary"
|
||||
size="small"
|
||||
onClick={() => updateSettings({ customCommandEnabled: true })}
|
||||
onClick={() => updateSettings({ enableCustomCommands: true })}
|
||||
>
|
||||
Enable custom commands
|
||||
</Button>
|
||||
@@ -132,7 +132,7 @@ export function TerminalView(): React.JSX.Element {
|
||||
}}
|
||||
placeholder="--version (Ctrl+Enter to run)"
|
||||
resize="vertical"
|
||||
disabled={!customCommandEnabled}
|
||||
disabled={!enableCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
@@ -145,7 +145,7 @@ export function TerminalView(): React.JSX.Element {
|
||||
appearance="primary"
|
||||
icon={<PlayRegular />}
|
||||
onClick={run}
|
||||
disabled={!customCommandEnabled || !args.trim()}
|
||||
disabled={!enableCustomCommands || !args.trim()}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function AppearanceCard(): React.JSX.Element {
|
||||
const focus = useFocusStyles()
|
||||
const theme = useSettings((s) => s.theme)
|
||||
const accentColor = useSettings((s) => s.accentColor)
|
||||
const hardwareAcceleration = useSettings((s) => s.hardwareAcceleration)
|
||||
const useHardwareAcceleration = useSettings((s) => s.useHardwareAcceleration)
|
||||
const highContrast = useSystemTheme((s) => s.shouldUseHighContrastColors)
|
||||
const update = useSettings((s) => s.update)
|
||||
// Store action wraps the IPC call (L93/CC13: no window.api in components).
|
||||
@@ -105,8 +105,8 @@ export function AppearanceCard(): React.JSX.Element {
|
||||
hint="Uses the GPU to draw the window (takes effect after you restart AeroFetch). Off by default: some older GPUs render the window blank with this on — turn it back off if that happens."
|
||||
>
|
||||
<Switch
|
||||
checked={hardwareAcceleration}
|
||||
onChange={(_, d) => update({ hardwareAcceleration: d.checked })}
|
||||
checked={useHardwareAcceleration}
|
||||
onChange={(_, d) => update({ useHardwareAcceleration: d.checked })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function CustomCommandsCard(): React.JSX.Element {
|
||||
const styles = useSettingsStyles()
|
||||
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
||||
const enableCustomCommands = useSettings((s) => s.enableCustomCommands)
|
||||
const defaultTemplateId = useSettings((s) => s.defaultTemplateId)
|
||||
const update = useSettings((s) => s.update)
|
||||
const templates = useTemplates((s) => s.templates)
|
||||
@@ -30,12 +30,12 @@ export function CustomCommandsCard(): React.JSX.Element {
|
||||
hint="Applies the default template below to every new download (still overridable per download)."
|
||||
>
|
||||
<Switch
|
||||
checked={customCommandEnabled}
|
||||
onChange={(_, d) => update({ customCommandEnabled: d.checked })}
|
||||
checked={enableCustomCommands}
|
||||
onChange={(_, d) => update({ enableCustomCommands: d.checked })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{customCommandEnabled && (
|
||||
{enableCustomCommands && (
|
||||
<Field label="Default template">
|
||||
<Select
|
||||
value={defaultTemplateId ?? 'none'}
|
||||
|
||||
@@ -18,15 +18,15 @@ import { useSettingsStyles } from './settingsStyles'
|
||||
|
||||
export function DownloadsCard(): React.JSX.Element {
|
||||
const styles = useSettingsStyles()
|
||||
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 videoFolder = useSettings((s) => s.videoFolder)
|
||||
const audioFolder = useSettings((s) => s.audioFolder)
|
||||
const chooseFolder = useSettings((s) => s.chooseFolder)
|
||||
const clearFolder = useSettings((s) => s.clearFolder)
|
||||
const defaultKind = useSettings((s) => s.defaultKind)
|
||||
const defaultVideoQuality = useSettings((s) => s.defaultVideoQuality)
|
||||
const defaultAudioQuality = useSettings((s) => s.defaultAudioQuality)
|
||||
const maxConcurrent = useSettings((s) => s.maxConcurrent)
|
||||
const clipboardWatch = useSettings((s) => s.clipboardWatch)
|
||||
const watchClipboard = useSettings((s) => s.watchClipboard)
|
||||
const notifyOnComplete = useSettings((s) => s.notifyOnComplete)
|
||||
const minimizeToTray = useSettings((s) => s.minimizeToTray)
|
||||
const launchAtStartup = useSettings((s) => s.launchAtStartup)
|
||||
@@ -58,16 +58,16 @@ export function DownloadsCard(): React.JSX.Element {
|
||||
<div className={styles.folderRow}>
|
||||
<Input
|
||||
className={styles.folderInput}
|
||||
value={videoDir}
|
||||
value={videoFolder}
|
||||
placeholder="Documents\Video (default)"
|
||||
contentBefore={<FolderRegular />}
|
||||
onChange={(_, d) => update({ videoDir: d.value })}
|
||||
onChange={(_, d) => update({ videoFolder: d.value })}
|
||||
/>
|
||||
<Button icon={<FolderRegular />} onClick={() => chooseDir('videoDir')}>
|
||||
<Button icon={<FolderRegular />} onClick={() => chooseFolder('videoFolder')}>
|
||||
Browse…
|
||||
</Button>
|
||||
{videoDir && (
|
||||
<Button appearance="subtle" onClick={() => clearDir('videoDir')}>
|
||||
{videoFolder && (
|
||||
<Button appearance="subtle" onClick={() => clearFolder('videoFolder')}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
@@ -81,16 +81,16 @@ export function DownloadsCard(): React.JSX.Element {
|
||||
<div className={styles.folderRow}>
|
||||
<Input
|
||||
className={styles.folderInput}
|
||||
value={audioDir}
|
||||
value={audioFolder}
|
||||
placeholder="Documents\Audio (default)"
|
||||
contentBefore={<FolderRegular />}
|
||||
onChange={(_, d) => update({ audioDir: d.value })}
|
||||
onChange={(_, d) => update({ audioFolder: d.value })}
|
||||
/>
|
||||
<Button icon={<FolderRegular />} onClick={() => chooseDir('audioDir')}>
|
||||
<Button icon={<FolderRegular />} onClick={() => chooseFolder('audioFolder')}>
|
||||
Browse…
|
||||
</Button>
|
||||
{audioDir && (
|
||||
<Button appearance="subtle" onClick={() => clearDir('audioDir')}>
|
||||
{audioFolder && (
|
||||
<Button appearance="subtle" onClick={() => clearFolder('audioFolder')}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
@@ -141,8 +141,8 @@ export function DownloadsCard(): React.JSX.Element {
|
||||
hint="When AeroFetch gains focus, offer a video link you've just copied."
|
||||
>
|
||||
<Switch
|
||||
checked={clipboardWatch}
|
||||
onChange={(_, d) => update({ clipboardWatch: d.checked })}
|
||||
checked={watchClipboard}
|
||||
onChange={(_, d) => update({ watchClipboard: d.checked })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export function FilenamesCard(): React.JSX.Element {
|
||||
const styles = useSettingsStyles()
|
||||
const filenameTemplate = useSettings((s) => s.filenameTemplate)
|
||||
const restrictFilenames = useSettings((s) => s.restrictFilenames)
|
||||
const downloadArchive = useSettings((s) => s.downloadArchive)
|
||||
const useDownloadArchive = useSettings((s) => s.useDownloadArchive)
|
||||
const update = useSettings((s) => s.update)
|
||||
|
||||
return (
|
||||
@@ -44,8 +44,8 @@ export function FilenamesCard(): React.JSX.Element {
|
||||
hint="Keeps a record of completed downloads (--download-archive) and skips them on repeat playlist/channel runs."
|
||||
>
|
||||
<Switch
|
||||
checked={downloadArchive}
|
||||
onChange={(_, d) => update({ downloadArchive: d.checked })}
|
||||
checked={useDownloadArchive}
|
||||
onChange={(_, d) => update({ useDownloadArchive: d.checked })}
|
||||
/>
|
||||
</Field>
|
||||
</Card>
|
||||
|
||||
@@ -17,8 +17,8 @@ type Api = Window['api']
|
||||
// work isn't blocked behind the welcome screen.
|
||||
const MOCK_SETTINGS: Settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
videoDir: 'C:\\Users\\you\\Documents\\Video',
|
||||
audioDir: 'C:\\Users\\you\\Documents\\Audio',
|
||||
videoFolder: 'C:\\Users\\you\\Documents\\Video',
|
||||
audioFolder: 'C:\\Users\\you\\Documents\\Audio',
|
||||
ytdlpChannel: 'nightly',
|
||||
ytdlpLastUpdateCheck: Date.now() - 3_600_000,
|
||||
autoDownloadNew: true,
|
||||
@@ -174,7 +174,7 @@ export const mockApi: 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
|
||||
const dir = opts.kind === 'audio' ? MOCK_SETTINGS.audioFolder : MOCK_SETTINGS.videoFolder
|
||||
return {
|
||||
ok: true,
|
||||
command: `yt-dlp.exe -f "bv*+ba/b" -o "${dir}\\%(title)s.%(ext)s"` + `${extra} -- ${opts.url}`
|
||||
|
||||
@@ -12,8 +12,8 @@ import { logError } from '../reportError'
|
||||
const FALLBACK: Settings = PREVIEW
|
||||
? {
|
||||
...DEFAULT_SETTINGS,
|
||||
videoDir: 'C:\\Users\\you\\Documents\\Video',
|
||||
audioDir: 'C:\\Users\\you\\Documents\\Audio',
|
||||
videoFolder: 'C:\\Users\\you\\Documents\\Video',
|
||||
audioFolder: 'C:\\Users\\you\\Documents\\Audio',
|
||||
hasCompletedOnboarding: true
|
||||
}
|
||||
: DEFAULT_SETTINGS
|
||||
@@ -25,9 +25,9 @@ interface SettingsState extends Settings {
|
||||
/** re-fetch persisted settings from main (a backup restore changes them underneath) */
|
||||
reload: () => void
|
||||
/** open the OS folder picker and store the result as the video or audio folder */
|
||||
chooseDir: (target: 'videoDir' | 'audioDir') => void
|
||||
chooseFolder: (target: 'videoFolder' | 'audioFolder') => void
|
||||
/** clear a per-kind folder override, restoring its Documents\… default */
|
||||
clearDir: (target: 'videoDir' | 'audioDir') => void
|
||||
clearFolder: (target: 'videoFolder' | 'audioFolder') => void
|
||||
/** open the Windows High Contrast settings page (AppearanceCard) */
|
||||
openHighContrastSettings: () => void
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const useSettings = create<SettingsState>((set, get) => ({
|
||||
.catch(logError('getSettings'))
|
||||
},
|
||||
|
||||
chooseDir: (target) => {
|
||||
chooseFolder: (target) => {
|
||||
if (PREVIEW) return
|
||||
// Seed the OS picker with the folder this target currently points at (W5).
|
||||
window.api
|
||||
@@ -73,7 +73,7 @@ export const useSettings = create<SettingsState>((set, get) => ({
|
||||
.catch(logError('chooseFolder'))
|
||||
},
|
||||
|
||||
clearDir: (target) => get().update({ [target]: '' }),
|
||||
clearFolder: (target) => get().update({ [target]: '' }),
|
||||
|
||||
openHighContrastSettings: () => {
|
||||
if (!PREVIEW) window.api.openHighContrastSettings().catch(logError('openHighContrastSettings'))
|
||||
|
||||
@@ -36,7 +36,7 @@ interface ClipboardLink {
|
||||
/**
|
||||
* Watches the clipboard on window focus and offers a freshly-copied http(s) link.
|
||||
* Shared by the download bar and the library's add-source field so both fields
|
||||
* can auto-suggest a copied URL. Respects the `clipboardWatch` setting and never
|
||||
* can auto-suggest a copied URL. Respects the `watchClipboard` setting and never
|
||||
* interrupts text the user is already typing — pass the field's current value as
|
||||
* `currentValue` and the watcher stays quiet while it's non-empty.
|
||||
*
|
||||
@@ -75,9 +75,9 @@ export function useClipboardLink(
|
||||
async function check(): Promise<void> {
|
||||
const s = useSettings.getState()
|
||||
// Don't touch the clipboard until real settings have loaded (L138): before
|
||||
// that we'd be acting on the pre-load fallback for clipboardWatch instead of
|
||||
// that we'd be acting on the pre-load fallback for watchClipboard instead of
|
||||
// the user's actual choice — and we never read it when the watcher is off.
|
||||
if (!s.loaded || !s.clipboardWatch || valueRef.current.trim()) return
|
||||
if (!s.loaded || !s.watchClipboard || valueRef.current.trim()) return
|
||||
let text = ''
|
||||
try {
|
||||
text = (await window.api.readClipboard()) ?? ''
|
||||
|
||||
Reference in New Issue
Block a user