33e56fb56f
桌面壳移除未由 Rust 真实驱动的 network.statusChanged 能力 桌面网络状态保留 Rust 主站可达性主动查询 门禁禁止桌面网络事件回退到 WebView online 状态 文档和项目记忆同步桌面网络能力边界
193 lines
7.7 KiB
Rust
193 lines
7.7 KiB
Rust
use crate::host_bridge::{DesktopShareState, HostBridgeReplayState};
|
|
use crate::shell::deep_link::{
|
|
register_desktop_deep_link_events, register_desktop_deep_link_schemes,
|
|
};
|
|
use crate::shell::menu::register_desktop_app_menu;
|
|
use crate::shell::tray::{
|
|
register_desktop_tray, register_desktop_window_close_events,
|
|
resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction,
|
|
};
|
|
use crate::shell::webview::{
|
|
desktop_window_config_with_runtime_platform, emit_current_desktop_lifecycle_event,
|
|
desktop_external_navigation_url, log_desktop_host_event_result,
|
|
open_desktop_external_navigation,
|
|
open_normalized_desktop_external_url, register_desktop_file_drop_events,
|
|
register_desktop_lifecycle_events, register_desktop_navigation_events,
|
|
replay_desktop_webview_state,
|
|
should_allow_desktop_webview_download, should_allow_desktop_webview_navigation,
|
|
should_replay_desktop_webview_state_on_page_load,
|
|
};
|
|
use crate::shell::window_state::desktop_window_state_plugin;
|
|
use tauri::webview::NewWindowResponse;
|
|
|
|
const DESKTOP_MAIN_WINDOW_LABEL: &str = "main";
|
|
|
|
fn desktop_main_window_config_from_windows(
|
|
windows: &[tauri::utils::config::WindowConfig],
|
|
) -> tauri::Result<tauri::utils::config::WindowConfig> {
|
|
windows
|
|
.iter()
|
|
.find(|window| window.label == DESKTOP_MAIN_WINDOW_LABEL)
|
|
.cloned()
|
|
.map(desktop_window_config_with_runtime_platform)
|
|
.ok_or(tauri::Error::WindowNotFound)
|
|
}
|
|
|
|
fn desktop_main_window_config(
|
|
app: &tauri::App,
|
|
) -> tauri::Result<tauri::utils::config::WindowConfig> {
|
|
desktop_main_window_config_from_windows(&app.config().app.windows)
|
|
}
|
|
|
|
fn desktop_new_window_external_url(url: &tauri::Url) -> Option<String> {
|
|
desktop_external_navigation_url(url)
|
|
}
|
|
|
|
fn desktop_new_window_response<R: tauri::Runtime>() -> NewWindowResponse<R> {
|
|
NewWindowResponse::Deny
|
|
}
|
|
|
|
pub(crate) fn run() {
|
|
tauri::Builder::default()
|
|
.manage(DesktopShareState::default())
|
|
.manage(HostBridgeReplayState::default())
|
|
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
|
match resolve_desktop_single_instance_action() {
|
|
DesktopSingleInstanceAction::ShowMainWindow => {
|
|
log_desktop_host_event_result("single_instance.show", show_main_window(app));
|
|
}
|
|
}
|
|
}))
|
|
.plugin(desktop_window_state_plugin())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.setup(|app| {
|
|
register_desktop_app_menu(app)?;
|
|
let tray_registered = match register_desktop_tray(app) {
|
|
Ok(()) => true,
|
|
Err(error) => {
|
|
eprintln!("desktop tray registration failed: {error}");
|
|
false
|
|
}
|
|
};
|
|
let config = desktop_main_window_config(app)?;
|
|
let app_handle = app.handle().clone();
|
|
let new_window_app_handle = app.handle().clone();
|
|
let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?
|
|
.on_navigation(move |url| {
|
|
if should_allow_desktop_webview_navigation(url) {
|
|
true
|
|
} else {
|
|
open_desktop_external_navigation(&app_handle, url);
|
|
false
|
|
}
|
|
})
|
|
.on_new_window(move |url, _features| {
|
|
if let Some(external_url) = desktop_new_window_external_url(&url) {
|
|
log_desktop_host_event_result(
|
|
"webview.new_window.external",
|
|
open_normalized_desktop_external_url(
|
|
&new_window_app_handle,
|
|
external_url,
|
|
),
|
|
);
|
|
}
|
|
desktop_new_window_response::<tauri::Wry>()
|
|
})
|
|
.on_page_load(|window, payload| {
|
|
if should_replay_desktop_webview_state_on_page_load(payload.event()) {
|
|
replay_desktop_webview_state(&window);
|
|
}
|
|
})
|
|
.on_download(|_webview, event| should_allow_desktop_webview_download(&event))
|
|
.build()?;
|
|
register_desktop_window_close_events(&window, tray_registered);
|
|
register_desktop_lifecycle_events(&window);
|
|
log_desktop_host_event_result(
|
|
"app.lifecycle",
|
|
emit_current_desktop_lifecycle_event(&window),
|
|
);
|
|
register_desktop_navigation_events(&window)?;
|
|
register_desktop_file_drop_events(&window);
|
|
register_desktop_deep_link_events(app)?;
|
|
register_desktop_deep_link_schemes(app);
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
crate::host_bridge::host_bridge_request
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("failed to run Genarrative desktop shell");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn desktop_main_window_config_uses_labeled_main_window() {
|
|
let mut secondary_window = tauri::utils::config::WindowConfig::default();
|
|
secondary_window.label = "secondary".to_string();
|
|
secondary_window.url = tauri::WebviewUrl::App(PathBuf::from("secondary.html"));
|
|
|
|
let mut main_window = tauri::utils::config::WindowConfig::default();
|
|
main_window.label = "main".to_string();
|
|
main_window.url = tauri::WebviewUrl::App(PathBuf::from("index.html"));
|
|
|
|
let config = desktop_main_window_config_from_windows(&[secondary_window, main_window])
|
|
.expect("main window config");
|
|
|
|
assert_eq!(config.label, "main");
|
|
match config.url {
|
|
tauri::WebviewUrl::App(path) => {
|
|
let path = path.to_string_lossy();
|
|
assert!(path.starts_with("index.html?"));
|
|
assert!(path.contains("clientRuntime=native_app"));
|
|
assert!(path.contains("hostShell=tauri_desktop"));
|
|
}
|
|
other => panic!("unexpected main window url {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_main_window_config_rejects_missing_main_window() {
|
|
let mut secondary_window = tauri::utils::config::WindowConfig::default();
|
|
secondary_window.label = "secondary".to_string();
|
|
|
|
let error = desktop_main_window_config_from_windows(&[secondary_window])
|
|
.expect_err("missing main window should fail startup");
|
|
|
|
assert!(matches!(error, tauri::Error::WindowNotFound));
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_new_window_policy_denies_embedded_windows() {
|
|
assert!(matches!(
|
|
desktop_new_window_response::<tauri::Wry>(),
|
|
NewWindowResponse::Deny
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_new_window_policy_opens_only_safe_external_urls() {
|
|
let external_url = tauri::Url::parse("https://example.com/share")
|
|
.expect("external desktop new window url");
|
|
assert_eq!(
|
|
desktop_new_window_external_url(&external_url),
|
|
Some("https://example.com/share".to_string())
|
|
);
|
|
|
|
let same_origin_url = tauri::Url::parse("https://app.genarrative.world/works/detail")
|
|
.expect("same-origin desktop new window url");
|
|
assert_eq!(desktop_new_window_external_url(&same_origin_url), None);
|
|
|
|
let unsafe_url =
|
|
tauri::Url::parse("javascript:alert(1)").expect("unsafe desktop new window url");
|
|
assert_eq!(desktop_new_window_external_url(&unsafe_url), None);
|
|
}
|
|
}
|