//! Window factory: main, license and module windows with their initialization scripts //! (the scripts replace Electron preloads: they define `window.chjCore`, `window.licenseApi` //! or `window.chjModule`/`window.chjPlugin` before any page script runs). use crate::core::i18n::I18n; use crate::security::caller::{is_app_url, module_url_matches, LICENSE_LABEL, MAIN_LABEL}; use crate::security::url_policy::is_allowed_external_url; use serde_json::{json, Value}; use std::path::Path; use tauri::webview::{NewWindowResponse, PageLoadEvent}; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; pub const CORE_BRIDGE: &str = include_str!("../bridges/core-bridge.js"); pub const MODULE_BRIDGE: &str = include_str!("../bridges/module-bridge.js"); pub const LICENSE_BRIDGE: &str = include_str!("../bridges/license-bridge.js"); pub const BACKGROUND: tauri::window::Color = tauri::window::Color(0x07, 0x11, 0x1f, 0xff); /// Node-style platform name used by the 0.1.0 UI (`platform-darwin`, …). pub fn platform_name() -> &'static str { match std::env::consts::OS { "macos" => "darwin", "windows" => "win32", other => other, } } /// Frameless windows with in-page controls on Windows/Linux (Electron used `titleBarOverlay`). pub fn frameless() -> bool { matches!(platform_name(), "win32" | "linux") } pub fn init_script(window: &str, module_id: Option<&str>, i18n: Value, bridge: &str) -> String { let boot = json!({ "platform": platform_name(), "window": window, "moduleId": module_id }); let script = format!( "(function(){{var d=function(n,v){{Object.defineProperty(globalThis,n,{{value:v,writable:false,configurable:false,enumerable:false}});}};d('__CHJ_BOOT__',Object.freeze({boot}));d('__CHJ_I18N__',{i18n});}})();\n{bridge}" ); smoke_extension(script) } #[cfg(feature = "smoke")] fn smoke_extension(script: String) -> String { if let Ok(dir) = std::env::var("CHJ_SMOKE_DUMP") { let _ = std::fs::write(std::path::Path::new(&dir).join(format!("init-{}.js", script.len())), &script); } match std::env::var("CHJ_SMOKE_SCRIPT").ok().and_then(|path| std::fs::read_to_string(path).ok()) { Some(scenario) => format!( "try {{\n{script}\n}} catch (e) {{ window.__TAURI_INTERNALS__.invoke('smoke_report', {{ payload: {{ step: 'bridge', failure: String(e && e.stack || e) }} }}); }}\n;(function(){{\n{scenario}\n}})();" ), None => script, } } #[cfg(not(feature = "smoke"))] fn smoke_extension(script: String) -> String { script } fn open_external_if_allowed(app: &AppHandle, url: &url::Url) { if is_allowed_external_url(url.as_str()) { use tauri_plugin_opener::OpenerExt; let _ = app.opener().open_url(url.as_str(), None::<&str>); } } pub fn create_main_window(app: &AppHandle, i18n: &I18n) -> tauri::Result { let handle = app.clone(); let handle_nav = app.clone(); let mut builder = WebviewWindowBuilder::new(app, MAIN_LABEL, WebviewUrl::App("index.html".into())) .title("CH-J Server Manager") .inner_size(1240.0, 780.0) .min_inner_size(960.0, 640.0) .background_color(BACKGROUND) .visible(false) .initialization_script(init_script(MAIN_LABEL, None, i18n.main_window_data(), CORE_BRIDGE)) .disable_drag_drop_handler() .on_navigation(move |url| { if is_app_url(url) { return true; } open_external_if_allowed(&handle_nav, url); false }) .on_new_window(move |url, _features| { open_external_if_allowed(&handle, &url); NewWindowResponse::Deny }) .on_page_load(|window, payload| { if cfg!(debug_assertions) { eprintln!("[page] {} {:?} {}", window.label(), payload.event(), payload.url()); } if payload.event() == PageLoadEvent::Finished { let _ = window.show(); let _ = window.set_focus(); auto_update_check(&window); } }); if frameless() { builder = builder.decorations(false).shadow(true); } builder.build() } /// Automatic update check after the main window loaded (if enabled), like 0.1.0. fn auto_update_check(window: &WebviewWindow) { let Some(core) = window.app_handle().state::().try_get() else { return }; if core.config.get()["updates"]["autoCheck"] != true { return; } #[cfg(feature = "smoke")] if std::env::var("CHJ_SMOKE_NO_NETWORK").is_ok() { return; } tauri::async_runtime::spawn(async move { match core.services.updates.check().await { Ok(state) => core.emit_main("core:updateState", state), Err(error) => core.logger.warn("Automatic update check failed.", serde_json::json!({ "message": error.message })), } }); } pub fn create_license_window(app: &AppHandle, i18n: &I18n, language: &str) -> tauri::Result { let mut data = i18n.license_window_data(); data["initialLanguage"] = Value::String(language.to_string()); WebviewWindowBuilder::new(app, LICENSE_LABEL, WebviewUrl::App("license.html".into())) .title("CH-J Server Manager — License") .inner_size(900.0, 760.0) .min_inner_size(640.0, 520.0) .background_color(BACKGROUND) .visible(false) .initialization_script(init_script(LICENSE_LABEL, None, data, LICENSE_BRIDGE)) .disable_drag_drop_handler() .on_navigation(|url| is_app_url(url)) .on_new_window(|_, _| NewWindowResponse::Deny) .on_page_load(|window, payload| { if payload.event() == PageLoadEvent::Finished { let _ = window.show(); let _ = window.set_focus(); } }) .build() } /// URL of a module page. Windows (WebView2) serves custom schemes as `http://.localhost/`. pub fn module_url(module_id: &str, entry: &str) -> url::Url { let text = if cfg!(windows) { format!("http://chj-module.localhost/{module_id}/{entry}") } else { format!("chj-module://{module_id}/{entry}") }; url::Url::parse(&text).expect("valid module URL") } pub struct ModuleWindowSpec<'a> { pub label: &'a str, pub module_id: &'a str, pub name: &'a str, pub entry: &'a str, pub root: &'a Path, pub data_directory: std::path::PathBuf, } pub fn create_module_window(app: &AppHandle, i18n: &I18n, spec: ModuleWindowSpec<'_>) -> tauri::Result { let id_for_nav = spec.module_id.to_string(); let mut builder = WebviewWindowBuilder::new(app, spec.label, WebviewUrl::CustomProtocol(module_url(spec.module_id, spec.entry))) .title(format!("{} · CH-J Server Manager", spec.name)) .inner_size(980.0, 700.0) .min_inner_size(760.0, 520.0) .background_color(BACKGROUND) .visible(false) .incognito(true) .data_directory(spec.data_directory) .initialization_script(init_script(spec.label, Some(spec.module_id), i18n.module_window_data(spec.root), MODULE_BRIDGE)) .disable_drag_drop_handler() .on_navigation(move |url| module_url_matches(url, &id_for_nav)) .on_new_window(|_, _| NewWindowResponse::Deny) .on_page_load(|window, payload| { if payload.event() == PageLoadEvent::Finished { if let Some(core) = window.app_handle().state::().try_get() { core.services.modules.on_loaded(&core, window.label()); } } }); if let Some(main) = app.get_webview_window(MAIN_LABEL) { builder = builder.parent(&main)?; } if frameless() { builder = builder.decorations(false).shadow(true); } builder.build() }