공개된 소스
소스 ZIP 다운로드
CH-J Tools
특정 애플리케이션 릴리스의 디렉터리와 파일을 살펴보세요.
CH-J / Server Manager
1704 개 파일
0.1.2-beta
추가됨: · 빌드됨:
1
//! Window factory: main, license and module windows with their initialization scripts2
//! (the scripts replace Electron preloads: they define `window.chjCore`, `window.licenseApi`3
//! or `window.chjModule`/`window.chjPlugin` before any page script runs).5
use crate::core::i18n::I18n;6
use crate::security::caller::{is_app_url, module_url_matches, LICENSE_LABEL, MAIN_LABEL, QUICK_LABEL};7
use crate::security::url_policy::is_allowed_external_url;8
use serde_json::{json, Value};9
use std::path::Path;10
use tauri::webview::{NewWindowResponse, PageLoadEvent};11
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder};13
pub const CORE_BRIDGE: &str = include_str!("../bridges/core-bridge.js");14
pub const MODULE_BRIDGE: &str = include_str!("../bridges/module-bridge.js");15
pub const LICENSE_BRIDGE: &str = include_str!("../bridges/license-bridge.js");16
pub const QUICK_BRIDGE: &str = include_str!("../bridges/quick-bridge.js");17
pub 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`, …).20
pub fn platform_name() -> &'static str {21
match std::env::consts::OS {22
"macos" => "darwin",23
"windows" => "win32",24
other => other,25
}26
}28
/// Frameless windows with in-page controls on Windows/Linux (Electron used `titleBarOverlay`).29
pub fn frameless() -> bool {30
matches!(platform_name(), "win32" | "linux")31
}33
pub 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)39
}41
#[cfg(feature = "smoke")]42
fn 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
}52
}54
#[cfg(not(feature = "smoke"))]55
fn smoke_extension(script: String) -> String {56
script57
}59
fn 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
}64
}66
/// Must be identical for every window, otherwise WebView2 refuses to share the browser process.67
fn 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()73
}75
/// WebKitGTK defaults to the caches of a web browser; the app has local pages only.76
fn 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;86
}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.90
fn 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();104
}106
pub 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
false124
})125
.on_new_window(move |url, _features| {126
open_external_if_allowed(&handle, &url);127
NewWindowResponse::Deny128
})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)157
}159
/// Automatic update check after the main window loaded (if enabled), like 0.1.0.160
fn 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
});175
}177
pub 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()201
}203
/// Small window of a system quick action (verify/sign one file). Native decorations, no external204
/// navigation; it only talks to the `quick_*` commands. An open quick window is reloaded for a new request.205
pub 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()231
}233
/// Shows the main window, or creates it when the process ran only for a quick action.234
pub 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(|_| ())242
}244
/// URL of a module page. Windows (WebView2) serves custom schemes as `http://<scheme>.localhost/`.245
pub 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")252
}254
pub struct ModuleWindowSpec<'a> {255
pub label: &'a str,256
pub module_id: &'a str,257
pub entry: &'a str,258
pub root: &'a Path,259
}261
/// A module runs as its own WebView inside the main window. It keeps its origin, label and permissions, and the262
/// same WebView can move to a separate window and back without reloading (`detach`, `attach` in ModuleService).263
pub 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)285
}287
/// The window that holds a detached module. The WebView moves into it with `Webview::reparent`.288
pub 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()299
}301
fn 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;311
}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 difference315
/// from the page's own height shows the title bar, which is also absent in full screen.316
pub 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.0325
}327
/// Places an embedded module over the module area of the main page (logical pixels).328
pub 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() });333
}335
/// Moves a module to the overlay of the main window; other systems position child WebViews natively.336
pub 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;341
}343
/// Prepares an embedded module for a separate window, where it fills the window again.344
pub 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;349
}351
/// Tauri packs child WebViews into a GtkBox, where they cannot be positioned. A GtkOverlay around the main page352
/// positions them on both Wayland and X11. All functions run on the GTK main thread.353
#[cfg(target_os = "linux")]354
mod 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
}396
}SHA-256
d96b91df3ccc1d8660a0a97876c706d24cd828cc1933dfd0b421d7a652214950CH-J Proprietary Software License 1.14 · 소스는 CH-J Proprietary Software License 1.14에 따라 제공됩니다. 소스를 공개한다고 해서 라이선스 조항이 바뀌거나 추가 권리가 부여되지는 않습니다.
압축 파일 SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f