mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 19:50:28 +00:00
feat: web nav update
This commit is contained in:
parent
6f0c760ab4
commit
b11603d68c
@ -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 登录脚本
|
||||
|
||||
@ -111,6 +111,82 @@ pub enum WebViewCommand {
|
||||
|
||||
type WebViewId = String;
|
||||
|
||||
/// Navigation history manager to track back/forward capability
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NavigationHistory {
|
||||
/// List of URLs in history (excluding about:blank)
|
||||
urls: Vec<String>,
|
||||
/// Current position in history (0-based index)
|
||||
current_index: i32,
|
||||
}
|
||||
|
||||
impl NavigationHistory {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
urls: Vec::new(),
|
||||
current_index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new URL to history (when navigating to a new page)
|
||||
fn push(&mut self, url: &str) {
|
||||
// Skip about:blank
|
||||
if url == "about:blank" {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're not at the end of history, truncate forward history
|
||||
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);
|
||||
}
|
||||
|
||||
// Don't add duplicate consecutive URLs
|
||||
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;
|
||||
}
|
||||
|
||||
/// Check if we can go back (not at first real page)
|
||||
fn can_go_back(&self) -> bool {
|
||||
self.current_index > 0
|
||||
}
|
||||
|
||||
/// Check if we can go forward
|
||||
fn can_go_forward(&self) -> bool {
|
||||
self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
/// Go back in history, returns true if successful
|
||||
fn go_back(&mut self) -> bool {
|
||||
if self.can_go_back() {
|
||||
self.current_index -= 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Go forward in history, returns true if successful
|
||||
fn go_forward(&mut self) -> bool {
|
||||
if self.can_go_forward() {
|
||||
self.current_index += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current URL
|
||||
fn current_url(&self) -> Option<&str> {
|
||||
if self.current_index >= 0 && (self.current_index as usize) < self.urls.len() {
|
||||
Some(&self.urls[self.current_index as usize])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebViewInstance {
|
||||
command_sender: Sender<WebViewCommand>,
|
||||
event_receiver: Receiver<WebViewEvent>,
|
||||
@ -333,7 +409,12 @@ fn run_webview_loop(
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
// Navigation history for tracking back/forward state
|
||||
let nav_history = Arc::new(RwLock::new(NavigationHistory::new()));
|
||||
|
||||
let event_tx_clone = event_tx.clone();
|
||||
let nav_history_ipc = Arc::clone(&nav_history);
|
||||
let state_ipc = Arc::clone(&state);
|
||||
|
||||
// Create web context with custom data directory if provided
|
||||
let mut web_context = config
|
||||
@ -354,20 +435,76 @@ fn run_webview_loop(
|
||||
builder = builder.with_user_agent(user_agent);
|
||||
}
|
||||
|
||||
// Store proxy for IPC commands
|
||||
let proxy_ipc = proxy.clone();
|
||||
|
||||
let webview = builder
|
||||
.with_ipc_handler(move |message| {
|
||||
let msg = message.body().to_string();
|
||||
// Forward all messages to Dart
|
||||
|
||||
// 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()) {
|
||||
match msg_type {
|
||||
"nav_back" => {
|
||||
// Check if we can go back (avoid about:blank)
|
||||
let can_back = nav_history_ipc.read().can_go_back();
|
||||
if can_back {
|
||||
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));
|
||||
}
|
||||
return;
|
||||
}
|
||||
"nav_reload" => {
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload));
|
||||
return;
|
||||
}
|
||||
"get_nav_state" => {
|
||||
// Send current state to JS
|
||||
let state_guard = state_ipc.read();
|
||||
let history = nav_history_ipc.read();
|
||||
let state_json = serde_json::json!({
|
||||
"can_go_back": history.can_go_back(),
|
||||
"can_go_forward": history.can_go_forward(),
|
||||
"is_loading": state_guard.is_loading,
|
||||
"url": state_guard.url
|
||||
});
|
||||
let script = format!("window._sctUpdateNavState({})", state_json);
|
||||
let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script)));
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward other messages to Dart
|
||||
let _ = event_tx_clone.send(WebViewEvent::WebMessage { message: msg });
|
||||
})
|
||||
.with_navigation_handler({
|
||||
let event_tx = event_tx.clone();
|
||||
let state = Arc::clone(&state);
|
||||
let nav_history = Arc::clone(&nav_history);
|
||||
move |uri| {
|
||||
// Skip about:blank for navigation events
|
||||
if uri == "about:blank" {
|
||||
return true;
|
||||
}
|
||||
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
state_guard.url = uri.clone();
|
||||
state_guard.is_loading = true;
|
||||
// Update can_go_back/can_go_forward from history
|
||||
let history = nav_history.read();
|
||||
state_guard.can_go_back = history.can_go_back();
|
||||
state_guard.can_go_forward = history.can_go_forward();
|
||||
}
|
||||
let _ = event_tx.send(WebViewEvent::NavigationStarted { url: uri });
|
||||
true // Allow navigation
|
||||
@ -376,20 +513,66 @@ fn run_webview_loop(
|
||||
.with_on_page_load_handler({
|
||||
let event_tx = event_tx.clone();
|
||||
let state = Arc::clone(&state);
|
||||
let nav_history = Arc::clone(&nav_history);
|
||||
let proxy = proxy.clone();
|
||||
move |event, url| {
|
||||
// Skip about:blank
|
||||
if url == "about:blank" {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
PageLoadEvent::Started => {
|
||||
let mut state_guard = state.write();
|
||||
state_guard.url = url.clone();
|
||||
state_guard.is_loading = true;
|
||||
let can_back;
|
||||
let can_forward;
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
state_guard.url = url.clone();
|
||||
state_guard.is_loading = true;
|
||||
let history = nav_history.read();
|
||||
can_back = history.can_go_back();
|
||||
can_forward = history.can_go_forward();
|
||||
state_guard.can_go_back = can_back;
|
||||
state_guard.can_go_forward = can_forward;
|
||||
}
|
||||
// Send loading state to JS
|
||||
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)));
|
||||
}
|
||||
PageLoadEvent::Finished => {
|
||||
let can_back;
|
||||
let can_forward;
|
||||
{
|
||||
// Add to history when page finishes loading
|
||||
let mut history = nav_history.write();
|
||||
history.push(&url);
|
||||
can_back = history.can_go_back();
|
||||
can_forward = history.can_go_forward();
|
||||
}
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
state_guard.url = url.clone();
|
||||
state_guard.is_loading = false;
|
||||
state_guard.can_go_back = can_back;
|
||||
state_guard.can_go_forward = can_forward;
|
||||
}
|
||||
let _ = event_tx.send(WebViewEvent::NavigationCompleted { url });
|
||||
let _ = event_tx.send(WebViewEvent::NavigationCompleted { url: url.clone() });
|
||||
|
||||
// Send completed state to JS
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -425,7 +608,7 @@ fn run_webview_loop(
|
||||
}
|
||||
Event::UserEvent(user_event) => match user_event {
|
||||
UserEvent::Command(cmd) => {
|
||||
handle_command(&webview_cmd, &window, cmd, &state, &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);
|
||||
@ -447,6 +630,7 @@ fn handle_command(
|
||||
window: &Arc<Window>,
|
||||
command: WebViewCommand,
|
||||
state: &Arc<RwLock<WebViewNavigationState>>,
|
||||
nav_history: &Arc<RwLock<NavigationHistory>>,
|
||||
event_tx: &Sender<WebViewEvent>,
|
||||
is_closed: &Arc<AtomicBool>,
|
||||
control_flow: &mut ControlFlow,
|
||||
@ -462,10 +646,24 @@ fn handle_command(
|
||||
let _ = event_tx.send(WebViewEvent::NavigationStarted { url });
|
||||
}
|
||||
WebViewCommand::GoBack => {
|
||||
let _ = webview.evaluate_script("history.back()");
|
||||
// Update history index before navigation
|
||||
let can_go = {
|
||||
let mut history = nav_history.write();
|
||||
history.go_back()
|
||||
};
|
||||
if can_go {
|
||||
let _ = webview.evaluate_script("history.back()");
|
||||
}
|
||||
}
|
||||
WebViewCommand::GoForward => {
|
||||
let _ = webview.evaluate_script("history.forward()");
|
||||
// Update history index before navigation
|
||||
let can_go = {
|
||||
let mut history = nav_history.write();
|
||||
history.go_forward()
|
||||
};
|
||||
if can_go {
|
||||
let _ = webview.evaluate_script("history.forward()");
|
||||
}
|
||||
}
|
||||
WebViewCommand::Reload => {
|
||||
let _ = webview.evaluate_script("location.reload()");
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
// SCToolbox WebView initialization script
|
||||
// Uses IPC (window.ipc.postMessage) to communicate with Rust backend
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
if (window._sctInitialized) return;
|
||||
window._sctInitialized = true;
|
||||
|
||||
// ========== IPC Communication ==========
|
||||
// Send message to Rust backend
|
||||
function sendToRust(type, payload) {
|
||||
if (window.ipc && typeof window.ipc.postMessage === 'function') {
|
||||
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>',
|
||||
@ -12,6 +21,14 @@
|
||||
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,
|
||||
canGoForward: false,
|
||||
isLoading: true,
|
||||
url: window.location.href
|
||||
};
|
||||
|
||||
function createNavBar() {
|
||||
if (window.location.href === 'about:blank') return;
|
||||
if (document.getElementById('sct-navbar')) return;
|
||||
@ -57,8 +74,8 @@
|
||||
transition: all 0.15s ease;
|
||||
padding: 0;
|
||||
}
|
||||
#sct-navbar button:hover { background: rgba(10, 49, 66, 0.9); color: #fff; }
|
||||
#sct-navbar button:active { background: rgba(10, 49, 66, 1); transform: scale(0.95); }
|
||||
#sct-navbar button:hover:not(:disabled) { background: rgba(10, 49, 66, 0.9); color: #fff; }
|
||||
#sct-navbar button:active:not(:disabled) { background: rgba(10, 49, 66, 1); transform: scale(0.95); }
|
||||
#sct-navbar button:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||
#sct-navbar button svg { display: block; }
|
||||
#sct-navbar-url {
|
||||
@ -112,8 +129,8 @@
|
||||
#sct-spinner { -webkit-animation: none; animation: none; }
|
||||
}
|
||||
</style>
|
||||
<button id="sct-back" title="Back">${icons.back}</button>
|
||||
<button id="sct-forward" title="Forward">${icons.forward}</button>
|
||||
<button id="sct-back" title="Back" disabled>${icons.back}</button>
|
||||
<button id="sct-forward" title="Forward" disabled>${icons.forward}</button>
|
||||
<button id="sct-reload" title="Reload">${icons.reload}</button>
|
||||
<div id="sct-spinner" role="progressbar" aria-hidden="false" aria-valuetext="Loading" title="Loading"></div>
|
||||
<div id="sct-favicon-slot"><img id="sct-favicon" src="" alt="Page icon" /></div>
|
||||
@ -121,51 +138,45 @@
|
||||
`;
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
|
||||
// Navigation buttons - send commands to Rust
|
||||
document.getElementById('sct-back').onclick = () => {
|
||||
// Check if going back would result in about:blank
|
||||
// If so, skip this entry and go back further
|
||||
const beforeBackUrl = window.location.href;
|
||||
history.back();
|
||||
|
||||
// After a short delay, if we landed on about:blank, go back again
|
||||
setTimeout(() => {
|
||||
if (window.location.href === 'about:blank' && beforeBackUrl !== 'about:blank') {
|
||||
history.back();
|
||||
}
|
||||
}, 100);
|
||||
sendToRust('nav_back', {});
|
||||
};
|
||||
document.getElementById('sct-forward').onclick = () => {
|
||||
sendToRust('nav_forward', {});
|
||||
};
|
||||
document.getElementById('sct-reload').onclick = () => {
|
||||
sendToRust('nav_reload', {});
|
||||
};
|
||||
document.getElementById('sct-forward').onclick = () => history.forward();
|
||||
document.getElementById('sct-reload').onclick = () => location.reload();
|
||||
|
||||
// Update back button state and URL display on navigation
|
||||
function updateNavBarState() {
|
||||
const backBtn = document.getElementById('sct-back');
|
||||
const urlEl = document.getElementById('sct-navbar-url');
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
if (backBtn) {
|
||||
// Disable back button if at start of history or at about:blank
|
||||
backBtn.disabled = window.history.length <= 1 || currentUrl === 'about:blank';
|
||||
}
|
||||
|
||||
if (urlEl) {
|
||||
urlEl.value = currentUrl;
|
||||
}
|
||||
}
|
||||
// Apply initial state from Rust
|
||||
updateNavBarFromState();
|
||||
|
||||
// Listen to popstate and hashchange to update nav bar
|
||||
window.addEventListener('popstate', updateNavBarState);
|
||||
window.addEventListener('hashchange', updateNavBarState);
|
||||
|
||||
// Initial state
|
||||
updateNavBarState();
|
||||
|
||||
// Spinner and favicon show/hide helpers
|
||||
// Request initial state from Rust
|
||||
sendToRust('get_nav_state', {});
|
||||
}
|
||||
|
||||
// Update navbar UI based on state from Rust
|
||||
function updateNavBarFromState() {
|
||||
const state = window._sctNavState;
|
||||
const backBtn = document.getElementById('sct-back');
|
||||
const forwardBtn = document.getElementById('sct-forward');
|
||||
const urlEl = document.getElementById('sct-navbar-url');
|
||||
const spinner = document.getElementById('sct-spinner');
|
||||
const faviconSlot = document.getElementById('sct-favicon-slot');
|
||||
const faviconImg = document.getElementById('sct-favicon');
|
||||
|
||||
function showSpinner() {
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.disabled = !state.canGoBack;
|
||||
}
|
||||
if (forwardBtn) {
|
||||
forwardBtn.disabled = !state.canGoForward;
|
||||
}
|
||||
if (urlEl && state.url) {
|
||||
urlEl.value = state.url;
|
||||
}
|
||||
|
||||
// Show spinner when loading, show favicon when complete
|
||||
if (state.isLoading) {
|
||||
if (spinner) {
|
||||
spinner.style.display = 'block';
|
||||
spinner.setAttribute('aria-hidden', 'false');
|
||||
@ -174,93 +185,85 @@
|
||||
if (faviconSlot) {
|
||||
faviconSlot.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function hideSpin() {
|
||||
} else {
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none';
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
spinner.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
// Show favicon when page is loaded
|
||||
showFaviconIfAvailable();
|
||||
}
|
||||
|
||||
// Extract favicon from page and show it when ready
|
||||
function showFaviconWhenReady() {
|
||||
hideSpin();
|
||||
|
||||
// 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"]');
|
||||
if (ogImage && ogImage.content) {
|
||||
faviconUrl = ogImage.content;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check page's existing favicon from document.head or body elements
|
||||
if (!faviconUrl) {
|
||||
const existingFavicon = document.querySelector('img[src*="favicon"], img[src*="icon"]');
|
||||
if (existingFavicon && existingFavicon.src) {
|
||||
faviconUrl = existingFavicon.src;
|
||||
}
|
||||
}
|
||||
|
||||
// Display favicon if found, otherwise hide slot
|
||||
if (faviconUrl) {
|
||||
if (faviconImg) {
|
||||
faviconImg.src = faviconUrl;
|
||||
faviconImg.onerror = () => {
|
||||
if (faviconSlot) faviconSlot.style.display = 'none';
|
||||
};
|
||||
}
|
||||
if (faviconSlot) {
|
||||
faviconSlot.style.display = 'flex';
|
||||
}
|
||||
} else if (faviconSlot) {
|
||||
faviconSlot.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor document readyState to show favicon when page is ready
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
setTimeout(showFaviconWhenReady, 150);
|
||||
}
|
||||
});
|
||||
|
||||
// Also trigger favicon display on load event
|
||||
window.addEventListener('load', function () {
|
||||
setTimeout(showFaviconWhenReady, 150);
|
||||
});
|
||||
}
|
||||
|
||||
// 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"]');
|
||||
if (ogImage && ogImage.content) {
|
||||
faviconUrl = ogImage.content;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Try default favicon.ico
|
||||
if (!faviconUrl) {
|
||||
try {
|
||||
const origin = window.location.origin;
|
||||
if (origin && origin !== 'null') {
|
||||
faviconUrl = origin + '/favicon.ico';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Display favicon if found
|
||||
if (faviconUrl) {
|
||||
faviconImg.src = faviconUrl;
|
||||
faviconImg.onerror = () => {
|
||||
faviconSlot.style.display = 'none';
|
||||
};
|
||||
faviconImg.onload = () => {
|
||||
faviconSlot.style.display = 'flex';
|
||||
};
|
||||
} else {
|
||||
faviconSlot.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rust -> JS Message Handler ==========
|
||||
// Rust will call this function to update navigation state
|
||||
window._sctUpdateNavState = function(state) {
|
||||
if (state) {
|
||||
window._sctNavState = {
|
||||
canGoBack: !!state.can_go_back,
|
||||
canGoForward: !!state.can_go_forward,
|
||||
isLoading: !!state.is_loading,
|
||||
url: state.url || window.location.href
|
||||
};
|
||||
updateNavBarFromState();
|
||||
}
|
||||
};
|
||||
|
||||
// 在 DOM 准备好时创建导航栏
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createNavBar);
|
||||
} else {
|
||||
createNavBar();
|
||||
}
|
||||
|
||||
// URL 变化时:进入加载状态,显示 spinner
|
||||
window.addEventListener('popstate', () => {
|
||||
// Show spinner when navigating via popstate (URL change)
|
||||
const spinner = document.getElementById('sct-spinner');
|
||||
const faviconSlot = document.getElementById('sct-favicon-slot');
|
||||
if (spinner) {
|
||||
spinner.style.display = 'block';
|
||||
spinner.setAttribute('aria-hidden', 'false');
|
||||
spinner.setAttribute('aria-busy', 'true');
|
||||
}
|
||||
if (faviconSlot) {
|
||||
faviconSlot.style.display = 'none';
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user