CH-J Tools透過 SSH 管理伺服器
選單
已發布的原始碼

CH-J Tools

瀏覽特定應用程式版本的目錄與檔案。

下載原始碼 ZIP
CH-J / Server Manager 1704 個檔案
0.1.2-beta 新增時間: · 建置時間:
windowChrome.ts 新增時間: 未記錄日期 6.5 KB · 116 行
程式碼 下載檔案
1/**
2 * Frameless windows on Windows/Linux. Electron 0.1.0 used `titleBarStyle: hidden` with a
3 * native `titleBarOverlay` (min/max/close drawn by Chromium in the top-right 40 px).
4 * Tauri has no overlay, so the same area gets three equivalent buttons here, and the
5 * existing drag regions keep working through a Core command.
6 */
7import { windowControls } from "../core/window";
8import { call, subscribe } from "../transport";
9import { bootInfo } from "./boot";
11const CSS = `
12chj-window-controls { position: fixed !important; top: 0 !important; right: 0 !important; height: 40px !important; z-index: 2147483647 !important; display: flex !important; }
13chj-window-controls button { width: 46px; height: 40px; border: 0; margin: 0; padding: 0; display: grid; place-items: center; color: #d9eaff; background: transparent; cursor: default; -webkit-app-region: no-drag; }
14chj-window-controls button:hover { background: rgba(217, 234, 255, .12); }
15chj-window-controls button.close:hover { background: #c42b1c; color: #fff; }
16chj-window-controls svg { width: 10px; height: 10px; stroke: currentColor; fill: none; stroke-width: 1.1; shape-rendering: crispEdges; }
17`;
19const ICONS: Record<string, string> = {
20 minimize: '<svg viewBox="0 0 10 10"><path d="M0 5.5h10"/></svg>',
21 toggleMaximize: '<svg viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9"/></svg>',
22 close: '<svg viewBox="0 0 10 10"><path d="M0 0l10 10M10 0L0 10"/></svg>'
23};
25function isDragTarget(target: EventTarget | null, selectors: string[]): boolean {
26 if (!(target instanceof Element)) return false;
27 if (target.closest("button, a, input, select, textarea, [contenteditable], chj-window-controls")) return false;
28 return selectors.some((selector) => Boolean(target.closest(selector)));
31let controlsEnabled = true;
32let controlsHost: HTMLElement | null = null;
34/** An embedded module has no window of its own: the controls and dragging return with a separate window. */
35export function setWindowControlsEnabled(enabled: boolean): void {
36 controlsEnabled = enabled;
37 if (!controlsHost) return;
38 if (enabled) { if (!controlsHost.isConnected) document.body.append(controlsHost); }
39 else controlsHost.remove();
42export function installWindowChrome(platform: string, dragSelectors: string[], dragRoots: () => Array<Document | ShadowRoot>): void {
43 // This handshake runs on WKWebView, WebView2 and WebKitGTK, including native macOS chrome.
44 window.addEventListener("chj:query-dirty", (event) => {
45 let dirty = true;
46 try {
47 const page = window as Window & { __CHJ_HAS_UNSAVED_CHANGES__?: () => boolean; isDirty?: () => boolean };
48 // NGINX uses a classic script with a global isDirty(), including disconnected drafts.
49 const query = page.__CHJ_HAS_UNSAVED_CHANGES__ || (bootInfo().moduleId === "chj.nginx-manager" ? page.isDirty : undefined);
50 dirty = query ? Boolean(query()) : false;
51 } catch { /* A failed dirty query preserves the draft. */ }
52 void call("window_control", "window:control", { action: "closeQueryReply", queryId: (event as CustomEvent<number>).detail, dirty }).catch(() => {});
53 });
54 if (platform !== "win32" && platform !== "linux") return;
55 const mount = () => {
56 if (document.querySelector("chj-window-controls")) return;
57 const host = document.createElement("chj-window-controls");
58 const shadow = host.attachShadow({ mode: "closed" });
59 const css = CSS.replace(/chj-window-controls /g, ":host ").replace(/chj-window-controls \{/g, ":host {");
60 // A constructed stylesheet is not inline, so module pages with a strict style-src keep their window controls.
61 if ("CSSStyleSheet" in globalThis && "replaceSync" in CSSStyleSheet.prototype) {
62 const sheet = new CSSStyleSheet();
63 sheet.replaceSync(css);
64 shadow.adoptedStyleSheets = [sheet];
65 } else {
66 const style = document.createElement("style");
67 style.textContent = css;
68 shadow.append(style);
69 }
70 let language = document.documentElement.lang || "en";
71 let maximized = false;
72 const buttons = new Map<string, HTMLButtonElement>();
73 const translate = () => {
74 const catalogs = globalThis.__CHJ_I18N__?.catalogs?.chrome || {};
75 for (const [action, button] of buttons) {
76 const key = `chrome.${action === "toggleMaximize" ? (maximized ? "restore" : "maximize") : action}`;
77 const label = catalogs[language]?.[key] || catalogs[language.split("-")[0]]?.[key] || catalogs.en?.[key] || key;
78 button.setAttribute("aria-label", label);
79 button.title = label;
80 if (action === "toggleMaximize") button.setAttribute("aria-pressed", String(maximized));
81 }
82 };
83 const updateMaximized = () => { void windowControls.isMaximized().then((value) => { maximized = value; translate(); }).catch(() => {}); };
84 subscribe<string>("module:ui:languageChanged", (value) => { language = value; translate(); });
85 new MutationObserver(() => { language = document.documentElement.lang || language; translate(); }).observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
86 window.addEventListener("resize", updateMaximized);
87 for (const action of ["minimize", "toggleMaximize", "close"] as const) {
88 const button = document.createElement("button");
89 button.type = "button";
90 button.className = action === "close" ? "close" : action;
91 buttons.set(action, button);
92 button.innerHTML = ICONS[action];
93 button.addEventListener("click", () => { void windowControls.perform(action).then(updateMaximized).catch(() => {}); });
94 shadow.append(button);
95 }
96 translate();
97 updateMaximized();
98 const outer = document.createElement("style");
99 outer.textContent = "chj-window-controls { position: fixed; top: 0; right: 0; height: 40px; z-index: 2147483647; display: flex; }";
100 document.head.append(outer);
101 controlsHost = host;
102 setWindowControlsEnabled(controlsEnabled);
103 const handler = (event: Event) => {
104 const mouse = event as MouseEvent;
105 if (mouse.button !== 0 || !controlsEnabled) return;
106 const path = mouse.composedPath();
107 const target = (path[0] as EventTarget) || mouse.target;
108 if (!isDragTarget(target, dragSelectors)) return;
109 if (mouse.detail === 2) void windowControls.perform("toggleMaximize").catch(() => {});
110 else void windowControls.perform("startDrag").catch(() => {});
111 };
112 for (const root of dragRoots()) root.addEventListener("mousedown", handler);
113 };
114 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount, { once: true });
115 else mount();
SHA-25647ede93033bf715e3aa50ba4788019311fef44efb561e72a146c4d8b04349d95

CH-J Proprietary Software License 1.14 · 原始碼依 CH-J Proprietary Software License 1.14 提供。公開原始碼不會改變授權條款,也不會授予額外的權利。

壓縮檔 SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
檔案