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:
@@ -13,6 +13,17 @@ export function getBinDir(): string {
|
||||
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
|
||||
* from the read-only bundled seed in resources/bin. The managed copy is what
|
||||
|
||||
+37
-1
@@ -8,7 +8,8 @@ import {
|
||||
type HistoryEntry,
|
||||
type CommandTemplate,
|
||||
type YtdlpUpdateChannel,
|
||||
type SystemThemeInfo
|
||||
type SystemThemeInfo,
|
||||
type TaskbarProgress
|
||||
} from '@shared/ipc'
|
||||
import { getYtdlpVersion, updateYtdlp, runStartupYtdlpAutoUpdate } from './ytdlp'
|
||||
import { getFfmpegVersions } from './ffmpeg'
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import { indexSource } from './indexer'
|
||||
import { syncWatchedSources } from './sync'
|
||||
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
|
||||
// for an aerofetch:// link or a "Send to AeroFetch" file — hands its argv to
|
||||
@@ -135,6 +137,15 @@ function createWindow(): void {
|
||||
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', () => {
|
||||
if (mainWindow === win) mainWindow = null
|
||||
})
|
||||
@@ -321,6 +332,13 @@ function registerIpcHandlers(): void {
|
||||
)
|
||||
ipcMain.handle(IpcChannels.scheduledSyncGet, () => getScheduledSync())
|
||||
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
|
||||
@@ -361,6 +379,20 @@ if (isPrimaryInstance) {
|
||||
registerSystemThemeBridge()
|
||||
registerSendToShortcut()
|
||||
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
|
||||
// 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', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
|
||||
@@ -47,7 +47,8 @@ const DEFAULTS: Settings = {
|
||||
defaultTemplateId: null,
|
||||
notifyOnComplete: true,
|
||||
autoDownloadNew: true,
|
||||
hasCompletedOnboarding: false
|
||||
hasCompletedOnboarding: false,
|
||||
minimizeToTray: false
|
||||
}
|
||||
|
||||
/** Fixed path for the --download-archive file; not user-configurable. */
|
||||
@@ -206,6 +207,7 @@ export function setSettings(partial: Partial<Settings>): Settings {
|
||||
case 'notifyOnComplete':
|
||||
case 'autoDownloadNew':
|
||||
case 'hasCompletedOnboarding':
|
||||
case 'minimizeToTray':
|
||||
if (typeof value === 'boolean') s.set(key, value)
|
||||
break
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user