CH-J Toolspengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J Tools

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / Server Manager 1704 fail
0.1.2-beta Ditambah: · Dibina:
windows.rs Ditambah: Tarikh tidak direkodkan 16.7 KB · 396 baris
Kod Muat turun fail
1//! Window factory: main, license and module windows with their initialization scripts
2//! (the scripts replace Electron preloads: they define `window.chjCore`, `window.licenseApi`
3//! or `window.chjModule`/`window.chjPlugin` before any page script runs).
5use crate::core::i18n::I18n;
6use crate::security::caller::{is_app_url, module_url_matches, LICENSE_LABEL, MAIN_LABEL, QUICK_LABEL};
7use crate::security::url_policy::is_allowed_external_url;
8use serde_json::{json, Value};
9use std::path::Path;
10use tauri::webview::{NewWindowResponse, PageLoadEvent};
11use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
13pub const CORE_BRIDGE: &str = include_str!("../bridges/core-bridge.js");
14pub const MODULE_BRIDGE: &str = include_str!("../bridges/module-bridge.js");
15pub const LICENSE_BRIDGE: &str = include_str!("../bridges/license-bridge.js");
16pub const QUICK_BRIDGE: &str = include_str!("../bridges/quick-bridge.js");
17pub const BACKGROUND: tauri::window::Color = tauri::window::Color(0x07, 0x11, 0x1f, 0xff);
19/// Node-style platform name used by the 0.1.0 UI (`platform-darwin`, …).
20pub fn platform_name() -> &'static str {
21 match std::env::consts::OS {
22 "macos" => "darwin",
23 "windows" => "win32",
24 other => other,
25 }
28/// Frameless windows with in-page controls on Windows/Linux (Electron used `titleBarOverlay`).
29pub fn frameless() -> bool {
30 matches!(platform_name(), "win32" | "linux")
33pub fn init_script(window: &str, module_id: Option<&str>, i18n: Value, bridge: &str) -> String {
34 let boot = json!({ "platform": platform_name(), "window": window, "moduleId": module_id });
35 let script = format!(
36 "(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}"
37 );
38 smoke_extension(script)
41#[cfg(feature = "smoke")]
42fn smoke_extension(script: String) -> String {
43 if let Ok(dir) = std::env::var("CHJ_SMOKE_DUMP") {
44 let _ = std::fs::write(std::path::Path::new(&dir).join(format!("init-{}.js", script.len())), &script);
45 }
46 match std::env::var("CHJ_SMOKE_SCRIPT").ok().and_then(|path| std::fs::read_to_string(path).ok()) {
47 Some(scenario) => format!(
48 "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}})();"
49 ),
50 None => script,
51 }
54#[cfg(not(feature = "smoke"))]
55fn smoke_extension(script: String) -> String {
56 script
59fn open_external_if_allowed(app: &AppHandle, url: &url::Url) {
60 if is_allowed_external_url(url.as_str()) {
61 use tauri_plugin_opener::OpenerExt;
62 let _ = app.opener().open_url(url.as_str(), None::<&str>);
63 }
66/// Must be identical for every window, otherwise WebView2 refuses to share the browser process.
67fn browser_args() -> String {
68 #[cfg(feature = "smoke")]
69 if let Ok(args) = std::env::var("CHJ_WEBVIEW_ARGS") {
70 return args;
71 }
72 "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-background-networking --disable-component-update --disable-extensions --disable-sync".to_string()
75/// WebKitGTK defaults to the caches of a web browser; the app has local pages only.
76fn reduce_web_caches(window: &WebviewWindow) {
77 #[cfg(target_os = "linux")]
78 let _ = window.with_webview(|webview| {
79 use webkit2gtk::{CacheModel, WebContextExt, WebViewExt};
80 if let Some(context) = webview.inner().web_context() {
81 context.set_cache_model(CacheModel::DocumentViewer);
82 }
83 });
84 #[cfg(not(target_os = "linux"))]
85 let _ = window;
88/// A window larger than the work area of its screen (e.g. 1280×800 with the Windows taskbar) is shrunk to fit,
89/// so its bottom buttons are never hidden, and centered. Called once before a window is first shown.
90fn fit_to_work_area(window: &WebviewWindow) {
91 let Ok(Some(monitor)) = window.current_monitor() else { return };
92 let (Ok(outer), Ok(inner)) = (window.outer_size(), window.inner_size()) else { return };
93 let area = monitor.work_area().size;
94 let margin = (16.0 * monitor.scale_factor()).round() as u32;
95 let (max_width, max_height) = (area.width.saturating_sub(margin), area.height.saturating_sub(margin));
96 if outer.width <= max_width && outer.height <= max_height {
97 return;
98 }
99 let (frame_width, frame_height) = (outer.width.saturating_sub(inner.width), outer.height.saturating_sub(inner.height));
100 let width = inner.width.min(max_width.saturating_sub(frame_width));
101 let height = inner.height.min(max_height.saturating_sub(frame_height));
102 let _ = window.set_size(tauri::PhysicalSize::new(width, height));
103 let _ = window.center();
106pub fn create_main_window(app: &AppHandle, i18n: &I18n, language: &str) -> tauri::Result<WebviewWindow> {
107 let handle = app.clone();
108 let handle_nav = app.clone();
109 let mut builder = WebviewWindowBuilder::new(app, MAIN_LABEL, WebviewUrl::App("index.html".into()))
110 .additional_browser_args(&browser_args())
111 .title("CH-J Tools")
112 .inner_size(1240.0, 780.0)
113 .min_inner_size(960.0, 640.0)
114 .background_color(BACKGROUND)
115 .visible(false)
116 .initialization_script(init_script(MAIN_LABEL, None, i18n.main_window_data(language), CORE_BRIDGE))
117 .disable_drag_drop_handler()
118 .on_navigation(move |url| {
119 if is_app_url(url) {
120 return true;
121 }
122 open_external_if_allowed(&handle_nav, url);
123 false
124 })
125 .on_new_window(move |url, _features| {
126 open_external_if_allowed(&handle, &url);
127 NewWindowResponse::Deny
128 })
129 .on_page_load(|window, payload| {
130 if cfg!(debug_assertions) {
131 eprintln!("[page] {} {:?} {}", window.label(), payload.event(), payload.url());
132 }
133 if payload.event() == PageLoadEvent::Finished {
134 if !window.is_visible().unwrap_or(true) {
135 fit_to_work_area(&window);
136 }
137 let _ = window.show();
138 let _ = window.set_focus();
139 auto_update_check(&window);
140 }
141 });
142 if frameless() {
143 builder = builder.decorations(false).shadow(true);
144 }
145 let window = builder.build()?;
146 reduce_web_caches(&window);
147 #[cfg(target_os = "linux")]
148 {
149 let page = window.clone();
150 let _ = window.run_on_main_thread(move || {
151 if let Ok(container) = page.default_vbox() {
152 linux_overlay::install(container);
153 }
154 });
155 }
156 Ok(window)
159/// Automatic update check after the main window loaded (if enabled), like 0.1.0.
160fn auto_update_check(window: &WebviewWindow) {
161 let Some(core) = window.app_handle().state::<crate::core::app::CoreSlot>().try_get() else { return };
162 if core.config.get()["updates"]["autoCheck"] != true {
163 return;
164 }
165 #[cfg(feature = "smoke")]
166 if std::env::var("CHJ_SMOKE_NO_NETWORK").is_ok() {
167 return;
168 }
169 tauri::async_runtime::spawn(async move {
170 match core.services.updates.check().await {
171 Ok(state) => core.emit_main("core:updateState", state),
172 Err(error) => core.logger.warn("Automatic update check failed.", serde_json::json!({ "message": error.message })),
173 }
174 });
177pub fn create_license_window(app: &AppHandle, i18n: &I18n, language: &str) -> tauri::Result<WebviewWindow> {
178 let mut data = i18n.license_window_data();
179 data["initialLanguage"] = Value::String(language.to_string());
180 WebviewWindowBuilder::new(app, LICENSE_LABEL, WebviewUrl::App("license.html".into()))
181 .additional_browser_args(&browser_args())
182 .title("CH-J Tools — License")
183 .inner_size(900.0, 760.0)
184 .min_inner_size(640.0, 520.0)
185 .background_color(BACKGROUND)
186 .visible(false)
187 .initialization_script(init_script(LICENSE_LABEL, None, data, LICENSE_BRIDGE))
188 .disable_drag_drop_handler()
189 .on_navigation(|url| is_app_url(url))
190 .on_new_window(|_, _| NewWindowResponse::Deny)
191 .on_page_load(|window, payload| {
192 if payload.event() == PageLoadEvent::Finished {
193 if !window.is_visible().unwrap_or(true) {
194 fit_to_work_area(&window);
195 }
196 let _ = window.show();
197 let _ = window.set_focus();
198 }
199 })
200 .build()
203/// Small window of a system quick action (verify/sign one file). Native decorations, no external
204/// navigation; it only talks to the `quick_*` commands. An open quick window is reloaded for a new request.
205pub fn open_quick_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
206 if let Some(window) = app.get_webview_window(QUICK_LABEL) {
207 let _ = window.eval("location.reload()");
208 let _ = window.unminimize();
209 let _ = window.show();
210 let _ = window.set_focus();
211 return Ok(window);
212 }
213 WebviewWindowBuilder::new(app, QUICK_LABEL, WebviewUrl::App("quick.html".into()))
214 .additional_browser_args(&browser_args())
215 .title("CH-J Tools")
216 .inner_size(520.0, 680.0)
217 .min_inner_size(440.0, 420.0)
218 .background_color(BACKGROUND)
219 .visible(false)
220 .initialization_script(init_script(QUICK_LABEL, None, json!({}), QUICK_BRIDGE))
221 .disable_drag_drop_handler()
222 .on_navigation(|url| is_app_url(url))
223 .on_new_window(|_, _| NewWindowResponse::Deny)
224 .on_page_load(|window, payload| {
225 if payload.event() == PageLoadEvent::Finished {
226 let _ = window.show();
227 let _ = window.set_focus();
228 }
229 })
230 .build()
233/// Shows the main window, or creates it when the process ran only for a quick action.
234pub fn show_or_create_main(app: &AppHandle, i18n: &I18n, language: &str) -> tauri::Result<()> {
235 if let Some(window) = app.get_window(MAIN_LABEL) {
236 let _ = window.unminimize();
237 let _ = window.show();
238 let _ = window.set_focus();
239 return Ok(());
240 }
241 create_main_window(app, i18n, language).map(|_| ())
244/// URL of a module page. Windows (WebView2) serves custom schemes as `http://<scheme>.localhost/`.
245pub fn module_url(module_id: &str, entry: &str) -> url::Url {
246 let text = if cfg!(windows) {
247 format!("http://chj-module.{module_id}.localhost/{entry}")
248 } else {
249 format!("chj-module://{module_id}/{entry}")
250 };
251 url::Url::parse(&text).expect("valid module URL")
254pub struct ModuleWindowSpec<'a> {
255 pub label: &'a str,
256 pub module_id: &'a str,
257 pub entry: &'a str,
258 pub root: &'a Path,
261/// A module runs as its own WebView inside the main window. It keeps its origin, label and permissions, and the
262/// same WebView can move to a separate window and back without reloading (`detach`, `attach` in ModuleService).
263pub fn create_module_webview(app: &AppHandle, i18n: &I18n, spec: ModuleWindowSpec<'_>) -> tauri::Result<tauri::Webview> {
264 let id_for_nav = spec.module_id.to_string();
265 let builder = tauri::webview::WebviewBuilder::new(spec.label, WebviewUrl::CustomProtocol(module_url(spec.module_id, spec.entry)))
266 .background_color(BACKGROUND)
267 .additional_browser_args(&browser_args())
268 .initialization_script(init_script(spec.label, Some(spec.module_id), i18n.module_window_data(spec.root), MODULE_BRIDGE))
269 .disable_drag_drop_handler()
270 .on_navigation(move |url| module_url_matches(url, &id_for_nav))
271 .on_new_window(|_, _| NewWindowResponse::Deny)
272 .on_page_load(|webview, payload| {
273 if payload.event() == PageLoadEvent::Finished {
274 if let Some(core) = webview.app_handle().state::<crate::core::app::CoreSlot>().try_get() {
275 core.services.modules.on_loaded(&core, webview.label());
276 }
277 }
278 });
279 let main = app.get_window(MAIN_LABEL).ok_or(tauri::Error::WindowNotFound)?;
280 let webview = main.add_child(builder, tauri::LogicalPosition::new(0.0, 0.0), tauri::LogicalSize::new(10.0, 10.0))?;
281 reduce_webview_caches(&webview);
282 webview.hide()?;
283 embed_in_main(&webview);
284 Ok(webview)
287/// The window that holds a detached module. The WebView moves into it with `Webview::reparent`.
288pub fn create_detached_window(app: &AppHandle, label: &str, name: &str) -> tauri::Result<tauri::Window> {
289 let mut builder = tauri::window::WindowBuilder::new(app, label)
290 .title(format!("{name} · CH-J Tools"))
291 .inner_size(980.0, 700.0)
292 .min_inner_size(760.0, 520.0)
293 .background_color(BACKGROUND)
294 .visible(false);
295 if frameless() {
296 builder = builder.decorations(false).shadow(true);
297 }
298 builder.build()
301fn reduce_webview_caches(webview: &tauri::Webview) {
302 #[cfg(target_os = "linux")]
303 let _ = webview.with_webview(|platform| {
304 use webkit2gtk::{CacheModel, WebContextExt, WebViewExt};
305 if let Some(context) = platform.inner().web_context() {
306 context.set_cache_model(CacheModel::DocumentViewer);
307 }
308 });
309 #[cfg(not(target_os = "linux"))]
310 let _ = webview;
313/// How far the main page starts below the origin that child WebViews are positioned from (logical pixels).
314/// On macOS the content view also covers the title bar, and WebKit keeps the page below it: only the difference
315/// from the page's own height shows the title bar, which is also absent in full screen.
316pub fn page_top(app: &AppHandle, page_height: f64) -> f64 {
317 #[cfg(target_os = "macos")]
318 if let Some(main) = app.get_window(MAIN_LABEL) {
319 if let (Ok(frame), Ok(scale)) = (main.outer_size(), main.scale_factor()) {
320 return (f64::from(frame.height) / scale - page_height).max(0.0);
321 }
322 }
323 let _ = (app, page_height);
324 0.0
327/// Places an embedded module over the module area of the main page (logical pixels).
328pub fn place_embedded(webview: &tauri::Webview, x: f64, y: f64, width: f64, height: f64) {
329 #[cfg(target_os = "linux")]
330 let _ = webview.with_webview(move |platform| linux_overlay::place(&platform.inner(), x, y, width, height));
331 #[cfg(not(target_os = "linux"))]
332 let _ = webview.set_bounds(tauri::Rect { position: tauri::LogicalPosition::new(x, y).into(), size: tauri::LogicalSize::new(width, height).into() });
335/// Moves a module to the overlay of the main window; other systems position child WebViews natively.
336pub fn embed_in_main(webview: &tauri::Webview) {
337 #[cfg(target_os = "linux")]
338 let _ = webview.with_webview(|platform| linux_overlay::adopt(&platform.inner()));
339 #[cfg(not(target_os = "linux"))]
340 let _ = webview;
343/// Prepares an embedded module for a separate window, where it fills the window again.
344pub fn release_from_main(webview: &tauri::Webview) {
345 #[cfg(target_os = "linux")]
346 let _ = webview.with_webview(|platform| linux_overlay::release(&platform.inner()));
347 #[cfg(not(target_os = "linux"))]
348 let _ = webview;
351/// Tauri packs child WebViews into a GtkBox, where they cannot be positioned. A GtkOverlay around the main page
352/// positions them on both Wayland and X11. All functions run on the GTK main thread.
353#[cfg(target_os = "linux")]
354mod linux_overlay {
355 use gtk::prelude::*;
356 use std::cell::RefCell;
358 thread_local! { static OVERLAY: RefCell<Option<gtk::Overlay>> = const { RefCell::new(None) }; }
360 pub fn install(container: gtk::Box) {
361 let Some(page) = container.children().into_iter().next() else { return };
362 let overlay = gtk::Overlay::new();
363 container.remove(&page);
364 overlay.add(&page);
365 container.pack_start(&overlay, true, true, 0);
366 overlay.show();
367 OVERLAY.with(|slot| *slot.borrow_mut() = Some(overlay));
368 }
370 pub fn adopt(view: &impl IsA<gtk::Widget>) {
371 OVERLAY.with(|slot| {
372 let slot = slot.borrow();
373 let Some(overlay) = slot.as_ref() else { return };
374 if let Some(parent) = view.parent().and_then(|p| p.downcast::<gtk::Container>().ok()) {
375 parent.remove(view);
376 }
377 view.set_halign(gtk::Align::Start);
378 view.set_valign(gtk::Align::Start);
379 overlay.add_overlay(view);
380 });
381 }
383 pub fn release(view: &impl IsA<gtk::Widget>) {
384 view.set_halign(gtk::Align::Fill);
385 view.set_valign(gtk::Align::Fill);
386 view.set_margin_start(0);
387 view.set_margin_top(0);
388 view.set_size_request(-1, -1);
389 }
391 pub fn place(view: &impl IsA<gtk::Widget>, x: f64, y: f64, width: f64, height: f64) {
392 view.set_margin_start(x.max(0.0).round() as i32);
393 view.set_margin_top(y.max(0.0).round() as i32);
394 view.set_size_request(width.max(1.0).round() as i32, height.max(1.0).round() as i32);
395 }
SHA-256d96b91df3ccc1d8660a0a97876c706d24cd828cc1933dfd0b421d7a652214950

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkibac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Fail