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>
This commit is contained in:
2026-07-02 12:15:51 -04:00
parent f54d9a1965
commit 3b0d668f6c
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,
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.*
- [ ] **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).
*Deferred (arguably by-design, needs the live taskbar): the taskbar reflects the *live queue*, so clearing
finished items legitimately returns it to idle — the persisted record lives in Diagnostics. Whether the red
bar should linger after a clear is a debatable UX call best judged against the real Windows taskbar; left
for the watched Windows pass (with Batch 14/15).*
*Resolved as by-design (Batch 20): the taskbar mirrors the live queue, and "Clear finished" is the user
acknowledging those results — a red bar lingering after the queue is cleared would point at nothing the
window can explain and would need its own dismiss affordance. Attention at failure time is already covered
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`).
*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),
@@ -1507,16 +1509,17 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
### 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
(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
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
and the new Toaster share the non-portal `Z`/`SCRIM`/`ELEVATION` tokens (Batch 6/11). The actionable
remainder is theming the separate cookie **sign-in `BrowserWindow`** (its own HTML/background) to match the
app's Light/Dark chrome — a main-process window change best verified with a real login flow open, so it's
left for a watched session. Native file/confirm dialogs stay native by design.*
*Fixed (Batch 20, the actionable remainder): the cookie sign-in window — and the PO-token window, its
sibling on the same login partition — now open with `backgroundColor` set to the app's resolved theme
(`PAGE_BACKGROUND[resolveBackgroundMode(...)]`, supplied by the IPC layer to avoid an import cycle), so
neither flashes default-white chrome before the remote page paints; their title bars already follow the
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;
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."
@@ -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
("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.)*
- [ ] **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
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
bundled multi-size `build/icon.ico` is missing (W14 regenerated it), so it's a fallback-of-a-fallback. A
multi-res embedded base64 is fiddly to author and best verified on a real high-DPI tray — not worth an
unattended change for a path that shouldn't be hit.*
*Fixed (Batch 20): the fallback is now a multi-representation `NativeImage` — 16/24/32/48 px renders of
build/icon.svg (ImageMagick, like W14) embedded in
[trayFallbackIcons.ts](src/main/trayFallbackIcons.ts) and added via `addRepresentation` at scale factors
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**
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
@@ -1878,14 +1883,16 @@ DPI handled by Chromium). The deviations:
### 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.
**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
dev machine's GPU/driver blank-overlay bug (documented in index.ts and the project memo) — the very reason
the app hand-rolls non-portal overlays. Re-enabling HW accel can't be validated until the NVIDIA driver
issue is confirmed resolved on the target GPU, so it's intentionally left until that watched hardware pass.
PERF8 (this batch) trims the first-paint cost that software rendering compounds.*
*Fixed (Batch 20) as a user setting rather than a blind re-enable (the dev-machine GPU bug still can't be
validated here): new `Settings.hardwareAcceleration` (default **false** = today's safe software rendering),
surfaced as Settings → Appearance → "Hardware acceleration" with a restart note. index.ts reads it before
ready (after the portable userData redirect) and only then calls `disableHardwareAcceleration()`; the
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,
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:
+7 -1
View File
@@ -216,7 +216,10 @@ function exportAndResolve(): void {
*/
export function openCookieLoginWindow(
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> {
return new Promise((resolve) => {
let validUrl: string
@@ -241,6 +244,9 @@ export function openCookieLoginWindow(
width: 480,
height: 720,
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,
// Group under the app window instead of taking its own taskbar button
// (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()
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
// 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()
// 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
// (?url=<encoded>) the way Android's share sheet hands Seal one. The NSIS
// 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) =>
// Parent the sign-in window to the app window (W6) so it groups under
// AeroFetch instead of spawning a second taskbar button.
openCookieLoginWindow(url, BrowserWindow.fromWebContents(e.sender) ?? undefined)
// AeroFetch instead of spawning a second taskbar button; theme its
// 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.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).
ipcMain.handle(IpcChannels.youtubePoTokenMint, async () => {
const token = await openPoTokenWindow()
const token = await openPoTokenWindow(
PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
)
if (token) await setSettings({ youtubePoToken: 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
* 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
// opening a second one.
if (mintWindow && !mintWindow.isDestroyed()) {
@@ -105,6 +109,8 @@ export function openPoTokenWindow(): Promise<string | null> {
width: 960,
height: 640,
title: 'AeroFetch — Fetch YouTube token',
// Match the app's resolved theme (UI23), like the cookie sign-in window.
backgroundColor,
autoHideMenuBar: true,
webPreferences: {
partition: LOGIN_PARTITION,
+1
View File
@@ -289,6 +289,7 @@ function applySettings(s: Store<Settings>, partial: Partial<Settings>): void {
case 'hasCompletedOnboarding':
case 'minimizeToTray':
case 'sidebarCollapsed':
case 'hardwareAcceleration':
if (typeof value === 'boolean') s.set(key, value)
break
case 'launchAtStartup':
+13 -14
View File
@@ -6,16 +6,7 @@
*/
import { app, Tray, Menu, nativeImage, type BrowserWindow } from 'electron'
import { getAppIconPath } from './binaries'
// 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=='
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
@@ -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. */
export function createTray(getWindow: () => BrowserWindow | null): void {
if (tray) return
// Prefer the real app icon; fall back to the embedded glyph when no icon.ico
// ships, so minimize-to-tray always has a tray to restore from.
// 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.createFromDataURL(`data:image/png;base64,${FALLBACK_TRAY_PNG}`)
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)
+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,
'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' +
'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).
*/
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: 'network', title: 'Network & accounts', cards: [NetworkCard, CookiesCard] },
{ id: 'advanced', title: 'Advanced', cards: [CustomCommandsCard] },
@@ -1,5 +1,13 @@
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 { type ThemeMode, type AccentColor } from '@shared/ipc'
import { useSettings } from '../../store/settings'
@@ -20,6 +28,7 @@ export function AppearanceCard(): React.JSX.Element {
const focus = useFocusStyles()
const theme = useSettings((s) => s.theme)
const accentColor = useSettings((s) => s.accentColor)
const hardwareAcceleration = useSettings((s) => s.hardwareAcceleration)
const highContrast = useSystemTheme((s) => s.shouldUseHighContrastColors)
const update = useSettings((s) => s.update)
// Store action wraps the IPC call (L93/CC13: no window.api in components).
@@ -91,6 +100,16 @@ export function AppearanceCard(): React.JSX.Element {
</div>
</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}>
{highContrast
? 'A Windows high-contrast theme is active -- AeroFetch follows your system colors.'
+9
View File
@@ -624,6 +624,14 @@ export interface Settings {
launchAtStartup: boolean
/** whether the navigation sidebar is collapsed to icon-only mode */
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
* only where the release repo allows anonymous access). When the release repo
@@ -689,6 +697,7 @@ export const DEFAULT_SETTINGS: Settings = {
minimizeToTray: false,
launchAtStartup: false,
sidebarCollapsed: false,
hardwareAcceleration: false,
updateToken: ''
}