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:
@@ -51,7 +51,13 @@ export default async function RootLayout({
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body>
|
||||
{/* Apply appearance preferences (theme color + zoom) before first paint to prevent FOUC */}
|
||||
{/* CSS-only dark background: applies before JS executes, preventing white flash in dark mode */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@media(prefers-color-scheme:dark){html:not(.light){background-color:#09090b;color-scheme:dark}}`,
|
||||
}}
|
||||
/>
|
||||
{/* Apply appearance preferences (theme color + zoom + dark class) before first paint to prevent FOUC */}
|
||||
<script dangerouslySetInnerHTML={{ __html: APPEARANCE_INIT_SCRIPT }} />
|
||||
{/* Suppress benign ResizeObserver loop warnings (W3C spec §3.3) */}
|
||||
<script>{`window.addEventListener("error",function(e){if(e.message&&e.message.indexOf("ResizeObserver")!==-1){e.stopImmediatePropagation();e.preventDefault()}});window.onerror=function(m){if(typeof m==="string"&&m.indexOf("ResizeObserver")!==-1)return true}`}</script>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
@@ -32,6 +32,21 @@ const SCRIPT = `
|
||||
var storedZoom = parseInt(localStorage.getItem("${STORAGE_KEY_ZOOM_LEVEL}") || "", 10);
|
||||
var zoom = VALID_ZOOMS.indexOf(storedZoom) >= 0 ? storedZoom : 100;
|
||||
document.documentElement.style.fontSize = (16 * zoom / 100) + "px";
|
||||
|
||||
// 在 next-themes 水合之前同步检测暗色模式,防止白色闪屏。
|
||||
// next-themes 使用 localStorage key "theme",attribute="class"。
|
||||
var storedMode = localStorage.getItem("theme");
|
||||
var isDark = storedMode === "dark" ||
|
||||
(storedMode !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.style.colorScheme = "dark";
|
||||
// 直接设置背景色,比等待 CSS 类匹配更快,覆盖"系统浅色 + 应用深色"场景
|
||||
document.documentElement.style.backgroundColor = "#09090b";
|
||||
} else {
|
||||
document.documentElement.style.colorScheme = "light";
|
||||
document.documentElement.style.backgroundColor = "";
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage 不可用时静默走默认
|
||||
}
|
||||
|
||||
@@ -454,6 +454,10 @@ export async function updateTrafficLightPosition(zoom: number): Promise<void> {
|
||||
return invoke("update_traffic_light_position", { zoom: zoom as number })
|
||||
}
|
||||
|
||||
export async function updateAppearanceMode(mode: string): Promise<void> {
|
||||
return invoke("update_appearance_mode", { mode })
|
||||
}
|
||||
|
||||
// Folder history commands
|
||||
|
||||
export async function loadFolderHistory(): Promise<FolderHistoryEntry[]> {
|
||||
|
||||
Reference in New Issue
Block a user