feat: Phase O Windows-native integration + settings search
- 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>
This commit is contained in:
@@ -10,6 +10,8 @@ import { Onboarding } from './components/Onboarding'
|
||||
import { CommandPalette, type PaletteAction } from './components/CommandPalette'
|
||||
import { getTheme, pageBackground } from './theme'
|
||||
import { useSettings } from './store/settings'
|
||||
import { useDownloads } from './store/downloads'
|
||||
import { summarizeQueue } from './store/queueStats'
|
||||
import { useResolvedDark } from './store/systemTheme'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -50,6 +52,18 @@ function App(): React.JSX.Element {
|
||||
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[] = [
|
||||
{ id: 'go-downloads', label: 'Go to Downloads', hint: 'Navigate', run: () => setTab('downloads') },
|
||||
{ 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 {
|
||||
Field,
|
||||
Input,
|
||||
@@ -34,7 +34,8 @@ import {
|
||||
PaintBucketRegular,
|
||||
AccessibilityRegular,
|
||||
ArrowSyncRegular,
|
||||
OpenRegular
|
||||
OpenRegular,
|
||||
SearchRegular
|
||||
} from '@fluentui/react-icons'
|
||||
import {
|
||||
COOKIE_BROWSERS,
|
||||
@@ -186,6 +187,28 @@ const UPDATE_CHANNEL_OPTIONS = [
|
||||
export function SettingsView(): React.JSX.Element {
|
||||
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 audioDir = useSettings((s) => s.audioDir)
|
||||
const chooseDir = useSettings((s) => s.chooseDir)
|
||||
@@ -210,6 +233,7 @@ export function SettingsView(): React.JSX.Element {
|
||||
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
||||
const defaultTemplateId = useSettings((s) => s.defaultTemplateId)
|
||||
const notifyOnComplete = useSettings((s) => s.notifyOnComplete)
|
||||
const minimizeToTray = useSettings((s) => s.minimizeToTray)
|
||||
const autoUpdateYtdlp = useSettings((s) => s.autoUpdateYtdlp)
|
||||
const ytdlpChannel = useSettings((s) => s.ytdlpChannel)
|
||||
const ytdlpLastUpdateCheck = useSettings((s) => s.ytdlpLastUpdateCheck)
|
||||
@@ -415,7 +439,22 @@ export function SettingsView(): React.JSX.Element {
|
||||
}
|
||||
|
||||
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}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<ArrowDownloadRegular className={styles.sectionIcon} />
|
||||
@@ -517,6 +556,17 @@ export function SettingsView(): React.JSX.Element {
|
||||
label={notifyOnComplete ? 'On' : 'Off'}
|
||||
/>
|
||||
</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 className={styles.card}>
|
||||
|
||||
@@ -36,7 +36,8 @@ if (import.meta.env.DEV && !window.api) {
|
||||
defaultTemplateId: null,
|
||||
notifyOnComplete: true,
|
||||
autoDownloadNew: true,
|
||||
hasCompletedOnboarding: true
|
||||
hasCompletedOnboarding: true,
|
||||
minimizeToTray: false
|
||||
}
|
||||
// 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.
|
||||
@@ -223,7 +224,8 @@ if (import.meta.env.DEV && !window.api) {
|
||||
onIndexProgress: () => () => {},
|
||||
runTerminal: async () => ({ ok: true }),
|
||||
cancelTerminal: async () => {},
|
||||
onTerminalOutput: () => () => {}
|
||||
onTerminalOutput: () => () => {},
|
||||
setTaskbarProgress: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ const FALLBACK: Settings = {
|
||||
autoDownloadNew: true,
|
||||
// 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.
|
||||
hasCompletedOnboarding: PREVIEW
|
||||
hasCompletedOnboarding: PREVIEW,
|
||||
minimizeToTray: false
|
||||
}
|
||||
|
||||
interface SettingsState extends Settings {
|
||||
|
||||
Reference in New Issue
Block a user