@nsnanocat/preference-panes 0.9.16 → 1.1.0
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 +24 -16
- package/dist/api.js +1454 -967
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +1519 -0
- package/dist/module/navigation.mjs +7 -6
- package/dist/preference-panes.mjs +861 -849
- package/dist/web.js +1202 -0
- package/package.json +1 -1
- package/src/api.mjs +191 -0
- package/src/browser/ModuleStatus.mjs +7 -6
- package/src/browser/Navigation.d.mts +8 -6
- package/src/{lib → browser}/boxjs.mjs +72 -16
- package/src/browser/client.d.mts +43 -133
- package/src/browser/client.mjs +146 -186
- package/src/browser/index.d.ts +21 -10
- package/src/browser/index.mjs +95 -72
- package/src/browser/module.html +1 -1
- package/src/browser/mount.mjs +119 -0
- package/src/browser/panel.mjs +470 -441
- package/src/build.mjs +4 -5
- package/src/index.d.ts +16 -2
- package/src/web.mjs +52 -0
- package/dist/module/app.mjs +0 -1461
- package/src/BoxJS.mjs +0 -72
- package/src/Store.mjs +0 -114
- package/src/browser/app.mjs +0 -51
- package/src/lib/response.mjs +0 -16
- package/src/proxy/handler.mjs +0 -39
- package/src/proxy/response.mjs +0 -16
package/src/browser/client.mjs
CHANGED
|
@@ -1,219 +1,179 @@
|
|
|
1
|
-
import { normalizeBoxJs, normalizeStoredValue, validValue } from "../lib/boxjs.mjs";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @typedef {object} ModuleSession
|
|
7
|
-
* @property {AbortController} controller 读取请求的取消控制器 / Abort controller for reads.
|
|
8
|
-
* @property {import("../index.js").ModuleDefinition | null} definition 加载完成的配置,加载中为 null / Loaded configuration, or null while loading.
|
|
9
|
-
* @property {import("./client.mjs").ModuleSnapshot["values"]} values 当前显示值 / Current display values.
|
|
10
|
-
* @property {boolean} saving 是否正在写入 / Whether a mutation is in progress.
|
|
2
|
+
* 管理单模块页面的 API 请求、值快照和会话终止。
|
|
3
|
+
* Manage API requests, value snapshots, and session termination for one module page.
|
|
11
4
|
*/
|
|
5
|
+
export class PreferencesClient {
|
|
6
|
+
#module;
|
|
7
|
+
#configURL;
|
|
8
|
+
#definition;
|
|
9
|
+
#request;
|
|
10
|
+
#notify;
|
|
11
|
+
#timeout;
|
|
12
|
+
#session = new AbortController();
|
|
13
|
+
#values;
|
|
14
|
+
#saving = false;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 创建只调用模块 API、不读取或解析 BoxJS 的页面客户端。
|
|
18
|
+
* Create a page client that only calls the module API and never reads or parses BoxJS.
|
|
19
|
+
* @param {import("./client.mjs").PreferencesClientOptions} options API 模型、请求与通知 / API model, requests, and notifications.
|
|
20
|
+
*/
|
|
21
|
+
constructor({ model, definition, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
|
|
22
|
+
this.#module = model.module;
|
|
23
|
+
this.#configURL = model.configURL;
|
|
24
|
+
this.#definition = definition;
|
|
25
|
+
this.#request = request;
|
|
26
|
+
this.#notify = notify;
|
|
27
|
+
this.#timeout = timeout;
|
|
28
|
+
this.#values = structuredClone(model.values);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取当前字段定义和值的深拷贝,不发起网络请求。
|
|
33
|
+
* Return a deep copy of the current field definition and values without a network request.
|
|
34
|
+
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
35
|
+
*/
|
|
36
|
+
snapshot() {
|
|
37
|
+
return structuredClone({ definition: this.#definition, values: this.#values });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 读取 Settings 子树。
|
|
42
|
+
* Read the Settings subtree.
|
|
43
|
+
* @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
|
|
44
|
+
*/
|
|
45
|
+
async readSettings() {
|
|
46
|
+
const response = await this.#send("get", { scope: "settings" });
|
|
47
|
+
return response.status === 404 ? undefined : response.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 读取 Caches 子树。
|
|
52
|
+
* Read the Caches subtree.
|
|
53
|
+
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
54
|
+
*/
|
|
55
|
+
async readCaches() {
|
|
56
|
+
const response = await this.#send("get", { scope: "caches" });
|
|
57
|
+
return response.status === 404 ? undefined : response.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 删除当前模块的 Caches 子树。
|
|
62
|
+
* Delete the current module Caches subtree.
|
|
63
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
64
|
+
*/
|
|
65
|
+
clearCaches() {
|
|
66
|
+
return this.#change("delete", { scope: "caches" }, "clearCaches");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 删除当前模块数据并恢复页面默认值。
|
|
71
|
+
* Delete current module data and restore page defaults.
|
|
72
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
73
|
+
*/
|
|
74
|
+
reset() {
|
|
75
|
+
return this.#change("delete", { scope: "module" }, "reset");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 终止当前页面仍在进行的请求。
|
|
80
|
+
* Abort requests still owned by the current page.
|
|
81
|
+
* @returns {void} 无返回值 / No return value.
|
|
82
|
+
*/
|
|
83
|
+
leave() {
|
|
84
|
+
this.#session.abort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 写入单个字段。
|
|
89
|
+
* Write one field.
|
|
90
|
+
* @param {string} key 字段路径 / Field path.
|
|
91
|
+
* @param {unknown} value 已校验值 / Validated value.
|
|
92
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
93
|
+
*/
|
|
94
|
+
set(key, value) {
|
|
95
|
+
return this.#change("set", { key, value }, "write", key);
|
|
96
|
+
}
|
|
12
97
|
|
|
13
|
-
/**
|
|
14
|
-
* 创建页面会话缓存;打开时重读,选项操作仅在 HTTP 200 后更新缓存。
|
|
15
|
-
* Create a page-session cache; reload on open and mutate cache only after HTTP 200.
|
|
16
|
-
* @param {import("./client.mjs").PreferencesClientOptions} options 包内目录、请求与通知 / Internal catalog, requests and notifications.
|
|
17
|
-
* @returns {import("./client.mjs").PreferencesClient} 通用客户端 / Generic client.
|
|
18
|
-
*/
|
|
19
|
-
export function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
|
|
20
98
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
99
|
+
* 删除单个字段覆盖值。
|
|
100
|
+
* Delete one field override.
|
|
101
|
+
* @param {string} key 字段路径 / Field path.
|
|
102
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
24
103
|
*/
|
|
25
|
-
|
|
104
|
+
remove(key) {
|
|
105
|
+
return this.#change("delete", { key }, "delete", key);
|
|
106
|
+
}
|
|
107
|
+
|
|
26
108
|
/**
|
|
27
|
-
*
|
|
28
|
-
* Send a
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {
|
|
31
|
-
* @
|
|
32
|
-
* @param {AbortSignal | undefined} signal 会话取消信号 / Session cancellation signal.
|
|
33
|
-
* @returns {Promise<Response>} 未消费正文的响应 / Response with an unread body.
|
|
34
|
-
* @throws {Error} 非 200 且非数据 GET 404、超时、取消或网络错误 / Non-200 status except missing-data GETs, timeout, cancellation or network error.
|
|
109
|
+
* 向模块 API 发送 JSON 动作。
|
|
110
|
+
* Send a JSON action to the module API.
|
|
111
|
+
* @param {"get" | "set" | "delete"} action 模块动作 / Module action.
|
|
112
|
+
* @param {unknown} payload JSON 请求体 / JSON request body.
|
|
113
|
+
* @returns {Promise<Response>} 原始响应 / Raw response.
|
|
35
114
|
*/
|
|
36
|
-
async
|
|
115
|
+
async #send(action, payload) {
|
|
37
116
|
const controller = new AbortController();
|
|
38
117
|
const abort = () => controller.abort();
|
|
39
|
-
if (signal
|
|
40
|
-
signal
|
|
41
|
-
const timer = setTimeout(abort, timeout);
|
|
118
|
+
if (this.#session.signal.aborted) abort();
|
|
119
|
+
this.#session.signal.addEventListener("abort", abort, { once: true });
|
|
120
|
+
const timer = setTimeout(abort, this.#timeout);
|
|
42
121
|
try {
|
|
43
|
-
const response = await request(`/api/${action}`, {
|
|
122
|
+
const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
|
|
44
123
|
method: "POST",
|
|
45
124
|
credentials: "omit",
|
|
46
125
|
cache: "no-store",
|
|
47
126
|
signal: controller.signal,
|
|
48
|
-
headers: { "Content-Type": "application/
|
|
49
|
-
body:
|
|
127
|
+
headers: { "Content-Type": "application/json", "X-PreferencePanes-JSON": this.#configURL },
|
|
128
|
+
body: JSON.stringify(payload),
|
|
50
129
|
});
|
|
51
130
|
if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
52
131
|
return response;
|
|
53
132
|
} finally {
|
|
54
133
|
clearTimeout(timer);
|
|
55
|
-
signal
|
|
134
|
+
this.#session.signal.removeEventListener("abort", abort);
|
|
56
135
|
}
|
|
57
136
|
}
|
|
137
|
+
|
|
58
138
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @
|
|
63
|
-
* @
|
|
64
|
-
|
|
65
|
-
const snapshot = module => {
|
|
66
|
-
const state = sessions.get(module);
|
|
67
|
-
if (!state?.definition) throw new Error("Open the module first");
|
|
68
|
-
return structuredClone({ definition: state.definition, values: state.values });
|
|
69
|
-
};
|
|
70
|
-
/**
|
|
71
|
-
* 串行修改单键,仅成功后更新仍存活的会话。
|
|
72
|
-
* Serialize single-key mutations and update a still-active session only after success.
|
|
73
|
-
* @param {string} module 已打开模块 / Open module.
|
|
74
|
-
* @param {string} key 完整点分字段路径 / Complete dotted field path.
|
|
75
|
-
* @param {"set" | "delete"} action 写入或删除 / Write or delete.
|
|
76
|
-
* @param {unknown} value 写入值,删除时忽略 / Write value, ignored for deletion.
|
|
77
|
-
* @param {"write" | "delete" | "clearCaches" | "reset"} [operation] 操作类型 / Operation kind.
|
|
139
|
+
* 执行写入动作;成功后只更新当前页面值。
|
|
140
|
+
* Execute a mutation and update only the current page values after success.
|
|
141
|
+
* @param {"set" | "delete"} action API 动作 / API action.
|
|
142
|
+
* @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
|
|
143
|
+
* @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
|
|
144
|
+
* @param {string} [key] 字段路径 / Field path.
|
|
78
145
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
79
|
-
* @throws {Error} 会话、字段、值或请求错误 / Session, field, value or request error.
|
|
80
146
|
*/
|
|
81
|
-
async
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (state.saving) throw new Error("A settings write is already in progress");
|
|
85
|
-
const field = state.definition.fields.find(field => field.key === key);
|
|
86
|
-
state.saving = true;
|
|
147
|
+
async #change(action, payload, operation, key) {
|
|
148
|
+
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
149
|
+
this.#saving = true;
|
|
87
150
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const candidate of state.definition.fields) {
|
|
99
|
-
if (candidate.key !== key && !candidate.key.startsWith(`${key}.`)) continue;
|
|
100
|
-
delete state.values[candidate.key];
|
|
101
|
-
if (Object.hasOwn(candidate, "defaultValue")) state.values[candidate.key] = structuredClone(candidate.defaultValue);
|
|
102
|
-
}
|
|
103
|
-
break;
|
|
151
|
+
await this.#send(action, payload);
|
|
152
|
+
switch (operation) {
|
|
153
|
+
case "write":
|
|
154
|
+
this.#values[key] = structuredClone(payload.value);
|
|
155
|
+
break;
|
|
156
|
+
case "delete": {
|
|
157
|
+
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
158
|
+
delete this.#values[key];
|
|
159
|
+
if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
160
|
+
break;
|
|
104
161
|
}
|
|
162
|
+
case "clearCaches":
|
|
163
|
+
break;
|
|
164
|
+
case "reset":
|
|
165
|
+
for (const field of this.#definition.fields) {
|
|
166
|
+
delete this.#values[field.key];
|
|
167
|
+
if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
105
170
|
}
|
|
106
|
-
notify({ kind: "success", operation, module, key });
|
|
171
|
+
this.#notify({ kind: "success", operation, module: this.#module, key });
|
|
107
172
|
} catch (error) {
|
|
108
|
-
notify({ kind: "error", operation, module, key, message: error.message });
|
|
173
|
+
this.#notify({ kind: "error", operation, module: this.#module, key, message: error.message });
|
|
109
174
|
throw error;
|
|
110
175
|
} finally {
|
|
111
|
-
|
|
176
|
+
this.#saving = false;
|
|
112
177
|
}
|
|
113
178
|
}
|
|
114
|
-
return {
|
|
115
|
-
/**
|
|
116
|
-
* 从已导入的 JSON 创建新会话,只读取一次设置值。
|
|
117
|
-
* Create a session from imported JSON and read stored settings once.
|
|
118
|
-
* @param {string} module 模块标识 / Module identifier.
|
|
119
|
-
* @returns {Promise<import("./client.mjs").ModuleSnapshot>} 新快照 / New snapshot.
|
|
120
|
-
* @throws {Error} 读取失败、会话被替换或写入尚未完成 / Read failure, replaced session or unfinished write.
|
|
121
|
-
*/
|
|
122
|
-
async open(module) {
|
|
123
|
-
const binding = catalog.modules.get(module);
|
|
124
|
-
if (!binding) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
125
|
-
const previous = sessions.get(module);
|
|
126
|
-
if (previous?.saving) throw new Error("Cannot refresh while saving");
|
|
127
|
-
previous?.controller.abort();
|
|
128
|
-
const state = { controller: new AbortController(), definition: null, values: {}, saving: false };
|
|
129
|
-
sessions.set(module, state);
|
|
130
|
-
try {
|
|
131
|
-
const definition = normalizeBoxJs(catalog, module);
|
|
132
|
-
const response = await send(`@${definition.storageKey}.${definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
|
|
133
|
-
let subtree = response.status === 404 ? {} : await response.json();
|
|
134
|
-
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
135
|
-
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
136
|
-
if (sessions.get(module) !== state) throw new Error("Module session was replaced");
|
|
137
|
-
state.definition = definition;
|
|
138
|
-
for (const field of definition.fields) {
|
|
139
|
-
const stored = field.key
|
|
140
|
-
.split(".")
|
|
141
|
-
.slice(definition.settingsPath.length)
|
|
142
|
-
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
143
|
-
const value = stored === undefined ? field.defaultValue : stored;
|
|
144
|
-
if (value !== undefined) state.values[field.key] = normalizeStoredValue(field, value);
|
|
145
|
-
}
|
|
146
|
-
return snapshot(module);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (sessions.get(module) === state) sessions.delete(module);
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
snapshot,
|
|
153
|
-
/**
|
|
154
|
-
* 按需重新读取模块 Settings,不更新页面会话缓存。
|
|
155
|
-
* Reread module Settings on demand without updating the page-session cache.
|
|
156
|
-
* @param {string} module 已打开的模块 / Open module.
|
|
157
|
-
* @returns {Promise<unknown>} 设置值,缺失为 undefined / Settings value, or undefined when absent.
|
|
158
|
-
*/
|
|
159
|
-
async readSettings(module) {
|
|
160
|
-
const state = sessions.get(module);
|
|
161
|
-
if (!state?.definition) throw new Error("Open the module first");
|
|
162
|
-
const response = await send(`@${state.definition.storageKey}.${state.definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
|
|
163
|
-
return response.status === 404 ? undefined : response.json();
|
|
164
|
-
},
|
|
165
|
-
/**
|
|
166
|
-
* 按需读取模块 Caches,不自动读取其它设置。
|
|
167
|
-
* Read module Caches on demand without refreshing other settings.
|
|
168
|
-
* @param {string} module 已打开的模块 / Open module.
|
|
169
|
-
* @returns {Promise<unknown>} 缓存值,缺失为 undefined / Cache value, or undefined when absent.
|
|
170
|
-
*/
|
|
171
|
-
async readCaches(module) {
|
|
172
|
-
const state = sessions.get(module);
|
|
173
|
-
if (!state?.definition) throw new Error("Open the module first");
|
|
174
|
-
const response = await send(`@${state.definition.storageKey}.${module}.Caches`, "get", undefined, state.controller.signal);
|
|
175
|
-
return response.status === 404 ? undefined : response.json();
|
|
176
|
-
},
|
|
177
|
-
/**
|
|
178
|
-
* 删除整个 Caches 节点,成功后不追加 GET。
|
|
179
|
-
* Delete the entire Caches node without a follow-up GET.
|
|
180
|
-
* @param {string} module 已打开模块 / Open module.
|
|
181
|
-
* @returns {Promise<void>} 清理完成 / Cleanup completion.
|
|
182
|
-
*/
|
|
183
|
-
clearCaches: module => change(module, `${module}.Caches`, "delete", undefined, "clearCaches"),
|
|
184
|
-
/**
|
|
185
|
-
* 删除整个模块持久化节点,以当前 BoxJS 默认值重置页面缓存。
|
|
186
|
-
* Delete module persistence and reset the page cache using current BoxJS defaults.
|
|
187
|
-
* @param {string} module 已打开模块 / Open module.
|
|
188
|
-
* @returns {Promise<void>} 重置完成 / Reset completion.
|
|
189
|
-
*/
|
|
190
|
-
reset: module => change(module, module, "delete", undefined, "reset"),
|
|
191
|
-
/**
|
|
192
|
-
* 取消读取并清除会话,不撤销已发送的写入。
|
|
193
|
-
* Abort reads and clear the session without undoing dispatched writes.
|
|
194
|
-
* @param {string} module 模块标识 / Module identifier.
|
|
195
|
-
* @returns {void} 无返回值 / No return value.
|
|
196
|
-
*/
|
|
197
|
-
leave(module) {
|
|
198
|
-
sessions.get(module)?.controller.abort();
|
|
199
|
-
sessions.delete(module);
|
|
200
|
-
},
|
|
201
|
-
/**
|
|
202
|
-
* 写入单键并更新当前会话。
|
|
203
|
-
* Write one key and update the current session.
|
|
204
|
-
* @param {string} module 已打开模块 / Open module.
|
|
205
|
-
* @param {string} key 点分字段路径 / Dotted field path.
|
|
206
|
-
* @param {import("../index.js").SettingsScalar | import("../index.js").SettingsScalar[]} value 字段值 / Field value.
|
|
207
|
-
* @returns {Promise<void>} 写入完成 / Write completion.
|
|
208
|
-
*/
|
|
209
|
-
set: (module, key, value) => change(module, key, "set", value),
|
|
210
|
-
/**
|
|
211
|
-
* 删除单键覆盖值并显示默认值。
|
|
212
|
-
* Delete one override and display its default value.
|
|
213
|
-
* @param {string} module 已打开模块 / Open module.
|
|
214
|
-
* @param {string} key 点分字段路径 / Dotted field path.
|
|
215
|
-
* @returns {Promise<void>} 删除完成 / Delete completion.
|
|
216
|
-
*/
|
|
217
|
-
remove: (module, key) => change(module, key, "delete"),
|
|
218
|
-
};
|
|
219
179
|
}
|
package/src/browser/index.d.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModuleModel } from "../index.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 具体模块设置页的生命周期句柄。
|
|
5
5
|
* Lifecycle handle for a concrete module settings page.
|
|
6
6
|
*/
|
|
7
7
|
export interface MountedPreferences {
|
|
8
|
+
/** 移除页面、样式和监听器 / Remove page, styles and listeners. */
|
|
9
|
+
destroy(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 管理模块设置视图的模型、样式、主题和面板生命周期。
|
|
13
|
+
* Manage model, styles, theme, and panel lifecycle for a module settings view.
|
|
14
|
+
*/
|
|
15
|
+
export class PreferencesView implements MountedPreferences {
|
|
8
16
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
17
|
+
* 使用模块 API 返回的模型挂载设置页。
|
|
18
|
+
* Mount a settings page from the model returned by the module API.
|
|
19
|
+
* @param model 模块 API 模型 / Module API model.
|
|
20
|
+
* @param css 可选 CSS 正文 / Optional CSS text.
|
|
12
21
|
*/
|
|
22
|
+
constructor(model: ModuleModel, css?: string);
|
|
23
|
+
/** 移除页面、样式和监听器 / Remove page, styles and listeners. */
|
|
13
24
|
destroy(): void;
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Mount
|
|
18
|
-
* @param
|
|
19
|
-
* @param css 可选 CSS
|
|
20
|
-
* @returns
|
|
27
|
+
* 使用模块 API 返回的模型挂载设置页;CSS 仅覆盖当前模块。
|
|
28
|
+
* Mount a settings page from a module API model; CSS only overrides this module.
|
|
29
|
+
* @param model 模块 API 模型 / Module API model.
|
|
30
|
+
* @param css 可选 CSS 正文 / Optional CSS text.
|
|
31
|
+
* @returns 模块视图 / Module view.
|
|
21
32
|
*/
|
|
22
|
-
export function mount(
|
|
33
|
+
export function mount(model: ModuleModel, css?: string): PreferencesView;
|
package/src/browser/index.mjs
CHANGED
|
@@ -1,82 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { pageInputs } from "../lib/page-inputs.mjs";
|
|
2
|
+
import { statusView } from "./components.mjs";
|
|
3
|
+
import { PreferencesView } from "./mount.mjs";
|
|
4
4
|
import { installDefaultStyles } from "./styles.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param {import("../index.js").BoxJSInput} boxjs 单个模块的 BoxJS JSON / BoxJS JSON for one module.
|
|
10
|
-
* @param {string} [css] 可选 CSS 正文 / Optional CSS text.
|
|
11
|
-
* @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
|
|
7
|
+
* 管理模块文档的页面输入、初始请求、重载和错误状态。
|
|
8
|
+
* Manage page inputs, initial requests, reloads, and error states for a module document.
|
|
12
9
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
export class ModulePage {
|
|
11
|
+
#document;
|
|
12
|
+
#window;
|
|
13
|
+
#root;
|
|
14
|
+
#view;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 创建模块页面控制器并安装基础样式。
|
|
18
|
+
* Create the module page controller and install base styles.
|
|
19
|
+
* @param {Document} document 模块文档 / Module document.
|
|
20
|
+
*/
|
|
21
|
+
constructor(document) {
|
|
22
|
+
this.#document = document;
|
|
23
|
+
this.#window = document.defaultView;
|
|
24
|
+
this.#root = document.querySelector("#preferences");
|
|
25
|
+
installDefaultStyles(document);
|
|
26
|
+
this.#window.addEventListener("pageshow", this.#show);
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
const custom = element("style", "");
|
|
28
|
-
custom.textContent = css;
|
|
29
|
-
document.head.append(custom);
|
|
30
|
-
const previousTitle = document.title;
|
|
31
|
-
const previousTheme = document.documentElement.dataset.theme;
|
|
32
|
-
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
33
|
-
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
34
|
-
const host = window.frameElement?.ownerDocument.documentElement;
|
|
28
|
+
|
|
35
29
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @returns {void}
|
|
30
|
+
* 从 URL 或代理传递的 Header 导入 JSON/CSS,支持独立文档与 srcdoc。
|
|
31
|
+
* Import JSON/CSS from the URL or proxy-carried headers in standalone and srcdoc documents.
|
|
32
|
+
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
39
33
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
async start() {
|
|
35
|
+
try {
|
|
36
|
+
this.#view?.destroy();
|
|
37
|
+
this.#view = undefined;
|
|
38
|
+
this.#root.replaceChildren(statusView("读取设置…"));
|
|
39
|
+
const inputs = this.#readInputs();
|
|
40
|
+
const apiURL = new URL(`/api/${encodeURIComponent(inputs.module)}`, inputs.url).href;
|
|
41
|
+
const styleURL = this.#resourceURL(inputs.css, inputs.url);
|
|
42
|
+
const [style, modelResponse] = await Promise.all([styleURL ? fetch(styleURL, { cache: "no-store", credentials: "omit" }) : null, fetch(apiURL, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json", "X-PreferencePanes-JSON": inputs.json } })]);
|
|
43
|
+
if ((style && style.status !== 200) || modelResponse.status !== 200) throw new Error(`HTTP ${modelResponse.status !== 200 ? modelResponse.status : style.status}`);
|
|
44
|
+
this.#view = new PreferencesView(await modelResponse.json(), style ? await style.text() : "");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
|
|
47
|
+
}
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
destroy()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 释放页面视图和页面级监听器。
|
|
52
|
+
* Release the page view and page-level listener.
|
|
53
|
+
* @returns {void} 无返回值 / No return value.
|
|
54
|
+
*/
|
|
55
|
+
destroy() {
|
|
56
|
+
this.#window.removeEventListener("pageshow", this.#show);
|
|
57
|
+
this.#view?.destroy();
|
|
58
|
+
this.#view = undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 读取嵌入参数、文档元数据或当前 URL 输入。
|
|
63
|
+
* Read embedded parameters, document metadata, or current URL inputs.
|
|
64
|
+
* @returns {ReturnType<typeof pageInputs>} 页面输入 / Page inputs.
|
|
65
|
+
*/
|
|
66
|
+
#readInputs() {
|
|
67
|
+
const context = this.#document.querySelector('meta[name="preference-panes-inputs"]');
|
|
68
|
+
const embedded = this.#window.frameElement?.dataset.preferencePanes;
|
|
69
|
+
switch (true) {
|
|
70
|
+
case embedded !== undefined:
|
|
71
|
+
this.#document.documentElement.dataset.preferencePanesEmbedded = "";
|
|
72
|
+
return JSON.parse(embedded);
|
|
73
|
+
case context !== null:
|
|
74
|
+
return JSON.parse(decodeURIComponent(context.content));
|
|
75
|
+
default:
|
|
76
|
+
return pageInputs(new URL(this.#window.location.href));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 将可选页面资源限制为 HTTP(S) 地址。
|
|
82
|
+
* Restrict an optional page resource to an HTTP(S) URL.
|
|
83
|
+
* @param {string | undefined} source 资源地址 / Resource location.
|
|
84
|
+
* @param {string} baseURL 页面基准地址 / Page base URL.
|
|
85
|
+
* @returns {string | null} 绝对资源地址 / Absolute resource URL.
|
|
86
|
+
*/
|
|
87
|
+
#resourceURL(source, baseURL) {
|
|
88
|
+
if (!source) return null;
|
|
89
|
+
const url = new URL(source, baseURL);
|
|
90
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Resources must use HTTP(S) URLs");
|
|
91
|
+
return url.href;
|
|
81
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 从前进后退缓存恢复时重新加载模块。
|
|
96
|
+
* Reload the module when restored from the back-forward cache.
|
|
97
|
+
* @param {PageTransitionEvent} event 页面显示事件 / Page show event.
|
|
98
|
+
* @returns {void} 无返回值 / No return value.
|
|
99
|
+
*/
|
|
100
|
+
#show = event => {
|
|
101
|
+
if (event.persisted) this.start();
|
|
102
|
+
};
|
|
82
103
|
}
|
|
104
|
+
|
|
105
|
+
new ModulePage(document).start();
|
package/src/browser/module.html
CHANGED