fix(ui): prevent light flash on dark startup (#5637)

* fix(ui): prevent light flash on dark startup

* fix(window): apply initial dark/light theme to prevent white flash

* refactor(boot): optimize config fetch and theme fallback

* fix: system theme detection

* fix(window): remove black flash before loader by aligning initial paint with theme
This commit is contained in:
Sline
2025-11-28 15:15:17 +08:00
committed by GitHub
parent 34aa26813f
commit 683667f8ae
8 changed files with 500 additions and 44 deletions
+85
View File
@@ -9,8 +9,93 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clash Verge</title>
<style>
:root {
--initial-bg: #f5f5f5;
--initial-text: #333;
--initial-spinner-track: #e3e3e3;
--initial-spinner-top: #3498db;
--bg-color: var(--initial-bg);
--text-color: var(--initial-text);
--spinner-track: var(--initial-spinner-track);
--spinner-top: var(--initial-spinner-top);
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--initial-bg: #2e303d;
--initial-text: #ffffff;
--initial-spinner-track: #3a3a3a;
--initial-spinner-top: #0a84ff;
--bg-color: var(--initial-bg);
--text-color: var(--initial-text);
--spinner-track: var(--initial-spinner-track);
--spinner-top: var(--initial-spinner-top);
color-scheme: dark;
}
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: var(--bg-color);
color: var(--text-color);
}
#root {
width: 100%;
height: 100%;
}
#initial-loading-overlay {
position: fixed;
inset: 0;
background: var(--bg-color);
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
transition: opacity 0.3s ease;
}
#initial-loading-overlay[data-hidden="true"] {
opacity: 0;
pointer-events: none;
}
.initial-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--spinner-track);
border-top: 3px solid var(--spinner-top);
border-radius: 50%;
animation: initial-spin 1s linear infinite;
}
@keyframes initial-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="initial-loading-overlay">
<div class="initial-spinner"></div>
<div style="font-size: 14px; opacity: 0.7; margin-top: 20px">
Loading Clash Verge...
</div>
</div>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
+96 -18
View File
@@ -55,9 +55,45 @@ document.addEventListener("keydown", (event) => {
}
});
const initializeApp = () => {
let cachedVergeConfig: IVergeConfig | null = null;
const detectSystemTheme = (): "light" | "dark" => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
const getInitialThemeModeFromWindow = ():
| IVergeConfig["theme_mode"]
| undefined => {
if (typeof window === "undefined") return undefined;
const mode = (
window as typeof window & {
__VERGE_INITIAL_THEME_MODE?: unknown;
}
).__VERGE_INITIAL_THEME_MODE;
if (mode === "light" || mode === "dark" || mode === "system") {
return mode;
}
return undefined;
};
const resolveInitialThemeMode = (
vergeConfig?: IVergeConfig | null,
): "light" | "dark" => {
const initialMode =
vergeConfig?.theme_mode ?? getInitialThemeModeFromWindow();
if (initialMode === "dark" || initialMode === "light") {
return initialMode;
}
return detectSystemTheme();
};
const initializeApp = (initialThemeMode: "light" | "dark") => {
const contexts = [
<ThemeModeProvider key="theme" />,
<ThemeModeProvider key="theme" initialState={initialThemeMode} />,
<LoadingCacheProvider key="loading" />,
<UpdateStateProvider key="update" />,
];
@@ -78,24 +114,47 @@ const initializeApp = () => {
);
};
const determineInitialLanguage = async () => {
const determineInitialLanguage = async (
vergeConfig?: IVergeConfig | null,
loadVergeConfig?: () => Promise<IVergeConfig | null>,
) => {
const cachedLanguage = getCachedLanguage();
if (cachedLanguage) {
return cachedLanguage;
}
try {
const vergeConfig = await getVergeConfig();
if (vergeConfig?.language) {
const resolved = resolveLanguage(vergeConfig.language);
cacheLanguage(resolved);
return resolved;
let resolvedConfig = vergeConfig;
if (resolvedConfig === undefined) {
if (loadVergeConfig) {
try {
resolvedConfig = await loadVergeConfig();
} catch (error) {
console.warn(
"[main.tsx] Failed to read language from Verge config:",
error,
);
resolvedConfig = null;
}
} else {
try {
resolvedConfig = await getVergeConfig();
cachedVergeConfig = resolvedConfig;
} catch (error) {
console.warn(
"[main.tsx] Failed to read language from Verge config:",
error,
);
resolvedConfig = null;
}
}
} catch (error) {
console.warn(
"[main.tsx] Failed to read language from Verge config:",
error,
);
}
const languageFromConfig = resolvedConfig?.language;
if (languageFromConfig) {
const resolved = resolveLanguage(languageFromConfig);
cacheLanguage(resolved);
return resolved;
}
const browserLanguage = resolveLanguage(
@@ -105,10 +164,29 @@ const determineInitialLanguage = async () => {
return browserLanguage;
};
const fetchVergeConfig = async () => {
try {
const config = await getVergeConfig();
cachedVergeConfig = config;
return config;
} catch (error) {
console.warn("[main.tsx] Failed to read Verge config:", error);
return null;
}
};
const bootstrap = async () => {
const initialLanguage = await determineInitialLanguage();
await initializeLanguage(initialLanguage);
initializeApp();
const vergeConfigPromise = fetchVergeConfig();
const initialLanguage = await determineInitialLanguage(
undefined,
() => vergeConfigPromise,
);
const [vergeConfig] = await Promise.all([
vergeConfigPromise,
initializeLanguage(initialLanguage),
]);
const initialThemeMode = resolveInitialThemeMode(vergeConfig);
initializeApp(initialThemeMode);
};
bootstrap().catch((error) => {
@@ -124,7 +202,7 @@ bootstrap().catch((error) => {
);
})
.finally(() => {
initializeApp();
initializeApp(resolveInitialThemeMode(cachedVergeConfig));
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { LogLevel } from "tauri-plugin-mihomo-api";
const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
"light" | "dark"
>("light");
>();
export type LogFilter = "all" | "debug" | "info" | "warn" | "err";
export type LogOrder = "asc" | "desc";