mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-14 04:00:27 +00:00
feat: WebView optimization
This commit is contained in:
parent
eb42c7101d
commit
dd762d53b2
@ -27,9 +27,6 @@ class RustWebViewController {
|
|||||||
/// 本地化脚本(从 assets 加载)
|
/// 本地化脚本(从 assets 加载)
|
||||||
String _localizationScript = "";
|
String _localizationScript = "";
|
||||||
|
|
||||||
/// 请求拦截器脚本
|
|
||||||
String _requestInterceptorScript = "";
|
|
||||||
|
|
||||||
/// 当前 URL
|
/// 当前 URL
|
||||||
String _currentUrl = "";
|
String _currentUrl = "";
|
||||||
String get currentUrl => _currentUrl;
|
String get currentUrl => _currentUrl;
|
||||||
@ -76,7 +73,6 @@ class RustWebViewController {
|
|||||||
Future<void> _loadScripts() async {
|
Future<void> _loadScripts() async {
|
||||||
try {
|
try {
|
||||||
_localizationScript = await rootBundle.loadString('assets/web_script.js');
|
_localizationScript = await rootBundle.loadString('assets/web_script.js');
|
||||||
_requestInterceptorScript = await rootBundle.loadString('assets/request_interceptor.js');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dPrint("Failed to load scripts: $e");
|
dPrint("Failed to load scripts: $e");
|
||||||
}
|
}
|
||||||
@ -120,10 +116,6 @@ class RustWebViewController {
|
|||||||
case rust_webview.WebViewEvent_NavigationCompleted(:final url):
|
case rust_webview.WebViewEvent_NavigationCompleted(:final url):
|
||||||
dPrint("Navigation completed: $url");
|
dPrint("Navigation completed: $url");
|
||||||
_currentUrl = url;
|
_currentUrl = url;
|
||||||
// 注入请求拦截器
|
|
||||||
if (_requestInterceptorScript.isNotEmpty) {
|
|
||||||
executeScript(_requestInterceptorScript);
|
|
||||||
}
|
|
||||||
// 导航完成回调(用于注入脚本)
|
// 导航完成回调(用于注入脚本)
|
||||||
for (final callback in _navigationCompletedCallbacks) {
|
for (final callback in _navigationCompletedCallbacks) {
|
||||||
callback(url);
|
callback(url);
|
||||||
|
|||||||
@ -36,7 +36,6 @@ class WebViewModel {
|
|||||||
final localizationResource = <String, dynamic>{};
|
final localizationResource = <String, dynamic>{};
|
||||||
|
|
||||||
var localizationScript = "";
|
var localizationScript = "";
|
||||||
var requestInterceptorScript = "";
|
|
||||||
|
|
||||||
bool enableCapture = false;
|
bool enableCapture = false;
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ class WebViewModel {
|
|||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
|
||||||
);
|
);
|
||||||
|
|
||||||
webview.addOnNavigationCallback(_onNavigation);
|
|
||||||
// 添加导航完成回调(用于注入脚本)
|
// 添加导航完成回调(用于注入脚本)
|
||||||
webview.addOnNavigationCompletedCallback(_onNavigationCompleted);
|
webview.addOnNavigationCompletedCallback(_onNavigationCompleted);
|
||||||
|
|
||||||
@ -208,7 +206,6 @@ class WebViewModel {
|
|||||||
|
|
||||||
Future<void> initLocalization(AppWebLocalizationVersionsData v) async {
|
Future<void> initLocalization(AppWebLocalizationVersionsData v) async {
|
||||||
localizationScript = await rootBundle.loadString('assets/web_script.js');
|
localizationScript = await rootBundle.loadString('assets/web_script.js');
|
||||||
requestInterceptorScript = await rootBundle.loadString('assets/request_interceptor.js');
|
|
||||||
|
|
||||||
/// https://github.com/CxJuice/Uex_Chinese_Translate
|
/// https://github.com/CxJuice/Uex_Chinese_Translate
|
||||||
// get versions
|
// get versions
|
||||||
@ -282,19 +279,10 @@ class WebViewModel {
|
|||||||
|
|
||||||
FutureOr<void> dispose() {
|
FutureOr<void> dispose() {
|
||||||
webview.removeOnNavigationCompletedCallback(_onNavigationCompleted);
|
webview.removeOnNavigationCompletedCallback(_onNavigationCompleted);
|
||||||
webview.removeOnNavigationCallback(_onNavigation);
|
|
||||||
if (loginMode && !_loginModeSuccess) {
|
if (loginMode && !_loginModeSuccess) {
|
||||||
loginCallback?.call(null, false);
|
loginCallback?.call(null, false);
|
||||||
}
|
}
|
||||||
_isClosed = true;
|
_isClosed = true;
|
||||||
webview.dispose();
|
webview.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNavigation(String url) {
|
|
||||||
// 在页面加载时注入拦截器
|
|
||||||
if (requestInterceptorScript.isNotEmpty) {
|
|
||||||
dPrint("Injecting request interceptor for: $url");
|
|
||||||
webview.executeScript(requestInterceptorScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::sync::atomic::Ordering;
|
|||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::webview::{WEBVIEW_INSTANCES, send_command};
|
use crate::webview::{send_command, WEBVIEW_INSTANCES};
|
||||||
|
|
||||||
// ============ Data Structures ============
|
// ============ Data Structures ============
|
||||||
|
|
||||||
@ -84,6 +84,7 @@ pub enum WebViewCommand {
|
|||||||
Close,
|
Close,
|
||||||
SetWindowSize(u32, u32),
|
SetWindowSize(u32, u32),
|
||||||
SetWindowPosition(i32, i32),
|
SetWindowPosition(i32, i32),
|
||||||
|
SetWindowTitle(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Public API ============
|
// ============ Public API ============
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/// ------- Request Interceptor Script --------------
|
/// ------- Request Interceptor Script --------------
|
||||||
/// 轻量级网络请求拦截器,不破坏网页正常功能
|
/// 轻量级网络请求拦截器,不破坏网页正常功能
|
||||||
(function() {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (window._sctRequestInterceptorInstalled) {
|
if (window._sctRequestInterceptorInstalled) {
|
||||||
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
// ============ 1. 拦截 Fetch API ============
|
// ============ 1. 拦截 Fetch API ============
|
||||||
const originalFetch = window.fetch;
|
const originalFetch = window.fetch;
|
||||||
window.fetch = function(...args) {
|
window.fetch = function (...args) {
|
||||||
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
||||||
if (shouldBlock(url)) {
|
if (shouldBlock(url)) {
|
||||||
logBlocked('fetch', url);
|
logBlocked('fetch', url);
|
||||||
@ -59,7 +59,7 @@
|
|||||||
const originalXHROpen = OriginalXHR.prototype.open;
|
const originalXHROpen = OriginalXHR.prototype.open;
|
||||||
const originalXHRSend = OriginalXHR.prototype.send;
|
const originalXHRSend = OriginalXHR.prototype.send;
|
||||||
|
|
||||||
OriginalXHR.prototype.open = function(method, url, ...rest) {
|
OriginalXHR.prototype.open = function (method, url, ...rest) {
|
||||||
this._url = url;
|
this._url = url;
|
||||||
if (shouldBlock(url)) {
|
if (shouldBlock(url)) {
|
||||||
logBlocked('XHR', url);
|
logBlocked('XHR', url);
|
||||||
@ -68,7 +68,7 @@
|
|||||||
return originalXHROpen.apply(this, [method, url, ...rest]);
|
return originalXHROpen.apply(this, [method, url, ...rest]);
|
||||||
};
|
};
|
||||||
|
|
||||||
OriginalXHR.prototype.send = function(...args) {
|
OriginalXHR.prototype.send = function (...args) {
|
||||||
if (this._blocked) {
|
if (this._blocked) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const errorEvent = new Event('error');
|
const errorEvent = new Event('error');
|
||||||
@ -84,7 +84,7 @@
|
|||||||
if (imgSrcDescriptor && imgSrcDescriptor.set) {
|
if (imgSrcDescriptor && imgSrcDescriptor.set) {
|
||||||
Object.defineProperty(HTMLImageElement.prototype, 'src', {
|
Object.defineProperty(HTMLImageElement.prototype, 'src', {
|
||||||
get: imgSrcDescriptor.get,
|
get: imgSrcDescriptor.get,
|
||||||
set: function(value) {
|
set: function (value) {
|
||||||
if (shouldBlock(value)) {
|
if (shouldBlock(value)) {
|
||||||
logBlocked('IMG.src', value);
|
logBlocked('IMG.src', value);
|
||||||
// 设置为透明 GIF,避免请求
|
// 设置为透明 GIF,避免请求
|
||||||
@ -104,7 +104,7 @@
|
|||||||
if (scriptSrcDescriptor && scriptSrcDescriptor.set) {
|
if (scriptSrcDescriptor && scriptSrcDescriptor.set) {
|
||||||
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
|
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
|
||||||
get: scriptSrcDescriptor.get,
|
get: scriptSrcDescriptor.get,
|
||||||
set: function(value) {
|
set: function (value) {
|
||||||
if (shouldBlock(value)) {
|
if (shouldBlock(value)) {
|
||||||
logBlocked('SCRIPT.src', value);
|
logBlocked('SCRIPT.src', value);
|
||||||
// 阻止加载,不设置 src
|
// 阻止加载,不设置 src
|
||||||
@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
// ============ 4. 拦截 setAttribute(用于 img.setAttribute('src', ...))============
|
// ============ 4. 拦截 setAttribute(用于 img.setAttribute('src', ...))============
|
||||||
const originalSetAttribute = Element.prototype.setAttribute;
|
const originalSetAttribute = Element.prototype.setAttribute;
|
||||||
Element.prototype.setAttribute = function(name, value) {
|
Element.prototype.setAttribute = function (name, value) {
|
||||||
if (name.toLowerCase() === 'src' && this.tagName === 'IMG' && shouldBlock(value)) {
|
if (name.toLowerCase() === 'src' && this.tagName === 'IMG' && shouldBlock(value)) {
|
||||||
logBlocked('IMG setAttribute', value);
|
logBlocked('IMG setAttribute', value);
|
||||||
originalSetAttribute.call(this, name, TRANSPARENT_GIF);
|
originalSetAttribute.call(this, name, TRANSPARENT_GIF);
|
||||||
@ -137,7 +137,7 @@
|
|||||||
// ============ 5. 拦截 navigator.sendBeacon ============
|
// ============ 5. 拦截 navigator.sendBeacon ============
|
||||||
if (navigator.sendBeacon) {
|
if (navigator.sendBeacon) {
|
||||||
const originalSendBeacon = navigator.sendBeacon.bind(navigator);
|
const originalSendBeacon = navigator.sendBeacon.bind(navigator);
|
||||||
navigator.sendBeacon = function(url, data) {
|
navigator.sendBeacon = function (url, data) {
|
||||||
if (shouldBlock(url)) {
|
if (shouldBlock(url)) {
|
||||||
logBlocked('sendBeacon', url);
|
logBlocked('sendBeacon', url);
|
||||||
return true; // 假装成功
|
return true; // 假装成功
|
||||||
@ -22,12 +22,26 @@ use crate::api::webview_api::{
|
|||||||
WebViewCommand, WebViewConfiguration, WebViewEvent, WebViewNavigationState,
|
WebViewCommand, WebViewConfiguration, WebViewEvent, WebViewNavigationState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============ Loading Progress Animation ============
|
||||||
|
|
||||||
|
/// Braille spinner characters for loading animation
|
||||||
|
/// These are standard Unicode characters that work across all platforms
|
||||||
|
const SPINNER_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||||
|
|
||||||
|
/// Get the progress animation character for the current frame
|
||||||
|
fn get_progress_char(frame: usize) -> char {
|
||||||
|
SPINNER_CHARS[frame % SPINNER_CHARS.len()]
|
||||||
|
}
|
||||||
|
|
||||||
// Embed the app icon at compile time
|
// Embed the app icon at compile time
|
||||||
static APP_ICON_DATA: &[u8] = include_bytes!("../../../windows/runner/resources/app_icon.ico");
|
static APP_ICON_DATA: &[u8] = include_bytes!("../../../windows/runner/resources/app_icon.ico");
|
||||||
|
|
||||||
// Embed the init script at compile time
|
// Embed the init script at compile time
|
||||||
static INIT_SCRIPT: &str = include_str!("webview_init_script.js");
|
static INIT_SCRIPT: &str = include_str!("webview_init_script.js");
|
||||||
|
|
||||||
|
// Embed the request interceptor script at compile time
|
||||||
|
static REQUEST_INTERCEPTOR_SCRIPT: &str = include_str!("request_interceptor.js");
|
||||||
|
|
||||||
// Loading page HTML for about:blank initialization
|
// Loading page HTML for about:blank initialization
|
||||||
static LOADING_PAGE_HTML: &str = r#"<!DOCTYPE html>
|
static LOADING_PAGE_HTML: &str = r#"<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@ -286,10 +300,14 @@ fn run_webview_loop(
|
|||||||
// When CF challenge is detected, we set this to 1, skip next load, then resume normal injection
|
// When CF challenge is detected, we set this to 1, skip next load, then resume normal injection
|
||||||
let cf_skip_count = Arc::new(AtomicU32::new(0));
|
let cf_skip_count = Arc::new(AtomicU32::new(0));
|
||||||
|
|
||||||
|
// Store original title for restoration after loading
|
||||||
|
let original_title = Arc::new(RwLock::new(config.title.clone()));
|
||||||
|
|
||||||
let event_tx_clone = event_tx.clone();
|
let event_tx_clone = event_tx.clone();
|
||||||
let nav_history_ipc = Arc::clone(&nav_history);
|
let nav_history_ipc = Arc::clone(&nav_history);
|
||||||
let state_ipc = Arc::clone(&state);
|
let state_ipc = Arc::clone(&state);
|
||||||
let cf_skip_count_ipc = Arc::clone(&cf_skip_count);
|
let cf_skip_count_ipc = Arc::clone(&cf_skip_count);
|
||||||
|
let original_title_ipc = Arc::clone(&original_title);
|
||||||
|
|
||||||
// Create web context with custom data directory if provided
|
// Create web context with custom data directory if provided
|
||||||
let mut web_context = config
|
let mut web_context = config
|
||||||
@ -302,7 +320,8 @@ fn run_webview_loop(
|
|||||||
.with_url("about:blank")
|
.with_url("about:blank")
|
||||||
.with_devtools(config.enable_devtools)
|
.with_devtools(config.enable_devtools)
|
||||||
.with_transparent(config.transparent)
|
.with_transparent(config.transparent)
|
||||||
.with_background_color((10, 29, 41, 255));
|
.with_background_color((10, 29, 41, 255))
|
||||||
|
.with_initialization_script(REQUEST_INTERCEPTOR_SCRIPT);
|
||||||
|
|
||||||
// Set user agent if provided
|
// Set user agent if provided
|
||||||
if let Some(ref user_agent) = config.user_agent {
|
if let Some(ref user_agent) = config.user_agent {
|
||||||
@ -362,6 +381,36 @@ fn run_webview_loop(
|
|||||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"loading_progress" => {
|
||||||
|
// Update window title with loading progress
|
||||||
|
if let Some(payload) = parsed.get("payload") {
|
||||||
|
let progress = payload.get("progress").and_then(|v| v.as_f64());
|
||||||
|
let is_loading = payload.get("is_loading").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
let dot_frame = payload.get("dot_frame").and_then(|v| v.as_i64()).unwrap_or(0) as usize;
|
||||||
|
|
||||||
|
let base_title = original_title_ipc.read().clone();
|
||||||
|
|
||||||
|
let new_title = if is_loading {
|
||||||
|
// Get Windows progress animation character
|
||||||
|
let progress_char = get_progress_char(dot_frame);
|
||||||
|
|
||||||
|
if let Some(p) = progress {
|
||||||
|
let percent = (p * 100.0).round() as i32;
|
||||||
|
format!("{} {} {}%", base_title, progress_char, percent)
|
||||||
|
} else {
|
||||||
|
format!("{} {} Loading", base_title, progress_char)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Loading complete, restore original title
|
||||||
|
base_title
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = proxy_ipc.send_event(UserEvent::Command(
|
||||||
|
WebViewCommand::SetWindowTitle(new_title)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,6 +424,7 @@ fn run_webview_loop(
|
|||||||
let state = Arc::clone(&state);
|
let state = Arc::clone(&state);
|
||||||
let nav_history = Arc::clone(&nav_history);
|
let nav_history = Arc::clone(&nav_history);
|
||||||
let proxy = proxy.clone();
|
let proxy = proxy.clone();
|
||||||
|
let original_title = Arc::clone(&original_title);
|
||||||
move |uri| {
|
move |uri| {
|
||||||
// Block navigation to about:blank (except initial load)
|
// Block navigation to about:blank (except initial load)
|
||||||
// This prevents users from going back to the blank initial page
|
// This prevents users from going back to the blank initial page
|
||||||
@ -408,6 +458,17 @@ fn run_webview_loop(
|
|||||||
state_guard.can_go_forward = can_forward;
|
state_guard.can_go_forward = can_forward;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update window title immediately to show loading state
|
||||||
|
// This provides instant feedback before JS is injected
|
||||||
|
{
|
||||||
|
let base_title = original_title.read().clone();
|
||||||
|
let progress_char = get_progress_char(0);
|
||||||
|
let loading_title = format!("{} {} 0%", base_title, progress_char);
|
||||||
|
let _ = proxy.send_event(UserEvent::Command(
|
||||||
|
WebViewCommand::SetWindowTitle(loading_title)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Send loading state to JS immediately when navigation starts
|
// Send loading state to JS immediately when navigation starts
|
||||||
// This makes the spinner appear on the current page before navigating away
|
// This makes the spinner appear on the current page before navigating away
|
||||||
let state_json = serde_json::json!({
|
let state_json = serde_json::json!({
|
||||||
@ -436,8 +497,15 @@ fn run_webview_loop(
|
|||||||
if url == "about:blank" {
|
if url == "about:blank" {
|
||||||
if matches!(event, PageLoadEvent::Finished) {
|
if matches!(event, PageLoadEvent::Finished) {
|
||||||
// if url is about:blank, show loading
|
// if url is about:blank, show loading
|
||||||
|
// Add URL check in JS to prevent injection if page has already navigated away
|
||||||
let loading_script = format!(
|
let loading_script = format!(
|
||||||
r#"document.open(); document.write({}); document.close();"#,
|
r#"(function() {{
|
||||||
|
if (window.location.href === 'about:blank') {{
|
||||||
|
document.open();
|
||||||
|
document.write({});
|
||||||
|
document.close();
|
||||||
|
}}
|
||||||
|
}})();"#,
|
||||||
serde_json::to_string(LOADING_PAGE_HTML).unwrap_or_default()
|
serde_json::to_string(LOADING_PAGE_HTML).unwrap_or_default()
|
||||||
);
|
);
|
||||||
let _ = proxy.send_event(UserEvent::Command(
|
let _ = proxy.send_event(UserEvent::Command(
|
||||||
@ -728,6 +796,9 @@ fn handle_command(
|
|||||||
WebViewCommand::SetWindowPosition(x, y) => {
|
WebViewCommand::SetWindowPosition(x, y) => {
|
||||||
window.set_outer_position(LogicalPosition::new(x, y));
|
window.set_outer_position(LogicalPosition::new(x, y));
|
||||||
}
|
}
|
||||||
|
WebViewCommand::SetWindowTitle(title) => {
|
||||||
|
window.set_title(&title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,144 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Loading Progress Tracker ==========
|
||||||
|
// Tracks page loading progress and updates window title via IPC
|
||||||
|
// Uses Braille spinner characters: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (10 frames)
|
||||||
|
(function initProgressTracker() {
|
||||||
|
var dotFrame = 0;
|
||||||
|
var progressInterval = null;
|
||||||
|
var estimatedProgress = 0;
|
||||||
|
var isPageLoading = true;
|
||||||
|
var lastProgressSent = -1;
|
||||||
|
|
||||||
|
// Send progress update to Rust (for window title)
|
||||||
|
function sendProgressUpdate(progress, loading) {
|
||||||
|
// Always send update for animation frames
|
||||||
|
lastProgressSent = (loading ? 1 : 0) * 1000 + Math.round((progress || 0) * 100);
|
||||||
|
|
||||||
|
sendToRust('loading_progress', {
|
||||||
|
progress: progress,
|
||||||
|
is_loading: loading,
|
||||||
|
dot_frame: dotFrame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the dot animation and progress estimation
|
||||||
|
function startProgressAnimation() {
|
||||||
|
if (progressInterval) return;
|
||||||
|
isPageLoading = true;
|
||||||
|
estimatedProgress = 0;
|
||||||
|
dotFrame = 0;
|
||||||
|
|
||||||
|
// Animation interval: update every 80ms for smooth Braille spinner animation
|
||||||
|
// 10 frames at 80ms = 800ms per full cycle
|
||||||
|
progressInterval = setInterval(function () {
|
||||||
|
if (!isPageLoading) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment frame counter (will be modulo'd by Rust to 10 frames)
|
||||||
|
dotFrame = dotFrame + 1;
|
||||||
|
|
||||||
|
// Estimate progress based on document state
|
||||||
|
var progress = estimateProgress();
|
||||||
|
sendProgressUpdate(progress, true);
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
// Send initial progress
|
||||||
|
sendProgressUpdate(0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate loading progress based on various signals
|
||||||
|
function estimateProgress() {
|
||||||
|
// Start with a base progress based on document ready state
|
||||||
|
var baseProgress = 0;
|
||||||
|
switch (document.readyState) {
|
||||||
|
case 'loading':
|
||||||
|
baseProgress = 0.15;
|
||||||
|
break;
|
||||||
|
case 'interactive':
|
||||||
|
baseProgress = 0.6;
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
baseProgress = 1.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Performance API if available for more accurate estimation
|
||||||
|
if (window.performance && window.performance.timing) {
|
||||||
|
var timing = window.performance.timing;
|
||||||
|
var now = Date.now();
|
||||||
|
|
||||||
|
if (timing.loadEventEnd > 0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timing.navigationStart > 0) {
|
||||||
|
// Estimate based on time elapsed (assuming 3-5 second load)
|
||||||
|
var elapsed = now - timing.navigationStart;
|
||||||
|
var timeProgress = Math.min(elapsed / 4000, 0.95);
|
||||||
|
baseProgress = Math.max(baseProgress, timeProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use PerformanceObserver data if available
|
||||||
|
if (window.performance && window.performance.getEntriesByType) {
|
||||||
|
var resources = window.performance.getEntriesByType('resource');
|
||||||
|
if (resources.length > 0) {
|
||||||
|
// Count completed resources vs estimated total
|
||||||
|
var completedCount = resources.filter(function (r) {
|
||||||
|
return r.responseEnd > 0;
|
||||||
|
}).length;
|
||||||
|
// Assume average page has ~30 resources
|
||||||
|
var resourceProgress = Math.min(completedCount / 30, 0.9);
|
||||||
|
baseProgress = Math.max(baseProgress, resourceProgress * 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth progress (never go backwards, approach target gradually)
|
||||||
|
if (baseProgress > estimatedProgress) {
|
||||||
|
estimatedProgress = estimatedProgress + (baseProgress - estimatedProgress) * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(estimatedProgress, 0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop progress tracking and notify completion
|
||||||
|
function stopProgressAnimation() {
|
||||||
|
isPageLoading = false;
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
}
|
||||||
|
// Send final update with 100% and loading=false
|
||||||
|
sendProgressUpdate(1.0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start tracking when page starts loading
|
||||||
|
startProgressAnimation();
|
||||||
|
|
||||||
|
// Update on ready state changes
|
||||||
|
document.addEventListener('readystatechange', function () {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
setTimeout(stopProgressAnimation, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback: stop on window load
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
setTimeout(stopProgressAnimation, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose for external control
|
||||||
|
window._sctProgressTracker = {
|
||||||
|
start: startProgressAnimation,
|
||||||
|
stop: stopProgressAnimation
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// ========== 导航栏 UI ==========
|
// ========== 导航栏 UI ==========
|
||||||
const icons = {
|
const icons = {
|
||||||
back: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
back: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user