fix(frontend,macos): reduce dark mode white flash on window open

Detect dark/light mode before React hydrates to eliminate the visible
white-to-dark flash when opening windows in dark mode.

Frontend:
- Inline script now reads next-themes localStorage key and applies
  .dark class, colorScheme, and backgroundColor on <html> before first
  paint
- Add CSS-only fallback via prefers-color-scheme media query in an
  inline <style> tag that fires before any JS executes

macOS backend:
- Detect system dark mode via `defaults read -g AppleInterfaceStyle`
  (cached with OnceLock) and set native window background color to
  match dark theme in apply_platform_window_style
- Persist user appearance mode preference (dark/light/system) to DB
  alongside zoom level so new windows use the correct background
- Add update_appearance_mode Tauri command; frontend syncs on mount,
  on settings change, and on cross-window storage events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-13 11:15:13 +08:00
parent e05ae76453
commit 41b28001af
7 changed files with 145 additions and 9 deletions

View File

@@ -22,6 +22,14 @@ function syncTrafficLightPosition(zoom: number) {
)
}
function syncAppearanceMode(mode: string) {
if (typeof window === "undefined" || !("__TAURI_INTERNALS__" in window))
return
import("@/lib/tauri").then((t) =>
t.updateAppearanceMode(mode).catch(() => {})
)
}
type AppearanceContextValue = {
themeColor: ThemeColor
setThemeColor: (color: ThemeColor) => void
@@ -89,9 +97,14 @@ export function AppearanceProvider({
}
}, [])
// Sync traffic-light position on mount (initial zoom)
// Sync traffic-light position and appearance mode on mount
useEffect(() => {
syncTrafficLightPosition(zoomLevel)
try {
syncAppearanceMode(localStorage.getItem("theme") ?? "system")
} catch {
// localStorage unavailable
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -113,6 +126,10 @@ export function AppearanceProvider({
syncTrafficLightPosition(zoom)
}
}
// Sync appearance mode to Tauri DB when changed in another window
if (e.key === "theme") {
syncAppearanceMode(e.newValue ?? "system")
}
}
window.addEventListener("storage", onStorage)
return () => window.removeEventListener("storage", onStorage)

View File

@@ -66,7 +66,18 @@ export function AppearanceSettings() {
</label>
<Select
value={theme ?? "system"}
onValueChange={(value) => setTheme(value as ThemeMode)}
onValueChange={(value) => {
setTheme(value as ThemeMode)
// Persist to Tauri DB so native window background matches on next open
if (
typeof window !== "undefined" &&
"__TAURI_INTERNALS__" in window
) {
import("@/lib/tauri").then((t) =>
t.updateAppearanceMode(value).catch(() => {})
)
}
}}
>
<SelectTrigger className="w-56">
<SelectValue placeholder={t("placeholder")} />