mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 19:50:28 +00:00
fix: webview
This commit is contained in:
parent
f68b7a4380
commit
a132c85b8c
@ -4,8 +4,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:starcitizen_doctor/common/rust/api/webview_api.dart'
|
||||
as rust_webview;
|
||||
import 'package:starcitizen_doctor/common/rust/api/webview_api.dart' as rust_webview;
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
|
||||
typedef OnWebMessageCallback = void Function(String message);
|
||||
@ -77,9 +76,7 @@ class RustWebViewController {
|
||||
Future<void> _loadScripts() async {
|
||||
try {
|
||||
_localizationScript = await rootBundle.loadString('assets/web_script.js');
|
||||
_requestInterceptorScript = await rootBundle.loadString(
|
||||
'assets/request_interceptor.js',
|
||||
);
|
||||
_requestInterceptorScript = await rootBundle.loadString('assets/request_interceptor.js');
|
||||
} catch (e) {
|
||||
dPrint("Failed to load scripts: $e");
|
||||
}
|
||||
@ -289,16 +286,12 @@ class RustWebViewController {
|
||||
}
|
||||
|
||||
/// 添加导航完成回调(用于在页面加载完成后注入脚本)
|
||||
void addOnNavigationCompletedCallback(
|
||||
OnNavigationCompletedCallback callback,
|
||||
) {
|
||||
void addOnNavigationCompletedCallback(OnNavigationCompletedCallback callback) {
|
||||
_navigationCompletedCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/// 移除导航完成回调
|
||||
void removeOnNavigationCompletedCallback(
|
||||
OnNavigationCompletedCallback callback,
|
||||
) {
|
||||
void removeOnNavigationCompletedCallback(OnNavigationCompletedCallback callback) {
|
||||
_navigationCompletedCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
@ -327,9 +320,7 @@ class RustWebViewController {
|
||||
/// 更新翻译词典
|
||||
void updateReplaceWords(List<Map<String, String>> words, bool enableCapture) {
|
||||
final jsonWords = json.encode(words);
|
||||
executeScript(
|
||||
"WebLocalizationUpdateReplaceWords($jsonWords, $enableCapture)",
|
||||
);
|
||||
executeScript("WebLocalizationUpdateReplaceWords($jsonWords, $enableCapture)");
|
||||
}
|
||||
|
||||
/// 执行 RSI 登录脚本
|
||||
|
||||
@ -71,8 +71,11 @@ class WebViewModel {
|
||||
height: loginMode ? 720 : 1080,
|
||||
userDataFolder: "$applicationSupportDir/webview_data",
|
||||
enableDevtools: kDebugMode,
|
||||
userAgent:
|
||||
"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);
|
||||
|
||||
@ -104,12 +107,6 @@ class WebViewModel {
|
||||
dPrint("Navigation completed: $newUrl");
|
||||
url = newUrl;
|
||||
|
||||
// 在页面加载时注入拦截器
|
||||
if (requestInterceptorScript.isNotEmpty) {
|
||||
dPrint("Injecting request interceptor for: $url");
|
||||
webview.executeScript(requestInterceptorScript);
|
||||
}
|
||||
|
||||
if (localizationResource.isEmpty) return;
|
||||
|
||||
dPrint("webview Navigating url === $url");
|
||||
@ -285,10 +282,19 @@ class WebViewModel {
|
||||
|
||||
FutureOr<void> dispose() {
|
||||
webview.removeOnNavigationCompletedCallback(_onNavigationCompleted);
|
||||
webview.removeOnNavigationCallback(_onNavigation);
|
||||
if (loginMode && !_loginModeSuccess) {
|
||||
loginCallback?.call(null, false);
|
||||
}
|
||||
_isClosed = true;
|
||||
webview.dispose();
|
||||
}
|
||||
|
||||
void _onNavigation(String url) {
|
||||
// 在页面加载时注入拦截器
|
||||
if (requestInterceptorScript.isNotEmpty) {
|
||||
dPrint("Injecting request interceptor for: $url");
|
||||
webview.executeScript(requestInterceptorScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,24 +2,24 @@
|
||||
// 使用 wry + tao 实现跨平台 WebView
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
use parking_lot::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use tao::dpi::{LogicalPosition, LogicalSize};
|
||||
use tao::event::{Event, WindowEvent};
|
||||
use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder};
|
||||
use tao::platform::run_return::EventLoopExtRunReturn;
|
||||
use tao::window::{Icon, Window, WindowBuilder};
|
||||
use wry::{PageLoadEvent, WebView, WebViewBuilder};
|
||||
use wry::{NewWindowResponse, PageLoadEvent, WebView, WebViewBuilder};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use tao::platform::windows::EventLoopBuilderExtWindows;
|
||||
|
||||
use crate::api::webview_api::{
|
||||
WebViewConfiguration, WebViewNavigationState, WebViewEvent, WebViewCommand,
|
||||
WebViewCommand, WebViewConfiguration, WebViewEvent, WebViewNavigationState,
|
||||
};
|
||||
|
||||
// Embed the app icon at compile time
|
||||
@ -28,6 +28,71 @@ static APP_ICON_DATA: &[u8] = include_bytes!("../../../windows/runner/resources/
|
||||
// Embed the init script at compile time
|
||||
static INIT_SCRIPT: &str = include_str!("webview_init_script.js");
|
||||
|
||||
// Loading page HTML for about:blank initialization
|
||||
static LOADING_PAGE_HTML: &str = r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading...</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
background: #0A1D29;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.16);
|
||||
border-top-color: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.loading-text {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.loading-dots::after {
|
||||
content: '';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
@keyframes dots {
|
||||
0%, 20% { content: ''; }
|
||||
40% { content: '.'; }
|
||||
60% { content: '..'; }
|
||||
80%, 100% { content: '...'; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading-container">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">Loading<span class="loading-dots"></span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
pub type WebViewId = String;
|
||||
@ -46,30 +111,32 @@ impl NavigationHistory {
|
||||
current_index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn push(&mut self, url: &str) {
|
||||
if url == "about:blank" {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1) {
|
||||
|
||||
if self.current_index >= 0
|
||||
&& (self.current_index as usize) < self.urls.len().saturating_sub(1)
|
||||
{
|
||||
self.urls.truncate((self.current_index + 1) as usize);
|
||||
}
|
||||
|
||||
|
||||
if self.urls.last().map(|s| s.as_str()) != Some(url) {
|
||||
self.urls.push(url.to_string());
|
||||
}
|
||||
self.current_index = (self.urls.len() as i32) - 1;
|
||||
}
|
||||
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
self.current_index > 0
|
||||
}
|
||||
|
||||
|
||||
fn can_go_forward(&self) -> bool {
|
||||
self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
|
||||
fn go_back(&mut self) -> bool {
|
||||
if self.can_go_back() {
|
||||
self.current_index -= 1;
|
||||
@ -78,7 +145,7 @@ impl NavigationHistory {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn go_forward(&mut self) -> bool {
|
||||
if self.can_go_forward() {
|
||||
self.current_index += 1;
|
||||
@ -87,7 +154,7 @@ impl NavigationHistory {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn current_url(&self) -> Option<&str> {
|
||||
if self.current_index >= 0 && (self.current_index as usize) < self.urls.len() {
|
||||
@ -131,7 +198,14 @@ pub fn create_webview(config: WebViewConfiguration) -> Result<String, String> {
|
||||
let is_closed_clone = Arc::clone(&is_closed);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
run_webview_loop(id_clone, config, cmd_rx, event_tx, state_clone, is_closed_clone);
|
||||
run_webview_loop(
|
||||
id_clone,
|
||||
config,
|
||||
cmd_rx,
|
||||
event_tx,
|
||||
state_clone,
|
||||
is_closed_clone,
|
||||
);
|
||||
});
|
||||
|
||||
// Wait a moment for the window to be created
|
||||
@ -179,10 +253,10 @@ fn run_webview_loop(
|
||||
let mut event_loop: EventLoop<UserEvent> = EventLoopBuilder::with_user_event()
|
||||
.with_any_thread(true)
|
||||
.build();
|
||||
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let mut event_loop: EventLoop<UserEvent> = EventLoopBuilder::with_user_event().build();
|
||||
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
// Load window icon from embedded ICO file
|
||||
@ -193,13 +267,14 @@ fn run_webview_loop(
|
||||
.with_title(&config.title)
|
||||
.with_inner_size(LogicalSize::new(config.width, config.height))
|
||||
.with_visible(true);
|
||||
|
||||
|
||||
// Set window icon if loaded successfully
|
||||
if let Some(icon) = window_icon {
|
||||
window_builder = window_builder.with_window_icon(Some(icon));
|
||||
}
|
||||
|
||||
let window = window_builder.build(&event_loop)
|
||||
|
||||
let window = window_builder
|
||||
.build(&event_loop)
|
||||
.expect("Failed to create window");
|
||||
|
||||
let window = Arc::new(window);
|
||||
@ -207,9 +282,14 @@ fn run_webview_loop(
|
||||
// Navigation history for tracking back/forward state
|
||||
let nav_history = Arc::new(RwLock::new(NavigationHistory::new()));
|
||||
|
||||
// Counter for CF bypass: number of page loads to skip init script 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 event_tx_clone = event_tx.clone();
|
||||
let nav_history_ipc = Arc::clone(&nav_history);
|
||||
let state_ipc = Arc::clone(&state);
|
||||
let cf_skip_count_ipc = Arc::clone(&cf_skip_count);
|
||||
|
||||
// Create web context with custom data directory if provided
|
||||
let mut web_context = config
|
||||
@ -222,8 +302,7 @@ fn run_webview_loop(
|
||||
.with_url("about:blank")
|
||||
.with_devtools(config.enable_devtools)
|
||||
.with_transparent(config.transparent)
|
||||
.with_background_color((26, 26, 26, 255)) // Dark background #1a1a1a
|
||||
.with_initialization_script(INIT_SCRIPT);
|
||||
.with_background_color((10, 29, 41, 255));
|
||||
|
||||
// Set user agent if provided
|
||||
if let Some(ref user_agent) = config.user_agent {
|
||||
@ -232,11 +311,11 @@ fn run_webview_loop(
|
||||
|
||||
// Store proxy for IPC commands
|
||||
let proxy_ipc = proxy.clone();
|
||||
|
||||
|
||||
let webview = builder
|
||||
.with_ipc_handler(move |message| {
|
||||
let msg = message.body().to_string();
|
||||
|
||||
|
||||
// Try to parse as navigation command from JS
|
||||
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
if let Some(msg_type) = parsed.get("type").and_then(|v| v.as_str()) {
|
||||
@ -244,19 +323,22 @@ fn run_webview_loop(
|
||||
"nav_back" => {
|
||||
let can_back = nav_history_ipc.read().can_go_back();
|
||||
if can_back {
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoBack));
|
||||
let _ = proxy_ipc
|
||||
.send_event(UserEvent::Command(WebViewCommand::GoBack));
|
||||
}
|
||||
return;
|
||||
}
|
||||
"nav_forward" => {
|
||||
let can_forward = nav_history_ipc.read().can_go_forward();
|
||||
if can_forward {
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoForward));
|
||||
let _ = proxy_ipc
|
||||
.send_event(UserEvent::Command(WebViewCommand::GoForward));
|
||||
}
|
||||
return;
|
||||
}
|
||||
"nav_reload" => {
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
||||
let _ =
|
||||
proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
||||
return;
|
||||
}
|
||||
"get_nav_state" => {
|
||||
@ -269,14 +351,22 @@ fn run_webview_loop(
|
||||
"url": state_guard.url
|
||||
});
|
||||
let script = format!("window._sctUpdateNavState({})", state_json);
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script)));
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(
|
||||
WebViewCommand::ExecuteScript(script),
|
||||
));
|
||||
return;
|
||||
}
|
||||
"cf_challenge_detected" => {
|
||||
// CF challenge was detected, skip next page load cycle (2 events: Started + Finished)
|
||||
cf_skip_count_ipc.store(2, Ordering::SeqCst);
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Forward other messages to Dart
|
||||
let _ = event_tx_clone.send(WebViewEvent::WebMessage { message: msg });
|
||||
})
|
||||
@ -286,10 +376,25 @@ fn run_webview_loop(
|
||||
let nav_history = Arc::clone(&nav_history);
|
||||
let proxy = proxy.clone();
|
||||
move |uri| {
|
||||
// Block navigation to about:blank (except initial load)
|
||||
// This prevents users from going back to the blank initial page
|
||||
if uri == "about:blank" {
|
||||
// Check if we already have navigation history
|
||||
// If yes, this is a back navigation to about:blank - block it
|
||||
let has_history = !nav_history.read().urls.is_empty();
|
||||
if has_history {
|
||||
// Navigate forward instead to stay on valid page
|
||||
let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(
|
||||
"history.forward()".to_string()
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Note: cf_skip_count is NOT reset here
|
||||
// The counter naturally decrements to 0 after skipping, allowing normal injection
|
||||
|
||||
let can_back;
|
||||
let can_forward;
|
||||
{
|
||||
@ -302,7 +407,7 @@ fn run_webview_loop(
|
||||
state_guard.can_go_back = can_back;
|
||||
state_guard.can_go_forward = can_forward;
|
||||
}
|
||||
|
||||
|
||||
// Send loading state to JS immediately when navigation starts
|
||||
// This makes the spinner appear on the current page before navigating away
|
||||
let state_json = serde_json::json!({
|
||||
@ -311,9 +416,12 @@ fn run_webview_loop(
|
||||
"is_loading": true,
|
||||
"url": uri
|
||||
});
|
||||
let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json);
|
||||
let script = format!(
|
||||
"window._sctUpdateNavState && window._sctUpdateNavState({})",
|
||||
state_json
|
||||
);
|
||||
let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script)));
|
||||
|
||||
|
||||
let _ = event_tx.send(WebViewEvent::NavigationStarted { url: uri });
|
||||
true
|
||||
}
|
||||
@ -322,12 +430,25 @@ fn run_webview_loop(
|
||||
let event_tx = event_tx.clone();
|
||||
let state = Arc::clone(&state);
|
||||
let nav_history = Arc::clone(&nav_history);
|
||||
let cf_skip_count = Arc::clone(&cf_skip_count);
|
||||
let proxy = proxy.clone();
|
||||
move |event, url| {
|
||||
if url == "about:blank" {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if we should skip injection/CF check (CF bypass active)
|
||||
// Counter is set to 2 when CF detected: decrements once in Started, once in Finished
|
||||
let should_skip = {
|
||||
let count = cf_skip_count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
cf_skip_count.fetch_sub(1, Ordering::SeqCst);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
PageLoadEvent::Started => {
|
||||
let can_back;
|
||||
@ -342,14 +463,32 @@ fn run_webview_loop(
|
||||
state_guard.can_go_back = can_back;
|
||||
state_guard.can_go_forward = can_forward;
|
||||
}
|
||||
let state_json = serde_json::json!({
|
||||
"can_go_back": can_back,
|
||||
"can_go_forward": can_forward,
|
||||
"is_loading": true,
|
||||
"url": url
|
||||
});
|
||||
let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json);
|
||||
let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script)));
|
||||
|
||||
// Inject init script at page start for fast navbar display
|
||||
// Skip injection if CF bypass counter is active
|
||||
if !should_skip {
|
||||
let state_json = serde_json::json!({
|
||||
"can_go_back": can_back,
|
||||
"can_go_forward": can_forward,
|
||||
"is_loading": true,
|
||||
"url": url
|
||||
});
|
||||
let init_script_with_state = format!(
|
||||
r#"(function() {{
|
||||
if (!window._sctInitialized) {{
|
||||
{init_script}
|
||||
}}
|
||||
if (window._sctUpdateNavState) {{
|
||||
window._sctUpdateNavState({state_json});
|
||||
}}
|
||||
}})();"#,
|
||||
init_script = INIT_SCRIPT,
|
||||
state_json = state_json
|
||||
);
|
||||
let _ = proxy.send_event(UserEvent::Command(
|
||||
WebViewCommand::ExecuteScript(init_script_with_state),
|
||||
));
|
||||
}
|
||||
}
|
||||
PageLoadEvent::Finished => {
|
||||
let can_back;
|
||||
@ -367,23 +506,110 @@ fn run_webview_loop(
|
||||
state_guard.can_go_back = can_back;
|
||||
state_guard.can_go_forward = can_forward;
|
||||
}
|
||||
let _ = event_tx.send(WebViewEvent::NavigationCompleted { url: url.clone() });
|
||||
|
||||
let _ =
|
||||
event_tx.send(WebViewEvent::NavigationCompleted { url: url.clone() });
|
||||
|
||||
// If CF bypass is active for this URL, skip CF check
|
||||
// Let the user complete the CF challenge without interference
|
||||
if should_skip {
|
||||
// Just update nav state if navbar somehow exists
|
||||
let state_json = serde_json::json!({
|
||||
"can_go_back": can_back,
|
||||
"can_go_forward": can_forward,
|
||||
"is_loading": false,
|
||||
"url": url
|
||||
});
|
||||
let script = format!(
|
||||
"window._sctUpdateNavState && window._sctUpdateNavState({})",
|
||||
state_json
|
||||
);
|
||||
let _ = proxy.send_event(UserEvent::Command(
|
||||
WebViewCommand::ExecuteScript(script),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for CF challenge and handle accordingly
|
||||
let state_json = serde_json::json!({
|
||||
"can_go_back": can_back,
|
||||
"can_go_forward": can_forward,
|
||||
"is_loading": false,
|
||||
"url": url
|
||||
});
|
||||
let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json);
|
||||
let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script)));
|
||||
|
||||
// Script to check CF challenge and notify Rust via IPC if detected
|
||||
let cf_check_script = format!(
|
||||
r#"(function() {{
|
||||
// Check if this is a Cloudflare challenge page
|
||||
var isCfChallenge = (function() {{
|
||||
// Check page title
|
||||
if (document.title && (
|
||||
document.title.indexOf('Just a moment') !== -1 ||
|
||||
document.title.indexOf('Checking your browser') !== -1 ||
|
||||
document.title.indexOf('Please wait') !== -1 ||
|
||||
document.title.indexOf('Attention Required') !== -1
|
||||
)) return true;
|
||||
// Check for CF challenge elements
|
||||
if (document.getElementById('challenge-running') ||
|
||||
document.getElementById('challenge-form') ||
|
||||
document.getElementById('cf-spinner-please-wait') ||
|
||||
document.getElementById('cf-stage') ||
|
||||
document.querySelector('.cf-browser-verification') ||
|
||||
document.querySelector('[data-ray]')) return true;
|
||||
// Check for CF challenge script
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
for (var i = 0; i < scripts.length; i++) {{
|
||||
if (scripts[i].src && scripts[i].src.indexOf('/cdn-cgi/challenge-platform/') !== -1) return true;
|
||||
}}
|
||||
return false;
|
||||
}})();
|
||||
|
||||
if (isCfChallenge) {{
|
||||
console.log('[SCToolbox] CF challenge detected, will reload without navbar');
|
||||
// Remove navbar if it was injected
|
||||
var navbar = document.getElementById('sct-navbar');
|
||||
if (navbar) navbar.remove();
|
||||
window._sctInitialized = false;
|
||||
// Notify Rust to skip init script on next load and reload
|
||||
if (window.ipc && typeof window.ipc.postMessage === 'function') {{
|
||||
window.ipc.postMessage(JSON.stringify({{ type: 'cf_challenge_detected' }}));
|
||||
}}
|
||||
}} else {{
|
||||
// Update nav state if navbar exists
|
||||
if (window._sctUpdateNavState) {{
|
||||
window._sctUpdateNavState({state_json});
|
||||
}}
|
||||
}}
|
||||
}})();"#,
|
||||
state_json = state_json
|
||||
);
|
||||
let _ = proxy.send_event(UserEvent::Command(
|
||||
WebViewCommand::ExecuteScript(cf_check_script),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_new_window_req_handler({
|
||||
let proxy = proxy.clone();
|
||||
move |uri, _features| {
|
||||
// Intercept new window requests (e.g., target="_blank" links)
|
||||
// Navigate in current window instead since we don't support multiple windows
|
||||
let _ = proxy.send_event(UserEvent::Command(WebViewCommand::Navigate(uri)));
|
||||
// Return Deny to prevent opening a new window
|
||||
NewWindowResponse::Deny
|
||||
}
|
||||
})
|
||||
.build(&window)
|
||||
.expect("Failed to create webview");
|
||||
|
||||
// Show loading page while waiting for Navigate command
|
||||
let loading_script = format!(
|
||||
r#"document.open(); document.write({}); document.close();"#,
|
||||
serde_json::to_string(LOADING_PAGE_HTML).unwrap_or_default()
|
||||
);
|
||||
let _ = webview.evaluate_script(&loading_script);
|
||||
|
||||
let webview = Arc::new(webview);
|
||||
let webview_cmd = Arc::clone(&webview);
|
||||
|
||||
@ -412,7 +638,16 @@ fn run_webview_loop(
|
||||
}
|
||||
Event::UserEvent(user_event) => match user_event {
|
||||
UserEvent::Command(cmd) => {
|
||||
handle_command(&webview_cmd, &window, cmd, &state, &nav_history, &event_tx, &is_closed, control_flow);
|
||||
handle_command(
|
||||
&webview_cmd,
|
||||
&window,
|
||||
cmd,
|
||||
&state,
|
||||
&nav_history,
|
||||
&event_tx,
|
||||
&is_closed,
|
||||
control_flow,
|
||||
);
|
||||
}
|
||||
UserEvent::Quit => {
|
||||
is_closed.store(true, Ordering::SeqCst);
|
||||
@ -422,7 +657,7 @@ fn run_webview_loop(
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Explicitly drop in correct order
|
||||
drop(webview_cmd);
|
||||
drop(web_context);
|
||||
@ -495,9 +730,9 @@ fn handle_command(
|
||||
|
||||
/// Load the application icon from embedded ICO data
|
||||
fn load_app_icon() -> Option<Icon> {
|
||||
use std::io::Cursor;
|
||||
use image::ImageReader;
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
let cursor = Cursor::new(APP_ICON_DATA);
|
||||
let reader = match ImageReader::new(cursor).with_guessed_format() {
|
||||
Ok(r) => r,
|
||||
@ -506,7 +741,7 @@ fn load_app_icon() -> Option<Icon> {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let img = match reader.decode() {
|
||||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
@ -514,11 +749,11 @@ fn load_app_icon() -> Option<Icon> {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let rgba = img.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
let raw_data = rgba.into_raw();
|
||||
|
||||
|
||||
match Icon::from_rgba(raw_data, width, height) {
|
||||
Ok(icon) => Some(icon),
|
||||
Err(e) => {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// SCToolbox WebView initialization script
|
||||
// Uses IPC (window.ipc.postMessage) to communicate with Rust backend
|
||||
(function() {
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
if (window._sctInitialized) return;
|
||||
window._sctInitialized = true;
|
||||
|
||||
|
||||
// ========== IPC Communication ==========
|
||||
// Send message to Rust backend
|
||||
function sendToRust(type, payload) {
|
||||
@ -13,14 +13,14 @@
|
||||
window.ipc.postMessage(JSON.stringify({ type, payload }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========== 导航栏 UI ==========
|
||||
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>',
|
||||
forward: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
reload: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C10.1012 2.5 11.9254 3.67022 12.8169 5.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M10.5 5.5H13V3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
||||
};
|
||||
|
||||
|
||||
// Global state from Rust
|
||||
window._sctNavState = {
|
||||
canGoBack: false,
|
||||
@ -28,7 +28,7 @@
|
||||
isLoading: true,
|
||||
url: window.location.href
|
||||
};
|
||||
|
||||
|
||||
function createNavBar() {
|
||||
if (window.location.href === 'about:blank') return;
|
||||
if (document.getElementById('sct-navbar')) return;
|
||||
@ -36,7 +36,7 @@
|
||||
setTimeout(createNavBar, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const nav = document.createElement('div');
|
||||
nav.id = 'sct-navbar';
|
||||
nav.innerHTML = `
|
||||
@ -137,7 +137,7 @@
|
||||
<input type="text" id="sct-navbar-url" readonly value="${window.location.href}" />
|
||||
`;
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
|
||||
|
||||
// Navigation buttons - send commands to Rust
|
||||
document.getElementById('sct-back').onclick = () => {
|
||||
sendToRust('nav_back', {});
|
||||
@ -148,14 +148,14 @@
|
||||
document.getElementById('sct-reload').onclick = () => {
|
||||
sendToRust('nav_reload', {});
|
||||
};
|
||||
|
||||
|
||||
// Apply initial state from Rust
|
||||
updateNavBarFromState();
|
||||
|
||||
|
||||
// Request initial state from Rust
|
||||
sendToRust('get_nav_state', {});
|
||||
}
|
||||
|
||||
|
||||
// Update navbar UI based on state from Rust
|
||||
function updateNavBarFromState() {
|
||||
const state = window._sctNavState;
|
||||
@ -164,7 +164,7 @@
|
||||
const urlEl = document.getElementById('sct-navbar-url');
|
||||
const spinner = document.getElementById('sct-spinner');
|
||||
const faviconSlot = document.getElementById('sct-favicon-slot');
|
||||
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.disabled = !state.canGoBack;
|
||||
}
|
||||
@ -174,7 +174,7 @@
|
||||
if (urlEl && state.url) {
|
||||
urlEl.value = state.url;
|
||||
}
|
||||
|
||||
|
||||
// Show spinner when loading, show favicon when complete
|
||||
if (state.isLoading) {
|
||||
if (spinner) {
|
||||
@ -195,23 +195,23 @@
|
||||
showFaviconIfAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract and show favicon from page
|
||||
function showFaviconIfAvailable() {
|
||||
const faviconSlot = document.getElementById('sct-favicon-slot');
|
||||
const faviconImg = document.getElementById('sct-favicon');
|
||||
|
||||
|
||||
if (!faviconSlot || !faviconImg) return;
|
||||
|
||||
|
||||
// Try to find favicon from page
|
||||
let faviconUrl = null;
|
||||
|
||||
|
||||
// 1. Look for link[rel="icon"] or link[rel="shortcut icon"]
|
||||
const linkIcon = document.querySelector('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]');
|
||||
if (linkIcon && linkIcon.href) {
|
||||
faviconUrl = linkIcon.href;
|
||||
}
|
||||
|
||||
|
||||
// 2. Look for og:image in meta tags (fallback)
|
||||
if (!faviconUrl) {
|
||||
const ogImage = document.querySelector('meta[property="og:image"]');
|
||||
@ -219,7 +219,7 @@
|
||||
faviconUrl = ogImage.content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. Try default favicon.ico
|
||||
if (!faviconUrl) {
|
||||
try {
|
||||
@ -231,7 +231,7 @@
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Display favicon if found
|
||||
if (faviconUrl) {
|
||||
faviconImg.src = faviconUrl;
|
||||
@ -245,10 +245,10 @@
|
||||
faviconSlot.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========== Rust -> JS Message Handler ==========
|
||||
// Rust will call this function to update navigation state
|
||||
window._sctUpdateNavState = function(state) {
|
||||
window._sctUpdateNavState = function (state) {
|
||||
if (state) {
|
||||
window._sctNavState = {
|
||||
canGoBack: !!state.can_go_back,
|
||||
@ -259,7 +259,7 @@
|
||||
updateNavBarFromState();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 在 DOM 准备好时创建导航栏
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createNavBar);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user