Complete Phase E theming, onboarding, and Windows share integration

Adds theme presets (Toffee/Slate/Evergreen/Lavender) with a "follow system"
mode and high-contrast awareness, a first-run welcome screen, and the
Windows analog of Android's share sheet for a Win32 app: an aerofetch://
protocol handler plus an Explorer "Send to AeroFetch" entry, both routed
through a single-instance lock so a second launch hands its link to the
already-running window instead of opening a duplicate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:45:09 -04:00
parent a822a2bb52
commit fa78b13cac
17 changed files with 883 additions and 77 deletions
+60 -8
View File
@@ -155,18 +155,70 @@ parallel storage. All items verified in the UI preview (typecheck + `npm run tes
Some items are Android-specific in Seal and adapted to Windows here.
- [ ] **Theme presets + "follow system"** and high-contrast (toffee light/dark already exists;
add system-follow + a few accent presets). *Material-You dynamic color is Android-only;
closest Windows analog is accent presets / system accent.*
- [ ] **i18n / language setting** (Seal ships ~40 languages). Large but optional.
- [ ] **Windows "open with" / share integration** — register a URL-protocol handler or Explorer
"Send to" entry (analog of Android's share sheet). *Lower priority, platform-specific.*
- [ ] **Onboarding / welcome screen** on first run.
- [x] **Theme presets + "follow system" and high-contrast.** Settings → Appearance has a
Theme select (Light / Dark / **Follow system**) and four accent presets — Toffee
(original), Slate, Evergreen, Lavender — each a 16-stop `BrandVariants` ramp sharing
toffee's lightness curve at a different hue (`src/renderer/src/theme.ts`). "Follow
system" reads Electron's `nativeTheme.shouldUseDarkColors` in the main process
(`src/main/index.ts`'s `resolveBackgroundMode`/`registerSystemThemeBridge`, pushed to
the renderer over a new `system-theme:update` channel into
`src/renderer/src/store/systemTheme.ts`) so the native window background stays in sync
too, not just the in-page theme. High contrast: `nativeTheme.shouldUseHighContrastColors`
surfaces as a status row in Settings → Appearance with a shortcut to Windows' contrast
settings (`ms-settings:easeofaccess-highcontrast`); actual color overrides are left to
Chromium's native `forced-colors` handling since nothing in the app sets
`forced-color-adjust: none`. The sidebar's quick toggle still works under "system" — it
sets an explicit light/dark away from whatever's currently resolved, breaking out of
auto. Verified in the UI preview (typecheck + `npm run test` clean): default Toffee,
switching to Slate/Evergreen/Lavender, Light/Dark/Follow-system, and the sidebar
toggle's break-out-of-system behavior all checked visually.
- [ ] **i18n / language setting** (Seal ships ~40 languages). Large but optional — deferred,
since shipping ~40 real translations isn't something to fake; doing this properly means
wiring up an i18n library + extracting every string now, with translation content
arriving incrementally from contributors.
- [x] **Windows "open with" / share integration.** Both mechanisms named above, since a Win32
(non-MSIX) app can't register as an actual Share Target:
- **`aerofetch://` protocol** (`aerofetch://download?url=<encoded>`) — registered at
install time via `electron-builder.yml`'s `protocols` field (NSIS), and at runtime via
`app.setAsDefaultProtocolClient` in `src/main/index.ts` (covers the portable build and
dev, which have no installer step to do it for them).
- **Explorer "Send to AeroFetch"** — `src/main/deeplink.ts`'s `registerSendToShortcut()`
writes a `.lnk` into `%APPDATA%\Microsoft\Windows\SendTo` via Electron's
`shell.writeShortcutLink` (no native deps). Sending a `.url` Internet Shortcut file
(what Explorer/browsers create from "Create shortcut"/dragging a tab to the desktop)
passes its path as argv; `extractIncomingUrl()` parses the `URL=` line.
Both paths funnel through the same `extractIncomingUrl()` (only ever forwards http(s)
targets — same restriction as the existing external-link window-open handler) and a new
`external-url` IPC push channel. The renderer surfaces the incoming link via
`DownloadBar.tsx`'s existing clipboard-suggestion banner (now labeled "Link received:"
for this source instead of "Use copied link?") rather than a new UI element. Added
`app.requestSingleInstanceLock()` + a `second-instance` handler so a second invocation
(the OS relaunching us for the protocol/SendTo case) hands its link to the already-running
window instead of opening a duplicate. `extractIncomingUrl()`'s parsing/validation logic
has 10 new unit tests (`test/deeplink.test.ts`); typecheck and the full suite are clean.
*Caveat: the OS-level wiring (protocol registration, second-instance routing, the SendTo
shortcut) is main-process/Electron-only and can't be exercised in the Vite browser
preview used elsewhere in this phase — worth a real install + a manual
`aerofetch://download?url=...` / "Send to" smoke test, same spirit as Phases AC's
real-download caveats.*
- [x] **Onboarding / welcome screen** on first run. `Settings.hasCompletedOnboarding`
(default `false`, persisted) gates a full-screen `Onboarding.tsx` rendered in place of
the sidebar + main content in `App.tsx` — not a Fluent `Dialog`, since this app avoids
Fluent's portal-based overlays entirely (GPU/driver blank-overlay issue noted in
`src/main/index.ts` and `Select.tsx`). Shows the brand mark, a one-line pitch, the
download-folder picker (same field as Settings → Downloads), three tip bullets, and a
"Get started" button that flips the flag. Gated on the settings store's `loaded` flag
so a returning user's real settings can never get clobbered by a one-frame flash of the
default-false state. Verified in the UI preview (typecheck + `npm run test` clean):
screen renders, folder field/browse work, "Get started" dismisses into the normal app.
---
## Not portable from Seal (noted for completeness)
- SAF directory picker (Android storage) → already covered by the native folder dialog.
- Android share-sheet / quick-download-on-share → partially mapped to Phase E.
- Android share-sheet / quick-download-on-share → mapped to Phase E's `aerofetch://` protocol
+ Explorer "Send to" integration (a true Share Target needs an MSIX package, out of scope
for this app's NSIS/portable distribution).
- F-Droid distribution → N/A (AeroFetch ships NSIS + portable).