mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-18 08:21:34 +08:00
refactor: eliminate startup flicker — defer window show until overlay renders
- Remove Rust-side `eval(INITIAL_LOADING_OVERLAY)` that prematurely dismissed the overlay before React/MUI theme was ready - Defer `window.show()` from Rust `activate_window` to an inline `<script>` in index.html, executed after the themed overlay is in DOM - Remove `useAppInitialization` hook (duplicate of `useLoadingOverlay` with no themeReady gate) - Simplify overlay to pure theme-colored background — no spinner or loading text — so fast startup feels instant - Simplify `hideInitialOverlay` API and reduce overlay fade to 0.2s - Clean up unused CSS variables (spinner-track, spinner-top, etc.)
This commit is contained in:
parent
04ce3d1772
commit
ec82b69786
@ -2,11 +2,7 @@ use dark_light::{Mode as SystemTheme, detect as detect_system_theme};
|
|||||||
use tauri::utils::config::Color;
|
use tauri::utils::config::Color;
|
||||||
use tauri::{Theme, WebviewWindow};
|
use tauri::{Theme, WebviewWindow};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::Config, core::handle, utils::resolve::window_script::build_window_initial_script};
|
||||||
config::Config,
|
|
||||||
core::handle,
|
|
||||||
utils::resolve::window_script::{INITIAL_LOADING_OVERLAY, build_window_initial_script},
|
|
||||||
};
|
|
||||||
use clash_verge_logging::{Type, logging_error};
|
use clash_verge_logging::{Type, logging_error};
|
||||||
|
|
||||||
const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D
|
const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D
|
||||||
@ -82,7 +78,6 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
|||||||
match builder.build() {
|
match builder.build() {
|
||||||
Ok(window) => {
|
Ok(window) => {
|
||||||
logging_error!(Type::Window, window.set_background_color(Some(background_color)));
|
logging_error!(Type::Window, window.set_background_color(Some(background_color)));
|
||||||
logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY));
|
|
||||||
Ok(window)
|
Ok(window)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
|
|||||||
@ -91,11 +91,3 @@ pub const WINDOW_INITIAL_SCRIPT: &str = r##"
|
|||||||
|
|
||||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
console.log('[Tauri] 窗口初始化脚本执行完成');
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
pub const INITIAL_LOADING_OVERLAY: &str = r"
|
|
||||||
const overlay = document.getElementById('initial-loading-overlay');
|
|
||||||
if (overlay) {
|
|
||||||
overlay.style.opacity = '0';
|
|
||||||
setTimeout(() => overlay.remove(), 300);
|
|
||||||
}
|
|
||||||
";
|
|
||||||
|
|||||||
@ -285,6 +285,7 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 创建新窗口,防抖避免重复调用
|
/// 创建新窗口,防抖避免重复调用
|
||||||
|
/// 窗口创建后保持隐藏,由前端 index.html 在 overlay 渲染后调用 show,避免主题闪烁
|
||||||
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
logging!(info, Type::Window, "开始创建/显示主窗口, is_show={}", is_show);
|
logging!(info, Type::Window, "开始创建/显示主窗口, is_show={}", is_show);
|
||||||
@ -293,23 +294,22 @@ impl WindowManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = match build_new_window().await {
|
match build_new_window().await {
|
||||||
Ok(window) => {
|
Ok(_) => {
|
||||||
logging!(info, Type::Window, "新窗口创建成功");
|
logging!(info, Type::Window, "新窗口创建成功,等待前端渲染后显示");
|
||||||
window
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
handle::Handle::global().set_activation_policy_regular();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Window, "新窗口创建失败: {}", e);
|
logging!(error, Type::Window, "新窗口创建失败: {}", e);
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 直接激活刚创建的窗口,避免因防抖导致首次显示被跳过
|
|
||||||
if WindowOperationResult::Failed == Self::activate_window(&window) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,27 +11,15 @@
|
|||||||
<title>Clash Verge</title>
|
<title>Clash Verge</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--initial-bg: #f5f5f5;
|
--bg-color: #f5f5f5;
|
||||||
--initial-text: #333;
|
--text-color: #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;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--initial-bg: #2e303d;
|
--bg-color: #2e303d;
|
||||||
--initial-text: #ffffff;
|
--text-color: #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;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,48 +41,28 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
font-family:
|
transition: opacity 0.2s ease-out;
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initial-loading-overlay[data-hidden='true'] {
|
#initial-loading-overlay[data-hidden='true'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="initial-loading-overlay">
|
<div id="initial-loading-overlay"></div>
|
||||||
<div class="initial-spinner"></div>
|
<script>
|
||||||
<div style="font-size: 14px; opacity: 0.7; margin-top: 20px">
|
if (window.__TAURI_INTERNALS__) {
|
||||||
Loading Clash Verge...
|
window.__TAURI_INTERNALS__
|
||||||
</div>
|
.invoke('plugin:window|show', { label: 'main' })
|
||||||
</div>
|
.catch(function () {});
|
||||||
|
window.__TAURI_INTERNALS__
|
||||||
|
.invoke('plugin:window|set_focus', { label: 'main' })
|
||||||
|
.catch(function () {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./main.tsx"></script>
|
<script type="module" src="./main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -44,7 +44,6 @@ import { useThemeMode } from '@/services/states'
|
|||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppInitialization,
|
|
||||||
useCustomTheme,
|
useCustomTheme,
|
||||||
useLayoutEvents,
|
useLayoutEvents,
|
||||||
useLoadingOverlay,
|
useLoadingOverlay,
|
||||||
@ -217,7 +216,6 @@ const Layout = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useLoadingOverlay(themeReady)
|
useLoadingOverlay(themeReady)
|
||||||
useAppInitialization()
|
|
||||||
|
|
||||||
const handleNotice = useCallback(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export { useAppInitialization } from './use-app-initialization'
|
|
||||||
export { useLayoutEvents } from './use-layout-events'
|
export { useLayoutEvents } from './use-layout-events'
|
||||||
export { useLoadingOverlay } from './use-loading-overlay'
|
export { useLoadingOverlay } from './use-loading-overlay'
|
||||||
export { useNavMenuOrder } from './use-nav-menu-order'
|
export { useNavMenuOrder } from './use-nav-menu-order'
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
import { hideInitialOverlay } from '../utils'
|
|
||||||
|
|
||||||
export const useAppInitialization = () => {
|
|
||||||
const initRef = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initRef.current) return
|
|
||||||
initRef.current = true
|
|
||||||
|
|
||||||
let isCancelled = false
|
|
||||||
const timers = new Set<number>()
|
|
||||||
|
|
||||||
const scheduleTimeout = (handler: () => void, delay: number) => {
|
|
||||||
if (isCancelled) return -1
|
|
||||||
const id = window.setTimeout(() => {
|
|
||||||
if (!isCancelled) {
|
|
||||||
handler()
|
|
||||||
}
|
|
||||||
timers.delete(id)
|
|
||||||
}, delay)
|
|
||||||
timers.add(id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLoadingOverlay = () => {
|
|
||||||
hideInitialOverlay({ schedule: scheduleTimeout })
|
|
||||||
}
|
|
||||||
|
|
||||||
const performInitialization = () => {
|
|
||||||
if (isCancelled) return
|
|
||||||
removeLoadingOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleTimeout(performInitialization, 100)
|
|
||||||
scheduleTimeout(performInitialization, 5000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCancelled = true
|
|
||||||
timers.forEach((id) => {
|
|
||||||
try {
|
|
||||||
window.clearTimeout(id)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Initialization] Failed to clear timer:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
timers.clear()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
@ -3,46 +3,15 @@ import { useEffect, useRef } from 'react'
|
|||||||
import { hideInitialOverlay } from '../utils'
|
import { hideInitialOverlay } from '../utils'
|
||||||
|
|
||||||
export const useLoadingOverlay = (themeReady: boolean) => {
|
export const useLoadingOverlay = (themeReady: boolean) => {
|
||||||
const overlayRemovedRef = useRef(false)
|
const doneRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!themeReady || overlayRemovedRef.current) return
|
if (!themeReady || doneRef.current) return
|
||||||
|
doneRef.current = true
|
||||||
let removalTimer: number | undefined
|
|
||||||
let retryTimer: number | undefined
|
|
||||||
let attempts = 0
|
|
||||||
const maxAttempts = 50
|
|
||||||
let stopped = false
|
|
||||||
|
|
||||||
const tryRemoveOverlay = () => {
|
|
||||||
if (stopped || overlayRemovedRef.current) return
|
|
||||||
|
|
||||||
const { removed, removalTimer: timerId } = hideInitialOverlay({
|
|
||||||
assumeMissingAsRemoved: true,
|
|
||||||
})
|
|
||||||
if (typeof timerId === 'number') {
|
|
||||||
removalTimer = timerId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed) {
|
|
||||||
overlayRemovedRef.current = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempts < maxAttempts) {
|
|
||||||
attempts += 1
|
|
||||||
retryTimer = window.setTimeout(tryRemoveOverlay, 100)
|
|
||||||
} else {
|
|
||||||
console.warn('[Loading Overlay] Element not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryRemoveOverlay()
|
|
||||||
|
|
||||||
|
const timer = hideInitialOverlay()
|
||||||
return () => {
|
return () => {
|
||||||
stopped = true
|
if (timer !== undefined) window.clearTimeout(timer)
|
||||||
if (typeof removalTimer === 'number') window.clearTimeout(removalTimer)
|
|
||||||
if (typeof retryTimer === 'number') window.clearTimeout(retryTimer)
|
|
||||||
}
|
}
|
||||||
}, [themeReady])
|
}, [themeReady])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1,17 @@
|
|||||||
const OVERLAY_ID = 'initial-loading-overlay'
|
let removed = false
|
||||||
const REMOVE_DELAY = 300
|
|
||||||
|
|
||||||
let overlayRemoved = false
|
export const hideInitialOverlay = (): number | undefined => {
|
||||||
|
if (removed) return undefined
|
||||||
|
|
||||||
type HideOverlayOptions = {
|
const overlay = document.getElementById('initial-loading-overlay')
|
||||||
schedule?: (handler: () => void, delay: number) => number
|
|
||||||
assumeMissingAsRemoved?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type HideOverlayResult = {
|
|
||||||
removed: boolean
|
|
||||||
removalTimer?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hideInitialOverlay = (
|
|
||||||
options: HideOverlayOptions = {},
|
|
||||||
): HideOverlayResult => {
|
|
||||||
if (overlayRemoved) {
|
|
||||||
return { removed: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlay = document.getElementById(OVERLAY_ID)
|
|
||||||
if (!overlay) {
|
if (!overlay) {
|
||||||
if (options.assumeMissingAsRemoved) {
|
removed = true
|
||||||
overlayRemoved = true
|
return undefined
|
||||||
return { removed: true }
|
|
||||||
}
|
|
||||||
return { removed: false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayRemoved = true
|
removed = true
|
||||||
overlay.dataset.hidden = 'true'
|
overlay.dataset.hidden = 'true'
|
||||||
|
|
||||||
const schedule = options.schedule ?? window.setTimeout
|
const timer = window.setTimeout(() => overlay.remove(), 200)
|
||||||
const removalTimer = schedule(() => {
|
return timer
|
||||||
try {
|
|
||||||
overlay.remove()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Loading Overlay] Removal failed:', error)
|
|
||||||
}
|
|
||||||
}, REMOVE_DELAY)
|
|
||||||
|
|
||||||
return { removed: true, removalTimer }
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user