@nsnanocat/preference-panes 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -116
- package/dist/module/app.mjs +1170 -0
- package/dist/module/index.html +14 -0
- package/dist/module/navigation.mjs +160 -0
- package/dist/preference-panes.config.js +839 -813
- package/dist/preference-panes.mjs +1052 -825
- package/dist/preference-panes.proxy.js +1751 -2123
- package/package.json +66 -67
- package/src/BoxJS.mjs +86 -0
- package/src/Store.mjs +127 -0
- package/src/browser/Navigation.d.mts +43 -0
- package/src/browser/Navigation.mjs +158 -0
- package/src/browser/app.mjs +30 -148
- package/src/browser/client.d.mts +150 -0
- package/src/browser/client.mjs +191 -212
- package/src/browser/components.mjs +59 -0
- package/src/browser/index.d.ts +19 -152
- package/src/browser/index.mjs +60 -5
- package/src/browser/module.html +14 -0
- package/src/browser/panel.css +245 -223
- package/src/browser/panel.mjs +414 -514
- package/src/build.mjs +33 -0
- package/src/index.d.ts +227 -133
- package/src/index.mjs +3 -6
- package/src/lib/boxjs.mjs +77 -99
- package/src/lib/page-inputs.mjs +17 -0
- package/src/lib/response.mjs +16 -0
- package/src/lib/settings-path.mjs +10 -23
- package/src/proxy/config.mjs +6 -11
- package/src/proxy/handler.mjs +48 -27
- package/src/proxy/response.mjs +16 -0
- package/dist/preference-panes.request.js +0 -2126
- package/dist/settings/app.mjs +0 -1044
- package/dist/settings/home.css +0 -134
- package/dist/settings/index.html +0 -15
- package/dist/settings/panel.css +0 -325
- package/src/PreferencesHandler.mjs +0 -50
- package/src/SettingsHandler.mjs +0 -144
- package/src/browser/home.css +0 -134
- package/src/browser/site.html +0 -15
- package/src/proxy/request.mjs +0 -5
package/src/browser/app.mjs
CHANGED
|
@@ -1,154 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { BoxJS } from "../BoxJS.mjs";
|
|
2
|
+
import { pageInputs } from "../lib/page-inputs.mjs";
|
|
3
|
+
import { errorView } from "./components.mjs";
|
|
4
|
+
import { mount } from "./index.mjs";
|
|
4
5
|
|
|
6
|
+
let view;
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
8
|
+
* 从 URL 或代理传递的 Header 导入 JSON/CSS,支持独立文档与 srcdoc。
|
|
9
|
+
* Import JSON/CSS from the URL or proxy-carried headers in standalone and srcdoc documents.
|
|
10
|
+
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
9
11
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (text !== undefined) element.textContent = text;
|
|
31
|
-
return element;
|
|
12
|
+
async function start() {
|
|
13
|
+
try {
|
|
14
|
+
view?.destroy();
|
|
15
|
+
view = undefined;
|
|
16
|
+
const context = document.querySelector('meta[name="preference-panes-inputs"]');
|
|
17
|
+
const inputs = context ? JSON.parse(decodeURIComponent(context.content)) : pageInputs(new URL(location.href));
|
|
18
|
+
const resources = [inputs.json, inputs.css].map(source => {
|
|
19
|
+
if (!source) return null;
|
|
20
|
+
const url = new URL(source, inputs.url);
|
|
21
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Resources must use HTTP(S) URLs");
|
|
22
|
+
return url.href;
|
|
23
|
+
});
|
|
24
|
+
const [data, style] = await Promise.all(resources.map(url => (url ? fetch(url, { cache: "no-store", credentials: "omit" }) : null)));
|
|
25
|
+
if (data.status !== 200 || (style && style.status !== 200)) throw new Error(`HTTP ${data.status !== 200 ? data.status : style.status}`);
|
|
26
|
+
const boxjs = await data.json();
|
|
27
|
+
if (new BoxJS(boxjs).module.module !== inputs.module) throw new Error("Imported JSON does not match the module URL");
|
|
28
|
+
view = mount(boxjs, style ? await style.text() : "");
|
|
29
|
+
} catch (error) {
|
|
30
|
+
document.querySelector("#preferences").replaceChildren(errorView(error, start));
|
|
31
|
+
}
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 按站点 JSON 显示图标;iconDark 是明确的暗色扩展,不改变 BoxJS icons 语义。
|
|
36
|
-
* Display configured icons; iconDark is an explicit dark variant without changing BoxJS icons semantics.
|
|
37
|
-
* @param {{icon?:string,iconDark?:string,icons?:string[]}} metadata 图标数据 / Icon data.
|
|
38
|
-
* @param {string} className 样式 / CSS class.
|
|
39
|
-
* @returns {HTMLPictureElement} 图片元素 / Picture element.
|
|
40
|
-
*/
|
|
41
|
-
function icon(metadata, className) {
|
|
42
|
-
const picture = node("picture", className),
|
|
43
|
-
image = node("img", "");
|
|
44
|
-
if (metadata.iconDark) {
|
|
45
|
-
const source = node("source", "");
|
|
46
|
-
source.media = theme ? (theme === "2" ? "all" : "not all") : "(prefers-color-scheme: dark)";
|
|
47
|
-
source.srcset = resourceURL(metadata.iconDark);
|
|
48
|
-
picture.append(source);
|
|
49
|
-
}
|
|
50
|
-
const selected = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
51
|
-
if (selected) image.src = resourceURL(selected);
|
|
52
|
-
image.alt = "";
|
|
53
|
-
picture.append(image);
|
|
54
|
-
return picture;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 校验配置中的资源 URL。
|
|
59
|
-
* Validate resource URLs in site configuration.
|
|
60
|
-
* @param {string} value 地址 / Address.
|
|
61
|
-
* @returns {string} 完整 URL / Absolute URL.
|
|
62
|
-
*/
|
|
63
|
-
function resourceURL(value) {
|
|
64
|
-
const url = new URL(value, location.href);
|
|
65
|
-
if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Resources must use HTTP(S)");
|
|
66
|
-
return url.href;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 挂载通用设置或站点菜单,每次进入菜单都重新 HEAD 探测。
|
|
71
|
-
* Mount settings or the site menu, repeating HEAD probes on every menu entry.
|
|
72
|
-
* @returns {Promise<void>} 挂载完成 / Mount completion.
|
|
73
|
-
*/
|
|
74
|
-
async function render() {
|
|
75
|
-
routedPath = location.pathname;
|
|
76
|
-
const version = ++revision;
|
|
77
|
-
panel?.destroy();
|
|
78
|
-
panel = undefined;
|
|
79
|
-
root.replaceChildren();
|
|
80
|
-
if (routedPath !== "/settings/") {
|
|
81
|
-
document.title = routedPath.split("/")[2] || "Preferences";
|
|
82
|
-
panel = mountPreferencePanes({ element: root });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
if (!menu) {
|
|
87
|
-
const response = await fetch("/settings/assets/site.boxjs.json", { cache: "no-store", credentials: "omit" });
|
|
88
|
-
if (response.status !== 200) throw new Error(`Site config HTTP ${response.status}`);
|
|
89
|
-
const data = await response.json();
|
|
90
|
-
if (typeof data.name !== "string" || !Array.isArray(data.apps)) throw new TypeError("Invalid site BoxJS JSON");
|
|
91
|
-
for (const app of data.apps) validatePathParts([app.module]);
|
|
92
|
-
const localStyles = document.head.querySelector('link[rel="stylesheet"]');
|
|
93
|
-
for (const href of data.stylesheets ?? []) {
|
|
94
|
-
const link = document.createElement("link");
|
|
95
|
-
link.rel = "stylesheet";
|
|
96
|
-
link.href = resourceURL(href);
|
|
97
|
-
document.head.insertBefore(link, localStyles);
|
|
98
|
-
}
|
|
99
|
-
menu = data;
|
|
100
|
-
}
|
|
101
|
-
if (version !== revision) return;
|
|
102
|
-
document.title = menu.name;
|
|
103
|
-
const home = node("section", "pp-home");
|
|
104
|
-
home.append(icon(menu, "brand-logo"), node("h1", "", menu.name));
|
|
105
|
-
const section = node("section", "self-panel is-zh");
|
|
106
|
-
section.append(node("h2", "header", menu.sectionTitle ?? "模块"));
|
|
107
|
-
const container = node("div", "container"),
|
|
108
|
-
scrollView = node("div", "scroll-view"),
|
|
109
|
-
rows = node("div", "scroll");
|
|
110
|
-
scrollView.append(rows);
|
|
111
|
-
container.append(scrollView);
|
|
112
|
-
section.append(container);
|
|
113
|
-
home.append(section);
|
|
114
|
-
if (menu.desc) home.append(node("p", "settings-note", menu.desc));
|
|
115
|
-
root.append(home);
|
|
116
|
-
for (const app of menu.apps) {
|
|
117
|
-
const button = node("button", "self-item is-zh"),
|
|
118
|
-
status = node("span", "module-status", "检测中");
|
|
119
|
-
button.type = "button";
|
|
120
|
-
button.disabled = true;
|
|
121
|
-
button.dataset.module = app.module;
|
|
122
|
-
button.append(icon(app, "logo"), node("span", "name", app.name ?? app.module), status);
|
|
123
|
-
rows.append(button);
|
|
124
|
-
button.onclick = () => {
|
|
125
|
-
history.pushState(null, "", `/settings/${app.module}`);
|
|
126
|
-
render();
|
|
127
|
-
if (!matchMedia("(prefers-reduced-motion: reduce)").matches) root.animate([{ transform: "translateX(100%)" }, { transform: "translateX(0)" }], { duration: 260, easing: "ease-out" });
|
|
128
|
-
};
|
|
129
|
-
client.probe(app.module).then(available => {
|
|
130
|
-
if (revision !== version) return;
|
|
131
|
-
button.disabled = !available;
|
|
132
|
-
status.textContent = available ? "" : "未响应";
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} catch (error) {
|
|
136
|
-
if (version === revision) {
|
|
137
|
-
const retry = node("button", "", "重新读取");
|
|
138
|
-
retry.type = "button";
|
|
139
|
-
retry.onclick = render;
|
|
140
|
-
root.replaceChildren(node("p", "pp-site-error", `加载失败:${error.message}`), retry);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
window.addEventListener("popstate", () => {
|
|
146
|
-
if (routedPath !== location.pathname) {
|
|
147
|
-
render();
|
|
148
|
-
if (location.pathname === "/settings/" && !matchMedia("(prefers-reduced-motion: reduce)").matches) root.animate([{ transform: "translateX(-100%)" }, { transform: "translateX(0)" }], { duration: 260, easing: "ease-out" });
|
|
149
|
-
}
|
|
150
|
-
});
|
|
33
|
+
start();
|
|
151
34
|
window.addEventListener("pageshow", event => {
|
|
152
|
-
|
|
35
|
+
if (event.persisted) start();
|
|
153
36
|
});
|
|
154
|
-
render();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ModuleDefinition, SettingsScalar } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* 单键写入或删除的通知事件,携带对应模块与点分键路径。
|
|
4
|
+
* Notification for a single-key write or delete, including module and dotted key path.
|
|
5
|
+
*/
|
|
6
|
+
export interface Notification {
|
|
7
|
+
/**
|
|
8
|
+
* 结果类别
|
|
9
|
+
* Result kind.
|
|
10
|
+
*/
|
|
11
|
+
kind: "success" | "error";
|
|
12
|
+
/**
|
|
13
|
+
* 操作类别
|
|
14
|
+
* Operation kind.
|
|
15
|
+
*/
|
|
16
|
+
operation: "write" | "delete" | "clearCaches" | "reset";
|
|
17
|
+
/**
|
|
18
|
+
* 模块标识
|
|
19
|
+
* Module identifier.
|
|
20
|
+
*/
|
|
21
|
+
module: string;
|
|
22
|
+
/**
|
|
23
|
+
* 含模块名、不含存储根的点分路径
|
|
24
|
+
* Dotted path including the module but excluding the storage root.
|
|
25
|
+
*/
|
|
26
|
+
key: string;
|
|
27
|
+
/**
|
|
28
|
+
* 失败原因,仅错误事件提供
|
|
29
|
+
* Failure reason, provided for errors only.
|
|
30
|
+
*/
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 浏览器会话客户端选项。
|
|
35
|
+
* Options for the browser session client.
|
|
36
|
+
*/
|
|
37
|
+
export interface PreferencesClientOptions {
|
|
38
|
+
/**
|
|
39
|
+
* 包内从 BoxJS 推导的目录。
|
|
40
|
+
* Internal catalog derived from BoxJS.
|
|
41
|
+
*/
|
|
42
|
+
catalog: { modules: ReadonlyMap<string, { storageKey: string }>; select(module: string): unknown };
|
|
43
|
+
/**
|
|
44
|
+
* 默认使用浏览器 fetch,可注入同签名传输
|
|
45
|
+
* Defaults to browser fetch; an equivalent transport may be supplied.
|
|
46
|
+
*/
|
|
47
|
+
fetch?: typeof globalThis.fetch;
|
|
48
|
+
/**
|
|
49
|
+
* 成功写入或失败时调用,不用于读取事件
|
|
50
|
+
* Called for successful mutations or failures, not reads.
|
|
51
|
+
*/
|
|
52
|
+
notify?: (notification: Notification) => void;
|
|
53
|
+
/**
|
|
54
|
+
* 单次请求超时,单位毫秒,默认 10000
|
|
55
|
+
* Per-request timeout in milliseconds; defaults to 10000.
|
|
56
|
+
*/
|
|
57
|
+
timeout?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 会话的深拷贝快照,调用方修改不会影响缓存。
|
|
61
|
+
* Deep-cloned session snapshot; caller changes cannot alter the cache.
|
|
62
|
+
*/
|
|
63
|
+
export interface ModuleSnapshot {
|
|
64
|
+
/**
|
|
65
|
+
* 当前配置生成的模块定义
|
|
66
|
+
* Module definition generated from current config.
|
|
67
|
+
*/
|
|
68
|
+
definition: ModuleDefinition;
|
|
69
|
+
/**
|
|
70
|
+
* 点分键到显示值的映射,已包含适用的默认值;持久化 null 原样保留。
|
|
71
|
+
* Dotted keys mapped to display values including applicable defaults; persisted null is preserved.
|
|
72
|
+
*/
|
|
73
|
+
values: Record<string, SettingsScalar | SettingsScalar[] | null>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 只在页面存活期间维护模块缓存的通用客户端。
|
|
77
|
+
* Generic client maintaining module caches only during the page lifetime.
|
|
78
|
+
*/
|
|
79
|
+
export interface PreferencesClient {
|
|
80
|
+
/**
|
|
81
|
+
* 使用已导入的 JSON 替换会话,只读取一次设置值。
|
|
82
|
+
* Replace the session from imported JSON and read stored settings once.
|
|
83
|
+
* @param module 模块标识 / Module identifier.
|
|
84
|
+
* @returns 新会话的独立快照 / Independent snapshot of the new session.
|
|
85
|
+
* @throws {Error} 写入进行中、请求或配置无效、会话被替换 / Active write, invalid request or config, or replaced session.
|
|
86
|
+
*/
|
|
87
|
+
open(module: string): Promise<ModuleSnapshot>;
|
|
88
|
+
/**
|
|
89
|
+
* 获取已打开模块的快照,不发请求。
|
|
90
|
+
* Get a snapshot of an open module without network requests.
|
|
91
|
+
* @param module 模块标识 / Module identifier.
|
|
92
|
+
* @returns 深拷贝快照 / Deep-cloned snapshot.
|
|
93
|
+
* @throws {Error} 模块尚未打开 / Module has not been opened.
|
|
94
|
+
*/
|
|
95
|
+
snapshot(module: string): ModuleSnapshot;
|
|
96
|
+
/**
|
|
97
|
+
* 取消未完成的读取并移除缓存,不撤销已发送的写入。
|
|
98
|
+
* Abort pending reads and discard the cache without undoing dispatched writes.
|
|
99
|
+
* @param module 模块标识 / Module identifier.
|
|
100
|
+
* @returns 无返回值 / No return value.
|
|
101
|
+
*/
|
|
102
|
+
leave(module: string): void;
|
|
103
|
+
/**
|
|
104
|
+
* POST 单个字段,HTTP 200 后更新缓存,不追加 GET。
|
|
105
|
+
* POST one field and update its cache only on HTTP 200, without a follow-up GET.
|
|
106
|
+
* @param module 已打开的模块 / Open module.
|
|
107
|
+
* @param key 完整点分字段路径 / Complete dotted field path.
|
|
108
|
+
* @param value 符合字段类型和选项的值 / Value matching the field type and choices.
|
|
109
|
+
* @returns 操作完成 / Completion of the operation.
|
|
110
|
+
* @throws {Error} 会话、值、并发写入或网络错误 / Session, value, concurrent-write or network error.
|
|
111
|
+
*/
|
|
112
|
+
set(module: string, key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* DELETE 单个覆盖值,HTTP 200 后显示默认值,不追加 GET。
|
|
115
|
+
* DELETE an override and display its default after HTTP 200, without a follow-up GET.
|
|
116
|
+
* @param module 已打开的模块 / Open module.
|
|
117
|
+
* @param key 完整点分字段路径 / Complete dotted field path.
|
|
118
|
+
* @returns 操作完成 / Completion of the operation.
|
|
119
|
+
* @throws {Error} 会话、路径、并发写入或网络错误 / Session, path, concurrent-write or network error.
|
|
120
|
+
*/
|
|
121
|
+
remove(module: string, key: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* 按需读取整个模块 Caches,不刷新设置。
|
|
124
|
+
* Read all module Caches on demand without refreshing settings.
|
|
125
|
+
* @param module 已打开模块 / Open module.
|
|
126
|
+
* @returns 缓存 JSON 值,缺失时为 undefined / Cache JSON value, or undefined when absent.
|
|
127
|
+
*/
|
|
128
|
+
readCaches(module: string): Promise<unknown>;
|
|
129
|
+
/**
|
|
130
|
+
* 删除模块 Caches 并更新相关页面状态,不追加 GET。
|
|
131
|
+
* Delete module Caches and update related page state without a follow-up GET.
|
|
132
|
+
* @param module 已打开模块 / Open module.
|
|
133
|
+
* @returns 清理完成 / Cleanup completion.
|
|
134
|
+
*/
|
|
135
|
+
clearCaches(module: string): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* 删除整个模块持久化数据,页面使用当前 BoxJS 默认值。
|
|
138
|
+
* Delete all module persistence and use current BoxJS defaults on the page.
|
|
139
|
+
* @param module 已打开模块 / Open module.
|
|
140
|
+
* @returns 重置完成 / Reset completion.
|
|
141
|
+
*/
|
|
142
|
+
reset(module: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 创建包内客户端,接管请求和会话。
|
|
146
|
+
* Create an internal client for requests and sessions.
|
|
147
|
+
* @param options 包内目录与运行环境 / Internal catalog and runtime environment.
|
|
148
|
+
* @returns 会话客户端 / Session client.
|
|
149
|
+
*/
|
|
150
|
+
export function createPreferencesClient(options: PreferencesClientOptions): PreferencesClient;
|