diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 3f454eb..aa4d0a9 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -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: diff --git a/src/main/cookies.ts b/src/main/cookies.ts index d97778b..554dc66 100644 --- a/src/main/cookies.ts +++ b/src/main/cookies.ts @@ -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 { 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. diff --git a/src/main/index.ts b/src/main/index.ts index f5827e2..3b97e4d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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=) the way Android's share sheet hands Seal one. The NSIS // installer also declares this scheme (electron-builder.yml's `protocols`) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 8c25099..276b501 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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 }) diff --git a/src/main/poToken.ts b/src/main/poToken.ts index cf024eb..936ea4f 100644 --- a/src/main/poToken.ts +++ b/src/main/poToken.ts @@ -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 { +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 { // 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 { 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, diff --git a/src/main/settings.ts b/src/main/settings.ts index 4a8f9cd..11afbd5 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -289,6 +289,7 @@ function applySettings(s: Store, partial: Partial): void { case 'hasCompletedOnboarding': case 'minimizeToTray': case 'sidebarCollapsed': + case 'hardwareAcceleration': if (typeof value === 'boolean') s.set(key, value) break case 'launchAtStartup': diff --git a/src/main/tray.ts b/src/main/tray.ts index 4042d4f..f51978f 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -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) diff --git a/src/main/trayFallbackIcons.ts b/src/main/trayFallbackIcons.ts new file mode 100644 index 0000000..98305ee --- /dev/null +++ b/src/main/trayFallbackIcons.ts @@ -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 = [ + // 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=' + ] +] diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index 69d93b4..bc66139 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -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] }, diff --git a/src/renderer/src/components/settings/AppearanceCard.tsx b/src/renderer/src/components/settings/AppearanceCard.tsx index e220d06..aeed6ba 100644 --- a/src/renderer/src/components/settings/AppearanceCard.tsx +++ b/src/renderer/src/components/settings/AppearanceCard.tsx @@ -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 { + + update({ hardwareAcceleration: d.checked })} + /> + + {highContrast ? 'A Windows high-contrast theme is active -- AeroFetch follows your system colors.' diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 054b1e6..5c7d021 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -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: '' }