Feat/binary management and library scale #6
+19
-14
@@ -368,23 +368,28 @@ per-item `options`) but not the surfaces. typecheck + test + `npm run build` cle
|
|||||||
(filter, ↑/↓, Enter, Esc) opened with Ctrl/Cmd+K from `App.tsx`; actions = navigate tabs,
|
(filter, ↑/↓, Enter, Esc) opened with Ctrl/Cmd+K from `App.tsx`; actions = navigate tabs,
|
||||||
new download (focuses the `aerofetch-url` input), toggle theme.*
|
new download (focuses the `aerofetch-url` input), toggle theme.*
|
||||||
|
|
||||||
## Phase O — Windows-native integration & discoverability
|
## Phase O — Windows-native integration & discoverability ✅ COMPLETE
|
||||||
|
|
||||||
Grepping `src/main` confirms none of these exist yet (no `setProgressBar`, `Tray`,
|
All in the main process. typecheck + test + `npm run build` clean.
|
||||||
`setThumbarButtons`, `setJumpList`, `globalShortcut`). All live in the main process, mostly in
|
|
||||||
`src/main/index.ts`.
|
|
||||||
|
|
||||||
- [ ] **Taskbar progress bar.** `win.setProgressBar(fraction)` driven by Phase M's aggregate
|
- [x] **Taskbar progress bar.** *Done: renderer pushes the `summarizeQueue` aggregate over a new
|
||||||
progress. ~20 lines, very high perceived-quality return.
|
`taskbar:progress` channel (App-level store subscription, no re-render);
|
||||||
- [ ] **System tray + minimize-to-tray.** A `Tray` with show/quit/pause-all. This is also the
|
`mainWindow.setProgressBar` with `mode: 'normal' | 'error'` (red when any failed) and `-1`
|
||||||
coherent home for the **watched-source background sync** (Phase J), which today just
|
to clear when idle.*
|
||||||
"launches the normal window unobtrusively."
|
- [x] **System tray + minimize-to-tray.** *Done: `src/main/tray.ts` builds a `Tray` (icon resolved
|
||||||
- [ ] **Thumbnail toolbar buttons** (`win.setThumbarButtons`) — pause/cancel from the taskbar
|
via `getAppIconPath()`; `build/icon.ico` added to electron-builder `extraResources` for
|
||||||
preview — and a **jump list** (`app.setJumpList`) with "Paste & download."
|
prod) with Show / Quit. A new `Settings.minimizeToTray` (Settings → Appearance) makes the
|
||||||
|
window's `close` hide to tray instead of quitting; `before-quit`/tray-Quit set an
|
||||||
|
`isQuitting` flag so real exits still work.*
|
||||||
|
- [x] **Jump list.** *Done: `app.setUserTasks` adds an "Open AeroFetch" task (focuses the
|
||||||
|
single instance). **Thumbnail toolbar buttons deferred** — they need per-button `NativeImage`
|
||||||
|
assets this repo doesn't have tooling to generate here.*
|
||||||
- [ ] **Taskbar overlay badge** (`win.setOverlayIcon`) showing active-download count / error state.
|
- [ ] **Taskbar overlay badge** (`win.setOverlayIcon`) showing active-download count / error state.
|
||||||
- [ ] **Settings discoverability.** `src/renderer/src/components/SettingsView.tsx` is ~1,000
|
*Deferred: a numeric/status badge needs a drawn `NativeImage` (no canvas in main); the
|
||||||
lines across ~10 cards. Add a **settings search box** or a **Basic / Advanced split** so
|
taskbar progress bar's `error` mode already signals failures.*
|
||||||
first-time users aren't faced with the full power-user wall.
|
- [x] **Settings discoverability.** *Done: a search box atop `SettingsView` filters the ~11 cards
|
||||||
|
live by matching each card's text (DOM `display` toggle keyed off the root's children — no
|
||||||
|
per-card refactor), with a "no settings match" hint.*
|
||||||
|
|
||||||
## Phase P — Reliability, longevity & trust
|
## Phase P — Reliability, longevity & trust
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ extraResources:
|
|||||||
to: bin
|
to: bin
|
||||||
filter:
|
filter:
|
||||||
- '**/*'
|
- '**/*'
|
||||||
|
# The app icon, copied so the runtime (system tray) can load it at
|
||||||
|
# <resources>/icon.ico — build/ itself isn't bundled into the app.
|
||||||
|
- from: build/icon.ico
|
||||||
|
to: icon.ico
|
||||||
|
|
||||||
win:
|
win:
|
||||||
# Two artifacts:
|
# Two artifacts:
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ export function getBinDir(): string {
|
|||||||
return is.dev ? join(app.getAppPath(), 'resources', 'bin') : join(process.resourcesPath, 'bin')
|
return is.dev ? join(app.getAppPath(), 'resources', 'bin') : join(process.resourcesPath, 'bin')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app icon (.ico), for the system tray. In dev it's the repo's build/icon.ico;
|
||||||
|
* in a packaged build, electron-builder's extraResources copies it to
|
||||||
|
* <resources>/icon.ico (see electron-builder.yml).
|
||||||
|
*/
|
||||||
|
export function getAppIconPath(): string {
|
||||||
|
return is.dev
|
||||||
|
? join(app.getAppPath(), 'build', 'icon.ico')
|
||||||
|
: join(process.resourcesPath, 'icon.ico')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AeroFetch keeps its OWN writable copy of yt-dlp.exe under userData, separate
|
* AeroFetch keeps its OWN writable copy of yt-dlp.exe under userData, separate
|
||||||
* from the read-only bundled seed in resources/bin. The managed copy is what
|
* from the read-only bundled seed in resources/bin. The managed copy is what
|
||||||
|
|||||||
+37
-1
@@ -8,7 +8,8 @@ import {
|
|||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
type CommandTemplate,
|
type CommandTemplate,
|
||||||
type YtdlpUpdateChannel,
|
type YtdlpUpdateChannel,
|
||||||
type SystemThemeInfo
|
type SystemThemeInfo,
|
||||||
|
type TaskbarProgress
|
||||||
} from '@shared/ipc'
|
} from '@shared/ipc'
|
||||||
import { getYtdlpVersion, updateYtdlp, runStartupYtdlpAutoUpdate } from './ytdlp'
|
import { getYtdlpVersion, updateYtdlp, runStartupYtdlpAutoUpdate } from './ytdlp'
|
||||||
import { getFfmpegVersions } from './ffmpeg'
|
import { getFfmpegVersions } from './ffmpeg'
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
import { indexSource } from './indexer'
|
import { indexSource } from './indexer'
|
||||||
import { syncWatchedSources } from './sync'
|
import { syncWatchedSources } from './sync'
|
||||||
import { getScheduledSync, setScheduledSync, isSyncLaunch } from './schedule'
|
import { getScheduledSync, setScheduledSync, isSyncLaunch } from './schedule'
|
||||||
|
import { createTray, markQuitting, isQuitting } from './tray'
|
||||||
|
|
||||||
// Only one instance ever runs. A second launch — e.g. the OS invoking us again
|
// Only one instance ever runs. A second launch — e.g. the OS invoking us again
|
||||||
// for an aerofetch:// link or a "Send to AeroFetch" file — hands its argv to
|
// for an aerofetch:// link or a "Send to AeroFetch" file — hands its argv to
|
||||||
@@ -135,6 +137,15 @@ function createWindow(): void {
|
|||||||
else win.show()
|
else win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Minimize-to-tray: when enabled, closing the window hides it to the tray
|
||||||
|
// instead of quitting. A real quit (tray menu / before-quit) sets isQuitting().
|
||||||
|
win.on('close', (e) => {
|
||||||
|
if (getSettings().minimizeToTray && !isQuitting()) {
|
||||||
|
e.preventDefault()
|
||||||
|
win.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
if (mainWindow === win) mainWindow = null
|
if (mainWindow === win) mainWindow = null
|
||||||
})
|
})
|
||||||
@@ -321,6 +332,13 @@ function registerIpcHandlers(): void {
|
|||||||
)
|
)
|
||||||
ipcMain.handle(IpcChannels.scheduledSyncGet, () => getScheduledSync())
|
ipcMain.handle(IpcChannels.scheduledSyncGet, () => getScheduledSync())
|
||||||
ipcMain.handle(IpcChannels.scheduledSyncSet, (_e, enabled: boolean) => setScheduledSync(enabled))
|
ipcMain.handle(IpcChannels.scheduledSyncSet, (_e, enabled: boolean) => setScheduledSync(enabled))
|
||||||
|
|
||||||
|
// Reflect overall queue progress on the Windows taskbar (fire-and-forget).
|
||||||
|
ipcMain.on(IpcChannels.taskbarProgress, (_e, p: TaskbarProgress) => {
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||||
|
if (p.mode === 'none') mainWindow.setProgressBar(-1)
|
||||||
|
else mainWindow.setProgressBar(Math.max(0, Math.min(1, p.fraction)), { mode: p.mode })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push OS theme/contrast changes to every window, and keep the native
|
// Push OS theme/contrast changes to every window, and keep the native
|
||||||
@@ -361,6 +379,20 @@ if (isPrimaryInstance) {
|
|||||||
registerSystemThemeBridge()
|
registerSystemThemeBridge()
|
||||||
registerSendToShortcut()
|
registerSendToShortcut()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
createTray(() => mainWindow)
|
||||||
|
|
||||||
|
// Taskbar right-click jump list: a quick "Open AeroFetch" task. Launching the
|
||||||
|
// exe again is caught by the single-instance lock, which focuses this window.
|
||||||
|
app.setUserTasks([
|
||||||
|
{
|
||||||
|
program: process.execPath,
|
||||||
|
arguments: '',
|
||||||
|
title: 'Open AeroFetch',
|
||||||
|
description: 'Open the AeroFetch window',
|
||||||
|
iconPath: process.execPath,
|
||||||
|
iconIndex: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// Keep yt-dlp current on its own: seed the managed copy, then (throttled to
|
// Keep yt-dlp current on its own: seed the managed copy, then (throttled to
|
||||||
// once a day) self-update it to the chosen channel so a stale binary can't
|
// once a day) self-update it to the chosen channel so a stale binary can't
|
||||||
@@ -377,6 +409,10 @@ if (isPrimaryInstance) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Any real quit path (tray "Quit", OS shutdown, the updater's app.quit) must set
|
||||||
|
// the quitting flag so the window's close handler exits instead of hiding to tray.
|
||||||
|
app.on('before-quit', () => markQuitting())
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ const DEFAULTS: Settings = {
|
|||||||
defaultTemplateId: null,
|
defaultTemplateId: null,
|
||||||
notifyOnComplete: true,
|
notifyOnComplete: true,
|
||||||
autoDownloadNew: true,
|
autoDownloadNew: true,
|
||||||
hasCompletedOnboarding: false
|
hasCompletedOnboarding: false,
|
||||||
|
minimizeToTray: false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fixed path for the --download-archive file; not user-configurable. */
|
/** Fixed path for the --download-archive file; not user-configurable. */
|
||||||
@@ -206,6 +207,7 @@ export function setSettings(partial: Partial<Settings>): Settings {
|
|||||||
case 'notifyOnComplete':
|
case 'notifyOnComplete':
|
||||||
case 'autoDownloadNew':
|
case 'autoDownloadNew':
|
||||||
case 'hasCompletedOnboarding':
|
case 'hasCompletedOnboarding':
|
||||||
|
case 'minimizeToTray':
|
||||||
if (typeof value === 'boolean') s.set(key, value)
|
if (typeof value === 'boolean') s.set(key, value)
|
||||||
break
|
break
|
||||||
case 'ytdlpChannel':
|
case 'ytdlpChannel':
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* System tray + minimize-to-tray (Phase O). When Settings.minimizeToTray is on,
|
||||||
|
* closing the window hides it to the tray instead of quitting; the tray's "Quit"
|
||||||
|
* really exits. The tray gives the app a background presence (a natural home for
|
||||||
|
* the watched-source sync) and a quick show/quit menu.
|
||||||
|
*/
|
||||||
|
import { app, Tray, Menu, nativeImage, type BrowserWindow } from 'electron'
|
||||||
|
import { getAppIconPath } from './binaries'
|
||||||
|
|
||||||
|
let tray: Tray | null = null
|
||||||
|
// True once the user has chosen to really quit (tray menu, or app.quit from the
|
||||||
|
// updater) — lets the window 'close' handler tell "hide to tray" from "exit".
|
||||||
|
let quitting = false
|
||||||
|
|
||||||
|
export function isQuitting(): boolean {
|
||||||
|
return quitting
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markQuitting(): void {
|
||||||
|
quitting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(getWindow: () => BrowserWindow | null): void {
|
||||||
|
const win = getWindow()
|
||||||
|
if (!win) return
|
||||||
|
if (win.isMinimized()) win.restore()
|
||||||
|
win.show()
|
||||||
|
win.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create the tray icon + menu once. No-op if it already exists or the icon is missing. */
|
||||||
|
export function createTray(getWindow: () => BrowserWindow | null): void {
|
||||||
|
if (tray) return
|
||||||
|
const icon = nativeImage.createFromPath(getAppIconPath())
|
||||||
|
// Without a usable icon a Windows tray entry is invisible/unclickable, which is
|
||||||
|
// worse than no tray — so skip it rather than ship a dead tray.
|
||||||
|
if (icon.isEmpty()) return
|
||||||
|
|
||||||
|
tray = new Tray(icon)
|
||||||
|
tray.setToolTip('AeroFetch')
|
||||||
|
tray.setContextMenu(
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{ label: 'Show AeroFetch', click: () => show(getWindow) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit AeroFetch',
|
||||||
|
click: () => {
|
||||||
|
quitting = true
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
tray.on('click', () => show(getWindow))
|
||||||
|
}
|
||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
type SyncResult,
|
type SyncResult,
|
||||||
type ScheduledSyncStatus,
|
type ScheduledSyncStatus,
|
||||||
type TerminalEvent,
|
type TerminalEvent,
|
||||||
type TerminalRunResult
|
type TerminalRunResult,
|
||||||
|
type TaskbarProgress
|
||||||
} from '@shared/ipc'
|
} from '@shared/ipc'
|
||||||
|
|
||||||
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
||||||
@@ -227,7 +228,11 @@ const api = {
|
|||||||
const listener = (_e: IpcRendererEvent, ev: TerminalEvent): void => cb(ev)
|
const listener = (_e: IpcRendererEvent, ev: TerminalEvent): void => cb(ev)
|
||||||
ipcRenderer.on(IpcChannels.terminalOutput, listener)
|
ipcRenderer.on(IpcChannels.terminalOutput, listener)
|
||||||
return () => ipcRenderer.removeListener(IpcChannels.terminalOutput, listener)
|
return () => ipcRenderer.removeListener(IpcChannels.terminalOutput, listener)
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/** Reflect overall queue progress on the Windows taskbar (fire-and-forget). */
|
||||||
|
setTaskbarProgress: (p: TaskbarProgress): void =>
|
||||||
|
ipcRenderer.send(IpcChannels.taskbarProgress, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Api = typeof api
|
export type Api = typeof api
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { Onboarding } from './components/Onboarding'
|
|||||||
import { CommandPalette, type PaletteAction } from './components/CommandPalette'
|
import { CommandPalette, type PaletteAction } from './components/CommandPalette'
|
||||||
import { getTheme, pageBackground } from './theme'
|
import { getTheme, pageBackground } from './theme'
|
||||||
import { useSettings } from './store/settings'
|
import { useSettings } from './store/settings'
|
||||||
|
import { useDownloads } from './store/downloads'
|
||||||
|
import { summarizeQueue } from './store/queueStats'
|
||||||
import { useResolvedDark } from './store/systemTheme'
|
import { useResolvedDark } from './store/systemTheme'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -50,6 +52,18 @@ function App(): React.JSX.Element {
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Mirror overall queue progress onto the Windows taskbar. Subscribe to the store
|
||||||
|
// directly (not via a selector) so taskbar updates don't re-render the app.
|
||||||
|
useEffect(() => {
|
||||||
|
function push(items: ReturnType<typeof useDownloads.getState>['items']): void {
|
||||||
|
const s = summarizeQueue(items)
|
||||||
|
const mode = s.active ? (s.failed > 0 ? 'error' : 'normal') : 'none'
|
||||||
|
window.api?.setTaskbarProgress?.({ fraction: s.progress, mode })
|
||||||
|
}
|
||||||
|
push(useDownloads.getState().items)
|
||||||
|
return useDownloads.subscribe((st) => push(st.items))
|
||||||
|
}, [])
|
||||||
|
|
||||||
const paletteActions: PaletteAction[] = [
|
const paletteActions: PaletteAction[] = [
|
||||||
{ id: 'go-downloads', label: 'Go to Downloads', hint: 'Navigate', run: () => setTab('downloads') },
|
{ id: 'go-downloads', label: 'Go to Downloads', hint: 'Navigate', run: () => setTab('downloads') },
|
||||||
{ id: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') },
|
{ id: 'go-library', label: 'Go to Library', hint: 'Navigate', run: () => setTab('library') },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
Input,
|
Input,
|
||||||
@@ -34,7 +34,8 @@ import {
|
|||||||
PaintBucketRegular,
|
PaintBucketRegular,
|
||||||
AccessibilityRegular,
|
AccessibilityRegular,
|
||||||
ArrowSyncRegular,
|
ArrowSyncRegular,
|
||||||
OpenRegular
|
OpenRegular,
|
||||||
|
SearchRegular
|
||||||
} from '@fluentui/react-icons'
|
} from '@fluentui/react-icons'
|
||||||
import {
|
import {
|
||||||
COOKIE_BROWSERS,
|
COOKIE_BROWSERS,
|
||||||
@@ -186,6 +187,28 @@ const UPDATE_CHANNEL_OPTIONS = [
|
|||||||
export function SettingsView(): React.JSX.Element {
|
export function SettingsView(): React.JSX.Element {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
|
// Settings search (Phase O). Rather than thread a query through all ~11 cards,
|
||||||
|
// filter by toggling each card's `display` based on whether its text matches.
|
||||||
|
// The cards are the root's direct children; the search box (marked
|
||||||
|
// data-settings-search) is skipped. React never sets `style` on the cards, so
|
||||||
|
// our inline display survives their re-renders.
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [noResults, setNoResults] = useState(false)
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
const root = rootRef.current
|
||||||
|
if (!root) return
|
||||||
|
const q = search.trim().toLowerCase()
|
||||||
|
let shown = 0
|
||||||
|
for (const el of Array.from(root.children) as HTMLElement[]) {
|
||||||
|
if (el.dataset.settingsSearch !== undefined) continue
|
||||||
|
const match = !q || (el.textContent ?? '').toLowerCase().includes(q)
|
||||||
|
el.style.display = match ? '' : 'none'
|
||||||
|
if (match) shown++
|
||||||
|
}
|
||||||
|
setNoResults(q !== '' && shown === 0)
|
||||||
|
}, [search])
|
||||||
|
|
||||||
const videoDir = useSettings((s) => s.videoDir)
|
const videoDir = useSettings((s) => s.videoDir)
|
||||||
const audioDir = useSettings((s) => s.audioDir)
|
const audioDir = useSettings((s) => s.audioDir)
|
||||||
const chooseDir = useSettings((s) => s.chooseDir)
|
const chooseDir = useSettings((s) => s.chooseDir)
|
||||||
@@ -210,6 +233,7 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
||||||
const defaultTemplateId = useSettings((s) => s.defaultTemplateId)
|
const defaultTemplateId = useSettings((s) => s.defaultTemplateId)
|
||||||
const notifyOnComplete = useSettings((s) => s.notifyOnComplete)
|
const notifyOnComplete = useSettings((s) => s.notifyOnComplete)
|
||||||
|
const minimizeToTray = useSettings((s) => s.minimizeToTray)
|
||||||
const autoUpdateYtdlp = useSettings((s) => s.autoUpdateYtdlp)
|
const autoUpdateYtdlp = useSettings((s) => s.autoUpdateYtdlp)
|
||||||
const ytdlpChannel = useSettings((s) => s.ytdlpChannel)
|
const ytdlpChannel = useSettings((s) => s.ytdlpChannel)
|
||||||
const ytdlpLastUpdateCheck = useSettings((s) => s.ytdlpLastUpdateCheck)
|
const ytdlpLastUpdateCheck = useSettings((s) => s.ytdlpLastUpdateCheck)
|
||||||
@@ -415,7 +439,22 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root} ref={rootRef}>
|
||||||
|
<div data-settings-search>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(_, d) => setSearch(d.value)}
|
||||||
|
placeholder="Search settings…"
|
||||||
|
contentBefore={<SearchRegular />}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
{noResults && (
|
||||||
|
<Caption1 style={{ color: tokens.colorNeutralForeground3, marginTop: '8px' }}>
|
||||||
|
No settings match “{search}”.
|
||||||
|
</Caption1>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<ArrowDownloadRegular className={styles.sectionIcon} />
|
<ArrowDownloadRegular className={styles.sectionIcon} />
|
||||||
@@ -517,6 +556,17 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
label={notifyOnComplete ? 'On' : 'Off'}
|
label={notifyOnComplete ? 'On' : 'Off'}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Keep running in the tray"
|
||||||
|
hint="When on, closing the window minimizes AeroFetch to the system tray instead of quitting. Use the tray icon to reopen or quit."
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={minimizeToTray}
|
||||||
|
onChange={(_, d) => update({ minimizeToTray: d.checked })}
|
||||||
|
label={minimizeToTray ? 'On' : 'Off'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ if (import.meta.env.DEV && !window.api) {
|
|||||||
defaultTemplateId: null,
|
defaultTemplateId: null,
|
||||||
notifyOnComplete: true,
|
notifyOnComplete: true,
|
||||||
autoDownloadNew: true,
|
autoDownloadNew: true,
|
||||||
hasCompletedOnboarding: true
|
hasCompletedOnboarding: true,
|
||||||
|
minimizeToTray: false
|
||||||
}
|
}
|
||||||
// Stands in for the cookies.txt file's mtime — lets the Cookies card's
|
// Stands in for the cookies.txt file's mtime — lets the Cookies card's
|
||||||
// sign-in/clear flow be exercised in this browser-only preview.
|
// sign-in/clear flow be exercised in this browser-only preview.
|
||||||
@@ -223,7 +224,8 @@ if (import.meta.env.DEV && !window.api) {
|
|||||||
onIndexProgress: () => () => {},
|
onIndexProgress: () => () => {},
|
||||||
runTerminal: async () => ({ ok: true }),
|
runTerminal: async () => ({ ok: true }),
|
||||||
cancelTerminal: async () => {},
|
cancelTerminal: async () => {},
|
||||||
onTerminalOutput: () => () => {}
|
onTerminalOutput: () => () => {},
|
||||||
|
setTaskbarProgress: () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const FALLBACK: Settings = {
|
|||||||
autoDownloadNew: true,
|
autoDownloadNew: true,
|
||||||
// True in preview so design work isn't blocked behind the welcome screen;
|
// True in preview so design work isn't blocked behind the welcome screen;
|
||||||
// a real first launch gets `false` from main/settings.ts's DEFAULTS instead.
|
// a real first launch gets `false` from main/settings.ts's DEFAULTS instead.
|
||||||
hasCompletedOnboarding: PREVIEW
|
hasCompletedOnboarding: PREVIEW,
|
||||||
|
minimizeToTray: false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsState extends Settings {
|
interface SettingsState extends Settings {
|
||||||
|
|||||||
+13
-1
@@ -76,7 +76,9 @@ export const IpcChannels = {
|
|||||||
terminalRun: 'terminal:run',
|
terminalRun: 'terminal:run',
|
||||||
terminalCancel: 'terminal:cancel',
|
terminalCancel: 'terminal:cancel',
|
||||||
/** main → renderer push channel for live terminal output lines */
|
/** main → renderer push channel for live terminal output lines */
|
||||||
terminalOutput: 'terminal:output'
|
terminalOutput: 'terminal:output',
|
||||||
|
/** renderer → main: reflect overall queue progress on the Windows taskbar (Phase O) */
|
||||||
|
taskbarProgress: 'taskbar:progress'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Sentinel format id meaning "let yt-dlp pick the best video+audio". */
|
/** Sentinel format id meaning "let yt-dlp pick the best video+audio". */
|
||||||
@@ -510,6 +512,8 @@ export interface Settings {
|
|||||||
autoDownloadNew: boolean
|
autoDownloadNew: boolean
|
||||||
/** false until the first-run welcome screen has been dismissed */
|
/** false until the first-run welcome screen has been dismissed */
|
||||||
hasCompletedOnboarding: boolean
|
hasCompletedOnboarding: boolean
|
||||||
|
/** keep AeroFetch running in the system tray when its window is closed (Phase O) */
|
||||||
|
minimizeToTray: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -709,3 +713,11 @@ export interface TerminalRunResult {
|
|||||||
ok: boolean
|
ok: boolean
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Overall queue state for the Windows taskbar progress bar (Phase O). */
|
||||||
|
export interface TaskbarProgress {
|
||||||
|
/** 0..1 overall fraction; ignored when mode is 'none' */
|
||||||
|
fraction: number
|
||||||
|
/** taskbar bar mode: hidden, normal, or red (some failed) */
|
||||||
|
mode: 'none' | 'normal' | 'error'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user