Merge Batch 20 — Windows/main-process (UI23, W12, W15, L131)

This commit is contained in:
2026-07-02 12:15:52 -04:00
11 changed files with 300 additions and 57 deletions
+29 -22
View File
@@ -1118,12 +1118,14 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
audit's own note ("Fine when interpolated") confirms it; every string that reaches the UI raw (via a toast, audit's own note ("Fine when interpolated") confirms it; every string that reaches the UI raw (via a toast,
a status line, `cleanError`) is already a complete capitalized sentence. Standard: complete sentences for a status line, `cleanError`) is already a complete capitalized sentence. Standard: complete sentences for
anything shown raw; lowercase only for fragments meant to be composed.* anything shown raw; lowercase only for fragments meant to be composed.*
- [ ] **L131 — Taskbar `error` state is ephemeral** — it's driven by live `error` items, so "Clear - [x] **L131 — Taskbar `error` state is ephemeral** — it's driven by live `error` items, so "Clear
finished" flips the red taskbar bar back to normal even though failures occurred (out of sync with the persisted error log). finished" flips the red taskbar bar back to normal even though failures occurred (out of sync with the persisted error log).
*Deferred (arguably by-design, needs the live taskbar): the taskbar reflects the *live queue*, so clearing *Resolved as by-design (Batch 20): the taskbar mirrors the live queue, and "Clear finished" is the user
finished items legitimately returns it to idle — the persisted record lives in Diagnostics. Whether the red acknowledging those results — a red bar lingering after the queue is cleared would point at nothing the
bar should linger after a clear is a debatable UX call best judged against the real Windows taskbar; left window can explain and would need its own dismiss affordance. Attention at failure time is already covered
for the watched Windows pass (with Batch 14/15).* by the notification + `flashFrame` (W10), and the durable record lives in Diagnostics/errorlog. Judged on
the state semantics rather than pixels; the running production instance on this machine was left
undisturbed.*
- [x] **L132 — `clearProbe` + form-reset logic is duplicated** in DownloadBar (`onUrlChange`, `download`, `addPlaylist`). - [x] **L132 — `clearProbe` + form-reset logic is duplicated** in DownloadBar (`onUrlChange`, `download`, `addPlaylist`).
*Fixed: a shared `resetForm()` in [useDownloadBar.ts](src/renderer/src/components/downloadBar/useDownloadBar.ts) *Fixed: a shared `resetForm()` in [useDownloadBar.ts](src/renderer/src/components/downloadBar/useDownloadBar.ts)
now owns the post-enqueue reset (URL + probe + the one-shot trim/schedule/advanced/incognito/options), now owns the post-enqueue reset (URL + probe + the one-shot trim/schedule/advanced/incognito/options),
@@ -1507,16 +1509,17 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
### Dialogs, menus, status, navigation ### Dialogs, menus, status, navigation
- [ ] **UI23 — Fragmented overlay/dialog system.** Native OS dialogs (pickers, backup save/open, - [x] **UI23 — Fragmented overlay/dialog system.** Native OS dialogs (pickers, backup save/open,
backup warning `messageBox`), one custom React overlay (CommandPalette), a full-screen view backup warning `messageBox`), one custom React overlay (CommandPalette), a full-screen view
(Onboarding), and an **unstyled separate `BrowserWindow`** (cookie sign-in) that shows none of the (Onboarding), and an **unstyled separate `BrowserWindow`** (cookie sign-in) that shows none of the
app's theme/chrome. **Standard:** keep native for file/confirm; unify in-app overlays under one app's theme/chrome. **Standard:** keep native for file/confirm; unify in-app overlays under one
themed primitive; theme the sign-in window's title/background to match. themed primitive; theme the sign-in window's title/background to match.
*Deferred (larger, needs the live windows): the in-app overlays are already fairly unified — CommandPalette *Fixed (Batch 20, the actionable remainder): the cookie sign-in window — and the PO-token window, its
and the new Toaster share the non-portal `Z`/`SCRIM`/`ELEVATION` tokens (Batch 6/11). The actionable sibling on the same login partition — now open with `backgroundColor` set to the app's resolved theme
remainder is theming the separate cookie **sign-in `BrowserWindow`** (its own HTML/background) to match the (`PAGE_BACKGROUND[resolveBackgroundMode(...)]`, supplied by the IPC layer to avoid an import cycle), so
app's Light/Dark chrome — a main-process window change best verified with a real login flow open, so it's neither flashes default-white chrome before the remote page paints; their title bars already follow the
left for a watched session. Native file/confirm dialogs stay native by design.* app theme via the W3 `nativeTheme.themeSource` bridge. In-app overlays were already unified on the
non-portal `Z`/`SCRIM`/`ELEVATION` tokens; native file/confirm dialogs stay native by design.*
- [x] **UI24 — No context menus anywhere.** Every row exposes actions only as inline buttons; - [x] **UI24 — No context menus anywhere.** Every row exposes actions only as inline buttons;
right-click does nothing on any screen, despite many per-row actions that conventionally also live on right-click does nothing on any screen, despite many per-row actions that conventionally also live on
right-click. **Standard:** add consistent right-click menus mirroring row actions, or note "none by design." right-click. **Standard:** add consistent right-click menus mirroring row actions, or note "none by design."
@@ -1858,13 +1861,15 @@ DPI handled by Chromium). The deviations:
[ipc.ts](src/main/ipc.ts) calls `win.setOverlayIcon(badge, label)` with an accessible count label [ipc.ts](src/main/ipc.ts) calls `win.setOverlayIcon(badge, label)` with an accessible count label
("N downloads active" / "Download error"), cleared to `null` when the queue goes idle. (Numeric count ("N downloads active" / "Download error"), cleared to `null` when the queue goes idle. (Numeric count
lives in the tooltip label — a 16px overlay can't legibly render digits.)* lives in the tooltip label — a 16px overlay can't legibly render digits.)*
- [ ] **W12 — Fallback tray icon is single-resolution.** The embedded fallback is a 32×32 PNG, so on - [x] **W12 — Fallback tray icon is single-resolution.** The embedded fallback is a 32×32 PNG, so on
150%/200% DPI the tray glyph is blurry when the real multi-size `.ico` is absent. **Standard:** a 150%/200% DPI the tray glyph is blurry when the real multi-size `.ico` is absent. **Standard:** a
multi-size fallback (or rely only on the `.ico`). multi-size fallback (or rely only on the `.ico`).
*Deferred (low value, needs the real tray to judge): the fallback only renders in the unlikely case the *Fixed (Batch 20): the fallback is now a multi-representation `NativeImage` — 16/24/32/48 px renders of
bundled multi-size `build/icon.ico` is missing (W14 regenerated it), so it's a fallback-of-a-fallback. A build/icon.svg (ImageMagick, like W14) embedded in
multi-res embedded base64 is fiddly to author and best verified on a real high-DPI tray — not worth an [trayFallbackIcons.ts](src/main/trayFallbackIcons.ts) and added via `addRepresentation` at scale factors
unattended change for a path that shouldn't be hit.* 1/1.5/2/3, so Windows picks the crisp size for the DPI. Verified in an Electron probe: the image decodes
(non-empty) and reports all four scale factors. As before, it only renders if the bundled icon.ico is
missing.*
- [x] **W13 — Notifications set no icon.** `new Notification({title, body})` omitted `icon`; on the **portable** - [x] **W13 — Notifications set no icon.** `new Notification({title, body})` omitted `icon`; on the **portable**
build (no installed AUMID shortcut) Windows toasts showed a generic icon. *Fixed: both notification sites build (no installed AUMID shortcut) Windows toasts showed a generic icon. *Fixed: both notification sites
([download.ts](src/main/download.ts) completion/failure toast + [index.ts](src/main/index.ts) background-running ([download.ts](src/main/download.ts) completion/failure toast + [index.ts](src/main/index.ts) background-running
@@ -1878,14 +1883,16 @@ DPI handled by Chromium). The deviations:
### High DPI, multiple monitors, touch ### High DPI, multiple monitors, touch
- [ ] **W15 — Software rendering is global.** Hardware acceleration is disabled for a documented GPU - [x] **W15 — Software rendering is global.** Hardware acceleration is disabled for a documented GPU
workaround; on high-DPI/large windows this trades GPU compositing for CPU, which can be sluggish. workaround; on high-DPI/large windows this trades GPU compositing for CPU, which can be sluggish.
**Standard:** re-enable HW accel where the target GPU allows (the memo already flags revisiting this). **Standard:** re-enable HW accel where the target GPU allows (the memo already flags revisiting this).
*Deferred (deliberate workaround, external dependency): `disableHardwareAcceleration()` is the fix for the *Fixed (Batch 20) as a user setting rather than a blind re-enable (the dev-machine GPU bug still can't be
dev machine's GPU/driver blank-overlay bug (documented in index.ts and the project memo) — the very reason validated here): new `Settings.hardwareAcceleration` (default **false** = today's safe software rendering),
the app hand-rolls non-portal overlays. Re-enabling HW accel can't be validated until the NVIDIA driver surfaced as Settings → Appearance → "Hardware acceleration" with a restart note. index.ts reads it before
issue is confirmed resolved on the target GPU, so it's intentionally left until that watched hardware pass. ready (after the portable userData redirect) and only then calls `disableHardwareAcceleration()`; the
PERF8 (this batch) trims the first-paint cost that software rendering compounds.* window-occlusion feature stays disabled in both modes (a separate documented flicker cause). Users on
modern GPUs can now opt in without a build change. Verified the module-top boot path runs cleanly (exit 0,
empty stderr) with the new pre-ready settings read.*
- [x] **W16 — Touch targets too small / hover-only labels.** Icon-only row actions are ~32px, 4px apart, - [x] **W16 — Touch targets too small / hover-only labels.** Icon-only row actions are ~32px, 4px apart,
4 per row (below the ~40px touch guideline), and their only labels are hover tooltips (`Hint`) that 4 per row (below the ~40px touch guideline), and their only labels are hover tooltips (`Hint`) that
never appear on touch. **Standard:** ≥40px touch targets and non-hover labels. *(extends UX17)* *Fixed: never appear on touch. **Standard:** ≥40px touch targets and non-hover labels. *(extends UX17)* *Fixed:
+7 -1
View File
@@ -216,7 +216,10 @@ function exportAndResolve(): void {
*/ */
export function openCookieLoginWindow( export function openCookieLoginWindow(
url: string, url: string,
parent?: BrowserWindow parent?: BrowserWindow,
/** app-theme window background (UI23) supplied by the IPC layer so the
* sign-in chrome matches the app's Light/Dark instead of flashing white. */
backgroundColor?: string
): Promise<CookiesLoginResult> { ): Promise<CookiesLoginResult> {
return new Promise((resolve) => { return new Promise((resolve) => {
let validUrl: string let validUrl: string
@@ -241,6 +244,9 @@ export function openCookieLoginWindow(
width: 480, width: 480,
height: 720, height: 720,
title: 'Sign in — AeroFetch', title: 'Sign in — AeroFetch',
// Match the app's resolved theme (UI23) so the window doesn't flash a
// default-white background before the remote page paints.
backgroundColor,
autoHideMenuBar: true, autoHideMenuBar: true,
// Group under the app window instead of taking its own taskbar button // Group under the app window instead of taking its own taskbar button
// (W6); a child window stays above its parent without a modal block. // (W6); a child window stays above its parent without a modal block.
+19 -14
View File
@@ -29,23 +29,28 @@ import { initialWindowState, saveWindowState } from './windowState'
const isPrimaryInstance = app.requestSingleInstanceLock() const isPrimaryInstance = app.requestSingleInstanceLock()
if (!isPrimaryInstance) app.quit() if (!isPrimaryInstance) app.quit()
// Force software compositing (GPU acceleration off). On this dev machine's old GeForce
// GT 625 — and plausibly on the old / locked-down public PCs this app targets —
// Chromium's GPU compositor renders blank: under hardware acceleration the WHOLE window
// paints white (popup overlays alone were blank earlier). Updating the NVIDIA driver to
// the last Fermi release did not fix it. Software compositing renders correctly and
// costs no meaningful performance for this lightweight UI, so it is the safe default for
// this app's target hardware. The only downside is a minor overlay-repaint flicker that
// appears on the most broken GPUs. The window backgroundColor is theme-matched (see
// createWindow); native window-occlusion tracking is disabled (a separate documented
// Windows blank/flicker cause). Must run before the app is ready.
app.disableHardwareAcceleration()
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
// Redirect user data next to the exe when possible (portable, no-admin). Must run // Redirect user data next to the exe when possible (portable, no-admin). Must run
// before the app is ready and before any user path is read. // before the app is ready and before any user path is read — including the
// settings read for the hardware-acceleration gate just below.
setupPortableData() setupPortableData()
// Software compositing (GPU acceleration off) is the DEFAULT, now behind an
// opt-in setting (W15). On this dev machine's old GeForce GT 625 — and plausibly
// on the old / locked-down public PCs this app targets — Chromium's GPU
// compositor renders blank: under hardware acceleration the WHOLE window paints
// white (popup overlays alone were blank earlier). Updating the NVIDIA driver to
// the last Fermi release did not fix it. Software compositing renders correctly
// and costs no meaningful performance for this lightweight UI, so it stays the
// safe default; users on modern GPUs can flip Settings → Appearance →
// "Hardware acceleration" (applies on next launch — this must run before the
// app is ready). The window backgroundColor is theme-matched (see createWindow).
if (!getSettings().hardwareAcceleration) {
app.disableHardwareAcceleration()
}
// Native window-occlusion tracking stays off in BOTH modes — it's a separate
// documented Windows blank/flicker cause, independent of GPU compositing.
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
// Register aerofetch:// so a browser/another app can hand AeroFetch a link // Register aerofetch:// so a browser/another app can hand AeroFetch a link
// (?url=<encoded>) the way Android's share sheet hands Seal one. The NSIS // (?url=<encoded>) the way Android's share sheet hands Seal one. The NSIS
// installer also declares this scheme (electron-builder.yml's `protocols`) // installer also declares this scheme (electron-builder.yml's `protocols`)
+10 -3
View File
@@ -196,8 +196,13 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
ipcMain.handle(IpcChannels.cookiesLogin, (e, url: string) => ipcMain.handle(IpcChannels.cookiesLogin, (e, url: string) =>
// Parent the sign-in window to the app window (W6) so it groups under // Parent the sign-in window to the app window (W6) so it groups under
// AeroFetch instead of spawning a second taskbar button. // AeroFetch instead of spawning a second taskbar button; theme its
openCookieLoginWindow(url, BrowserWindow.fromWebContents(e.sender) ?? undefined) // background to the app's Light/Dark (UI23).
openCookieLoginWindow(
url,
BrowserWindow.fromWebContents(e.sender) ?? undefined,
PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
)
) )
ipcMain.handle(IpcChannels.cookiesStatus, () => getCookiesStatus()) ipcMain.handle(IpcChannels.cookiesStatus, () => getCookiesStatus())
ipcMain.handle(IpcChannels.cookiesClear, () => clearCookies()) ipcMain.handle(IpcChannels.cookiesClear, () => clearCookies())
@@ -296,7 +301,9 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
// Open a YouTube WebView and extract a PO token for bot-check bypass (Phase P). // Open a YouTube WebView and extract a PO token for bot-check bypass (Phase P).
ipcMain.handle(IpcChannels.youtubePoTokenMint, async () => { ipcMain.handle(IpcChannels.youtubePoTokenMint, async () => {
const token = await openPoTokenWindow() const token = await openPoTokenWindow(
PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
)
if (token) await setSettings({ youtubePoToken: token }) if (token) await setSettings({ youtubePoToken: token })
return token return token
}) })
+7 -1
View File
@@ -80,7 +80,11 @@ let mintWindow: BrowserWindow | null = null
* Resolves with the token string on success, or null if the window was closed * Resolves with the token string on success, or null if the window was closed
* by the user before extraction completed or if the field was not found. * by the user before extraction completed or if the field was not found.
*/ */
export function openPoTokenWindow(): Promise<string | null> { export function openPoTokenWindow(
/** app-theme window background (UI23) supplied by the IPC layer so this
* window's chrome matches the app's Light/Dark instead of flashing white. */
backgroundColor?: string
): Promise<string | null> {
// If a minting window is already open, bring it to the front rather than // If a minting window is already open, bring it to the front rather than
// opening a second one. // opening a second one.
if (mintWindow && !mintWindow.isDestroyed()) { if (mintWindow && !mintWindow.isDestroyed()) {
@@ -105,6 +109,8 @@ export function openPoTokenWindow(): Promise<string | null> {
width: 960, width: 960,
height: 640, height: 640,
title: 'AeroFetch — Fetch YouTube token', title: 'AeroFetch — Fetch YouTube token',
// Match the app's resolved theme (UI23), like the cookie sign-in window.
backgroundColor,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
partition: LOGIN_PARTITION, partition: LOGIN_PARTITION,
+1
View File
@@ -289,6 +289,7 @@ function applySettings(s: Store<Settings>, partial: Partial<Settings>): void {
case 'hasCompletedOnboarding': case 'hasCompletedOnboarding':
case 'minimizeToTray': case 'minimizeToTray':
case 'sidebarCollapsed': case 'sidebarCollapsed':
case 'hardwareAcceleration':
if (typeof value === 'boolean') s.set(key, value) if (typeof value === 'boolean') s.set(key, value)
break break
case 'launchAtStartup': case 'launchAtStartup':
+13 -14
View File
@@ -6,16 +6,7 @@
*/ */
import { app, Tray, Menu, nativeImage, type BrowserWindow } from 'electron' import { app, Tray, Menu, nativeImage, type BrowserWindow } from 'electron'
import { getAppIconPath } from './binaries' import { getAppIconPath } from './binaries'
import { FALLBACK_TRAY_PNGS } from './trayFallbackIcons'
// Fallback tray glyph (32×32 teal disc + white download arrow) used when no
// build/icon.ico is present. Without this 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. Embedded as base64 so it needs no
// asset-bundling step.
const FALLBACK_TRAY_PNG =
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAl0lEQVR4nO3TQQ6AIAxEUU7giV17bd0i' +
'aaDTmdqY0ISl/peirf11jvO6+/N5cHXKwlIIG6cQqngIoY5DiKy4C4G8aBwJohSArpIBmIgNKAWgDys' +
'AL8QGlANmCHZc8dUW1HEYEEFA6/d+B6q4CVAhwnHkb2DiUwCDkMRRBHpccQsRnXB8RLCAULxHMAAqbm0j5b4VoPRg1jzWxpzrS/JA7QAAAABJRU5ErkJggg=='
let tray: Tray | null = null let tray: Tray | null = null
// True once the user has chosen to really quit (tray menu, or app.quit from the // True once the user has chosen to really quit (tray menu, or app.quit from the
@@ -41,11 +32,19 @@ function show(getWindow: () => BrowserWindow | null): void {
/** Create the tray icon + menu once. No-op if it already exists or the icon is missing. */ /** Create the tray icon + menu once. No-op if it already exists or the icon is missing. */
export function createTray(getWindow: () => BrowserWindow | null): void { export function createTray(getWindow: () => BrowserWindow | null): void {
if (tray) return if (tray) return
// Prefer the real app icon; fall back to the embedded glyph when no icon.ico // Prefer the real app icon; fall back to the embedded multi-resolution glyph
// ships, so minimize-to-tray always has a tray to restore from. // 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()) let icon = nativeImage.createFromPath(getAppIconPath())
if (icon.isEmpty()) if (icon.isEmpty()) {
icon = nativeImage.createFromDataURL(`data:image/png;base64,${FALLBACK_TRAY_PNG}`) 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 if (icon.isEmpty()) return
tray = new Tray(icon) tray = new Tray(icon)
+180
View File
@@ -0,0 +1,180 @@
/**
* Multi-resolution fallback tray glyphs (W12), rendered from build/icon.svg at
* 16/24/32/48 px (scale factors 1/1.5/2/3) so the fallback tray icon stays
* crisp on high-DPI displays even when the bundled icon.ico is missing.
* Regenerate with ImageMagick:
* magick -background none build/icon.svg -resize NxN -strip out.png
* (base64-embedded so the fallback needs no asset-bundling step).
*/
export const FALLBACK_TRAY_PNGS: ReadonlyArray<readonly [number, string]> = [
// 16×16 — scaleFactor 1
[
1,
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAFEElEQVRIx52Ua1CVVRSGn/WdczhwAAnRgCPgDfGGihCZiqXj' +
'bdS0EqyZSsUUMsy71ahlZU5W2miWOWkmYc14BxW18VaD5oji/QoqircwQUsEzne+c77dDxAx6o/vnz2z9157ve+711rS3Z29' +
'aavXJ0CepL90Su8hrbQICZ7xK5N4i/Sow/hjw1+68LioxKBSHeNrlrL86mvqmlmsyhZOwCBfFSyfK4kt183YlPt2qnSUGOn8' +
'6UK6kSQ9AkO4zz0qAAUoTDRA0LhSs++dp66Zf9UmUWB5XyK1YKAFjQgEzNo4ATQ0AmhEAJDPPnWgwq2Oq7Pq7KyREtf9Z7+1' +
'va8IT5NE7+Ym1VRRRRUgCI46JVdrEst7rJUseGZi+LbQrwALGgIHl/wx+NYkUPN5WY0ComqJPIRCUYUfDhw4OMhv5F25Lh3S' +
'fvg8a4u5H8HElJ61mhtAmXzBIghebV9lHwqZsYPCBrgBK4JA6okdpTttcPd1PVXfCqLxLlP/81Oklo4gapfVHak31WOkJyaq' +
'zjLQ6q678eIFtUXdxgNmX4tDewl8mmrlWs6DDwAzxJNkvAjufFe1ng0yTFYRDfhgwfKIDwAmGoJIf6vbrf+pl9Qda3Uc71BN' +
'NahilUYGqBDS1Megeyx5Wh6ovapAHX34rt7BtU9vDHqInuYaA3KEeTIQpJWs4FugMX741ZGoE2h1t3BN1mfWY/hAcTUT1BQY' +
'cr9tWkwodCuJmNUsBLJzz3Y+nwXeNeZe0/dhWOhg306+62B8SvzouLGQ3/H6+BtO2OYoDCm6A+JkqSxu6IjVk+X+SH+jHoG/' +
'0XGBilUr+QAS+4R3DD0H6XOfSooPgIRqZ0z4KAga5BtrzwSq8eCBeaH9xvROgYRS522nBuZS7wxvCmw+eHLz6Tkgu2Q6k4Eg' +
'7NQjblW3jCijvGGlmNNVscqBZcf3jzjgguiKxtGNB0K/wDYXo+8BvR693wQHDmBP1wsDL46CZYv3j/xdgdnduOguAO1VaSPb' +
'ARdQ+TBOU4lGnBEE/15lheee5xCUdLp9uaw/TEvZuC/HF3ZnFuYWHWpIePelwsKicpjaa+OOHANK/G4fKosD+d5T4Tnc8P0H' +
'q1W1M7oYQf/RLDVdAWNVe2WHSwtLKc2AiYlrt60vh08WPB82pCnQihCawJyM3DXbfoKSrDtpd5PBNs7S3rIQ1AhpK75AM6Su' +
'COv35BO/TI6YtkEpBEEwqUTHjYYVG1aIXeLc5WwCMfawZ0M3gKmbx9Q5cFzwKbJtp2ZcAVWt3W2MQaD5al2lAxTppXm3UuD0' +
'pJv9bpYBnppawR87PrVzUqFZVYIRbgSqrYCGJkPZzDGOg22TVm65AFOX9T3ZZxEMn5YwO/4EeEvMXLM7qHR+5IV6SjIZzViw' +
'tNNma2Ww6Zsj5UcT4M2pmedXTwcj2WzijQF60ZU4tBqP1RoJikybP7748nZcnON0i0FUIQhVWAHB0XFzxMZm/hATFRYfth5Y' +
'rHayG3BhYNTz0hcbNmCKDKAfFF0tPVo6As4Mu558oxLwAooqHCgUDux0IPbKSmmUPuazcbsnlFGkolTk/LN4OMXJwF5QMzi8' +
'q9VctQBTJZiX1DU0ArBj5/9xHx0d5IjWWiIxLSNljryDBjWDDSud6VxxnJZyXW7MdErgqZEZqR/a7mJSTHl6PntUJ9VyRgF3' +
'yGZd83Yo3BiSzONCwwcftZZghpNcYuM5OSOXvnwFRTzO7+7/A1vMJNSBLSZHAAAAAElFTkSuQmCC'
],
// 24×24 — scaleFactor 1.5
[
1.5,
'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'
],
// 32×32 — scaleFactor 2
[
2,
'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'
],
// 48×48 — scaleFactor 3
[
3,
'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwEAYAAAAHkiXEAAARyUlEQVR42u2ceXyN19bHv/s5zzkZREKMQVBVV7X0ahERQ2ip' +
'0tZYQ2ue1avKpaa26C2u6d6iWqrUVDVUDZ1UK5FEhFCtWc2toIYgkeHknPPs9f6RE1zD5baJuP3090eSc55n77XWb61nP3ut' +
'vXcUNyC879rgL9zYZDalOYhl7MehPgRjhXFITfNvysu4VXDkP9S/eIGX2wYRTjg1IxZQhYaUL/OkqkQAlfyqEIxLBaki/FFw' +
'EYekSLIcIp1Dmfv4iU0cSlpADGuI3XJB3pAVRH06mtPsku3Ri3WaLildM2ZJcw5KADCFmjTHltClZclnO2DldKty/gh7YdWp' +
'de9gVweMWioDt+Hr6WjtBQbYJ9uL1KqlLqjp9B3WQ03kZdWySSAPq/KEBnbE9PZh4cECNIIFCCDIjXL+RyEoQKEwUNgAGyY2' +
'wOO18pAc4WzqaZnKe7Ls235SUuoybkoCypPiydp2Qe+3TTEag7SS6tIa+7Zyrac+Xxa3qlVl5b7Vl7Gp08ZoYxeWJ47aRrx6' +
'zBFmfMWRThV5n5FMmtCXJwhVVcs0xoWLLMCNB+sPQ/DvhcbExIaBLw58gER2yfZTkbzMQPqNumBVJ1mlLS6intffaY/ESoB0' +
'19HYVK0an5ZbUxKMjepvqg/IapYinVeqL3mXQTOjqUgohYNmeYnP8gr0yW+L73Nk4cCBDz6c4FcupcyWnkxjzsAC1OSSrre4' +
'i8RIdWMhqJrfrvhydQNQvWiIrrWUN9XbaseqZjxEBSqWCcKNCw9pZEd5QH5b9j8EAdIwcWASwGGOciLpvLSXvtKkzQfs4xQq' +
'8XXTmiuvW3P9axqabipqWDmOSkOWlgniJyzkasT/SfxvQw5vWRiUQ5UpJgG8S9SwLGkjJZjetbAplbSS5xpetF5hl/RuchBf' +
'9vE5dbwv0ftnqLFjYICclLeYBPqYBEqpa5eNCipVnQYVqt5kOOBGo/Nb6avwQaFQQBzpZDZRNGAmyZHRpmzWl3XFNus4yQXS' +
'AqsgCGlXVTfyW3Mc2LCBniIdpRsUaeX7g+9FqHK2yITghddu218iedTFXpD8vTPS2RyM19QytRBwYV2b9OU7NKAgcBzHKEGL' +
'tj1MPdVK1OcjmqkwirCbEwiC3AfEeyNGt6CBPA0VqgStCKoKo0eEZdV4Bf7av9jkolHeewV+nHd+woX5ML7ZtuAddeFYqZSL' +
'KaPBWEuM+ga8duU3jBy7ZA9nOFenm3q05YJ1S5LS++CPYPrPyW8Nr+I4qaQCUbRXbWB0wTBV41/QwV3ZrPTk7Zstq/5T8KEd' +
'MH7b1vM7ugORfCKfAQ8QSGB+G8XVwOISTjIzipg6TR/Xu/wG4L6aUnEfRAryETvYCb6nzN7mAahYudDPQTUBKH5LHb26V3QG' +
'7Q16AOy/GAXUfnB+5HnRWg7qb9Tg8fy2yqupApy4cfl9amrRG/V6VY1Mr1nqenPyD3KBeBJAftaVdDlQU1Uv1Q6Aday/vYbq' +
'HdVPvQSSqP+iHwBdTq/Ta0E5SeZcflt1zbxs/VVDUz9iLdbzrumf35pd1fARNssW0EVVNWUDmslW2Q0cvUPDl6WLdAH9lVXb' +
'qga6hLXbskA9Ql1VJ7+tuoqrPJu6lF5v/Zrf+twCIcQRD2JpS4eALGcYYwFoy4u3byZTs++TqnqGXgli0xv0GZCxpHEf2mlq' +
'S8/S7+W3GreAR2LYDDrY6KYfB8bL/8lAoNUd2nnv04X1Kv0Z6IvWAr0TsFQDduW3UTfD1PWtaXppfqtxC9RlqsSBfkgvNZqD' +
'9JRUtnivDbx9M5klA3gV9DwdqOuAPqwftb4E6jNN1ctvo26GqRN1qvVqfqtxCySSwiug7dpuTQJG8pYsBKKY8B/bjeQt6QXa' +
'1HarK+hEnaJfARJJ5T6007Ta6056072QlF1KoId8xlrQs2kiz4OaRyueA/WMWqGWAGUpRBCwW74jGqwOhqnrgpyRDjL8zmKk' +
's3SV4WCFWD46CqxSVjsdDXRUTwHwC5dJAfla2kknkJ6s5nMw+rFBrQPmq9a0ADz3ppRhSrI1zJqUhxJ8MTFBqksQJcC/oT3U' +
'XhKKFw6YWOAyZCa7i7svw7nd6QHpI4DObGUbUI3hNAPJ1DHGu0A0G1kJdCeWb/6DvGh60x+kld6lM0HS9PfWFCCS0vwKDKM1' +
'YVA8uUBkgb3gV9hey34Zzqm00HQ/yHjAPd69CNSPKoWzgBMLT146oInVV9fIi56zI14/QWEJgcqVis4v+iAMX1R/SsQXUD05' +
'pE5IQ7jyYdaRrHRYXOXHxrsPwcK6PzT5cTFkVbSGWO+AdDXGG21AzkgBuYuarLwv3aUdSIiVbqWBNdKabi0HH5dtnK03dP1n' +
'dcdfW0Pn4389Uu0RKDjU50GfffDDojNTzkTDpHax9eLbwcERF/pfeBaMRC6q0+TZE6GK95/QafJSyfXcV3ryGWvA/xnzmH0N' +
'vLejxYzm8dCsbKXxD93C4a5F1lprB/xrbnxIQmWYMSfh5LZkMKcYe43NsMLZ8e12vSHskzLu0hm3l7utY5L9lD+08//kjRUf' +
'gqeXLq8fh4GFwpPCfoEhn0eEhvuC4zVbZ1v9m9t/XenQ+4fPQn9Z2/7LByHjE08Zd3NQ82lDy9x3gKkL6Q16Yu53rOdLhqRB' +
'sRaBVmAPeLx7yPiQucBGoAbXSglZePCAo4utha0GDF4RMTr8A9D7ZYtsgw/Dtj/1vQd0uE7Sd0rCAD1an9LHwVyiJqqe0O//' +
'wkbUGg1DHBHDwgeBI8E2yvYqkJktN2eIzEmNqoeE9A55E4o96H/GrwscW3DJ6VwPRiG1QR3IAwcwWzfVHXK/Y6ZLPXkKMpa4' +
'nsp6AFJjswKcWVCSgokBkF0LcQN+2LEDmbjxgOML23hbHxiyoe7DdVaBXzHzsBkCPiHmIDO7FLGMuNuL9cHsYTaFgZfDfwmz' +
'oH+32qVqFQLHQNsFL/FuPF65pvfzdXqkHspq4pwIGQGuC1kK6KA/0N2AQSpOfZf7NKki6WMPvH0294cgTmbPNjibXdXsUzjs' +
'aM1gGDuz8YEnl4HjA9sM28ibCbiRIPcqXVbXAx0oWQL4NLadtf0HB2TNt3ytamCkKh8F2F81Uozdt5Fz3WfXHGuytQjGzfy2' +
'XtQrMGf+tszEGCAIf/yBUO/sLNcd8PiYyn8vmwcOMLx17xayUJaAw23Ws5WGgfUiutbpB0N7NTherxA4Ttg22JbfgqCchRTv' +
'gsw1hr1Dh4936LjT9zf2cyPxva0B1miYFhbzetyzMCMgvs6W6uD6ybPMSgT1hequupC93SYPqsQmx/UI3TP3O86Beo9DxILr' +
'gtvHPQhmvBZ3MX4HsEbaySAYWjqyav1nwHHKtsf29S0ccSNuJPhO39+O+GZWE6snTGsf833sezBjSlzAlrrg2med92wEFcIR' +
'tRlwynDpkXf8mDh0m7x0QM7UTZWiDWngDta93Q1hRnpswc1u4GuZJ0NhaMWGVRs0BccR2x7b+rtwxN3iRuL7Wa2tgTCt0yZP' +
'7B6YMSp21ea54DppzbXKgCrHBTIBJx7pCjjykBvAlM9lmDTKWyH/Bh8KYQdXlLuyOxJmpMS+GFcK6CsbZQEMLdeoVGQEOH62' +
'nbbF89sdcSPxz1k1rBdgWvHoajEFYXrdWHdcHLhmeYKsOaBCVbA6DJLFYOr/F3J+rwN4TrfXo+6hA7xQvlzBDq5zbssTBjM8' +
'MQvimgHP48sJGFq4UUBkFXBcsqXZ9nP3jriR+FZWLasFTDsQ1XtTI5j+S4wj7hi4KloTrGBQpUhnLuCUNjLs3vNg4pZBOj+q' +
'hO7sX6oQAewF1xl3gO4N05dEH44ZC3SVhjIThj7wpL1hOXAct7ltP3N7R9wc8eWscJi2d+PS6P4wvWhM59jSkJXgGWR9DkY5' +
'FcQB4AoD8rLUcGcHaN1FBv7+jn4zspdCUSXR+ILLx33FXQKmL4t+JOYU0FzqSGsYWu5JV6MQcPxsOmxnriM8BznEd7ceshrA' +
'tH3fHYj6BKb7bpoc2wyy4j3bPePBCFUfqwKAUzrSPx/t9sKU9rpfniRivxFqMCbp4DrnvujOhOk+UQM2FQR5TQZLJAzt/1Ro' +
'o6Xg8745wrxuZSxrnOddz1cwLeC7hKh5MH1gtBnzJbi2WHuto6BeIwAnSAvpKW3z28prMPlY+upm+a3GzVC++PEruC64z+ke' +
'MMOMCopeAqd3Xh51ORJazH5scrVxXC1prF2+K213cfi0wc7LPyZA1iBPbc/zYExQAZwHnPT8tyfmPoEqHPzqN0N75UEilgMD' +
'UCDnucIVoApjZBzIFPmGb7masN0WObWa/ixjBegWgviC7yr7P83rXprO1u4hnslgrFWinMBsOtIecHoTtNvBm2CpYeppGgP7' +
'GafGgCpGQQqSPY3Ow206qpBrUOUh83LfAcrhXQcYI2tYByzMXmQPPObn43sAfB+zd7M3Ak7KJS5y50wzx0nu7MxWVvC9/HDd' +
'5XY8oaoDdm/idSeLchwfqgoTDM5d7gXuKEitkJnlfBjoSj0iQI1TLXkexHUHR/5GmNTXeTL90kfZKolgb24E2nZAp6bhe8KG' +
'QOeg2n8Pi4fADn4r/UzgFybxM2Ch5W7q7TmOaH/T91GchLsmSWEoA6jNSopDalDmU5nxsPilrW9sGwxLlic035YE7qO6hBUB' +
'qj61Va3c50kVChyQOKibZEefyr0TL56LOkQ/BJGNK1erFAYfz+hTq1c0BFb1Xe/799w3JLeQ+oOzsXMkvNT/g60fRsCmzQcP' +
'HNoDZrBxxjica2LE+1OZ+MgI3VK+RXBjqsa5JiFdD9CjoPK8kmVKLobACr7rffdzbWUpZ434foFXr8Dqvt/6ToTK75d8sGQp' +
'iE7fH3+wGOCjZt1hO8B/g+xzZiIfmRKr28rwjGJ4UOgCuSfhRxEJhYONTrU6fRxS450FnLUhsLZvuu/We8/vHeENiNTNTn9n' +
'GByoeerV0zZQW0WkKEi0flEG56I8jcaRcdykoe6tI5JmoZQPzr/MJZe2JxpL2EICJAQcPnp4Obx5atVfVr8EnddENAhPgaA+' +
'flX9PCAr2Mp2wLrHByps2YSrdtSmJqR8kLkn04TFVvzDCZdga/HDAYdXg7GaCrQDmkgdHZ4rkrOHHxN//JMmqaCQ3r37xcyL' +
'IgMhuUdDBJUr7wJH9jFOeZNVrAEWyza2QVCy/3i/luBTy5xtjgVOksxF7v3+/Zxt4qEUIRiyEj39PGMhpUjG6Mw1QGcVRhio' +
't7xrwS7vMdzfS75CEBSB+FBwfleTgrJIPlo1ApfEs6WtiYWbrMD430n/1dmIGkNXbx5whCuQUiV9Z3oRkA5ynmbcOQ/IO2S7' +
'W3MeAbVT7eQZIIEj6iyoYrKOaLIDI4rcOSskKCzs+KRWJIknqPnpDhX0bI/ZvX/1+14a8QWTFp4giSTOvNAasGHD5W2ax1Xx' +
'PzxcgMbCQXEqU2HlOF6iKW90DTfFIZtkeeYTjCGBj6d8hch02R7+OJpTXCqzE8HEJM3b0Z+nJf87pKGw8BCARRmKJtWUc+p1' +
'1XRKT1WHY5TPfNrE0u9oAZnDx+zY3kz15yvWjmwArGHWzB0YkkJyoRooZcf+5xNxl3CBuHETgKWCKJpSAOjGSyO7qsoyUGK2' +
'95f9vMwxMNFqsvoMgwb6H7oFWveU2pZesslYzCZjJSVJZwDfTDgqqfIsJ0MLoHBgBxQGtj//VYEXGkGjMRBcuHCoAP5GmaSe' +
'2GUXrUbO1eN0AVp+PES1tmUaH4Eqa4xRj2JcJa7g2i6R3ctjUkuayHw8lLeVMJoBcZ411rqadZhFHKOHLiVJEmTD01m4Oc+Z' +
'oErXKZFdg1H/9lL9ozjomh3XZmvZuy3U1StFKZlak6Kqrmr0zdt0pgl9pjajn+1zo2miJkYv5lHgtOrMe9ivRC5K/6gb7puI' +
'CWzeqWS38hhUkP08hpZeqoN0ATmr++sH/QbzJGUpHrlPFWYvX7dNBuKIiwDNcQ6X6YImDfxLIHhwqfZ/GPIVJg5Zhg0ffDJ+' +
'wkY1QpImY1CfevEz5RSPErFqA+PZzq7onqqVsdEIzGyiajNBTQRxUpoT2K7YlqgFZ65NaP8foNDayppt+VIAAAAASUVORK5C' +
'YII='
]
]
+5 -1
View File
@@ -62,7 +62,11 @@ type CardComponent = () => React.JSX.Element
* state instead of mutating the card's own DOM `style` (M14). * state instead of mutating the card's own DOM `style` (M14).
*/ */
const SECTIONS: { id: string; title: string; cards: CardComponent[] }[] = [ const SECTIONS: { id: string; title: string; cards: CardComponent[] }[] = [
{ id: 'downloads', title: 'Downloads', cards: [DownloadsCard, PostProcessingCard, FilenamesCard] }, {
id: 'downloads',
title: 'Downloads',
cards: [DownloadsCard, PostProcessingCard, FilenamesCard]
},
{ id: 'appearance', title: 'Appearance', cards: [AppearanceCard] }, { id: 'appearance', title: 'Appearance', cards: [AppearanceCard] },
{ id: 'network', title: 'Network & accounts', cards: [NetworkCard, CookiesCard] }, { id: 'network', title: 'Network & accounts', cards: [NetworkCard, CookiesCard] },
{ id: 'advanced', title: 'Advanced', cards: [CustomCommandsCard] }, { id: 'advanced', title: 'Advanced', cards: [CustomCommandsCard] },
@@ -1,5 +1,13 @@
import { useRef } from 'react' import { useRef } from 'react'
import { Field, Button, Card, Subtitle2, Caption1, mergeClasses } from '@fluentui/react-components' import {
Field,
Button,
Card,
Subtitle2,
Caption1,
Switch,
mergeClasses
} from '@fluentui/react-components'
import { PaintBucketRegular, AccessibilityRegular } from '@fluentui/react-icons' import { PaintBucketRegular, AccessibilityRegular } from '@fluentui/react-icons'
import { type ThemeMode, type AccentColor } from '@shared/ipc' import { type ThemeMode, type AccentColor } from '@shared/ipc'
import { useSettings } from '../../store/settings' import { useSettings } from '../../store/settings'
@@ -20,6 +28,7 @@ export function AppearanceCard(): React.JSX.Element {
const focus = useFocusStyles() const focus = useFocusStyles()
const theme = useSettings((s) => s.theme) const theme = useSettings((s) => s.theme)
const accentColor = useSettings((s) => s.accentColor) const accentColor = useSettings((s) => s.accentColor)
const hardwareAcceleration = useSettings((s) => s.hardwareAcceleration)
const highContrast = useSystemTheme((s) => s.shouldUseHighContrastColors) const highContrast = useSystemTheme((s) => s.shouldUseHighContrastColors)
const update = useSettings((s) => s.update) const update = useSettings((s) => s.update)
// Store action wraps the IPC call (L93/CC13: no window.api in components). // Store action wraps the IPC call (L93/CC13: no window.api in components).
@@ -91,6 +100,16 @@ export function AppearanceCard(): React.JSX.Element {
</div> </div>
</Field> </Field>
<Field
label="Hardware acceleration"
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 })}
/>
</Field>
<Caption1 className={styles.hint}> <Caption1 className={styles.hint}>
{highContrast {highContrast
? 'A Windows high-contrast theme is active -- AeroFetch follows your system colors.' ? 'A Windows high-contrast theme is active -- AeroFetch follows your system colors.'
+9
View File
@@ -624,6 +624,14 @@ export interface Settings {
launchAtStartup: boolean launchAtStartup: boolean
/** whether the navigation sidebar is collapsed to icon-only mode */ /** whether the navigation sidebar is collapsed to icon-only mode */
sidebarCollapsed: boolean sidebarCollapsed: boolean
/**
* Opt in to GPU compositing (W15; takes effect on next launch). Off by
* default: software rendering is the safe choice for this app's target
* hardware on some old/locked-down GPUs (the documented GeForce GT 625
* case) Chromium's GPU compositor paints the whole window blank. Users on
* modern GPUs can turn this on for smoother high-DPI/large-window rendering.
*/
hardwareAcceleration: boolean
/** /**
* Optional Gitea access token for the in-app updater. Empty = anonymous (works * Optional Gitea access token for the in-app updater. Empty = anonymous (works
* only where the release repo allows anonymous access). When the release repo * only where the release repo allows anonymous access). When the release repo
@@ -689,6 +697,7 @@ export const DEFAULT_SETTINGS: Settings = {
minimizeToTray: false, minimizeToTray: false,
launchAtStartup: false, launchAtStartup: false,
sidebarCollapsed: false, sidebarCollapsed: false,
hardwareAcceleration: false,
updateToken: '' updateToken: ''
} }