Files
AeroFetch/src/main/tray.ts
T
debont80 3b0d668f6c feat(audit): Batch 20 — Windows/main-process (UI23, W12, W15, L131)
UI23: cookie sign-in + PO-token windows open with the app's resolved
theme background (passed from the IPC layer), no more white flash.
W12: multi-resolution fallback tray icon (16/24/32/48 via
addRepresentation dataURL, embedded in trayFallbackIcons.ts); decode +
scale factors verified in an Electron probe.
W15: hardwareAcceleration setting (default false = software rendering);
index.ts gates disableHardwareAcceleration on it after the portable
redirect; Appearance card switch with restart note; boot path verified.
L131: resolved by-design — clearing the queue acknowledges failures;
flashFrame (W10) covers attention, Diagnostics persists the record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:15:51 -04:00

67 lines
2.3 KiB
TypeScript

/**
* 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'
import { FALLBACK_TRAY_PNGS } from './trayFallbackIcons'
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
// Prefer the real app icon; fall back to the embedded multi-resolution glyph
// when no icon.ico ships, so minimize-to-tray always has a tray to restore
// from — and stays crisp at 150%/200% DPI (W12). Without any icon the tray is
// skipped (an empty image makes an invisible/unclickable Windows tray entry),
// which would strand a minimized-to-tray window with no way back.
let icon = nativeImage.createFromPath(getAppIconPath())
if (icon.isEmpty()) {
icon = nativeImage.createEmpty()
for (const [scaleFactor, b64] of FALLBACK_TRAY_PNGS) {
// dataURL (not `buffer`) so the PNG is decoded — `buffer` is raw bitmap data.
icon.addRepresentation({ scaleFactor, dataURL: `data:image/png;base64,${b64}` })
}
}
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))
}