@nsnanocat/preference-panes 1.1.1 → 1.1.2

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.
@@ -1,94 +1,3 @@
1
- /**
2
- * 创建元素,所有展示文本通过 textContent 写入。
3
- * Create elements and assign display text through textContent only.
4
- * @template {keyof HTMLElementTagNameMap} T
5
- * @param {T} tag 元素标签 / Element tag.
6
- * @param {string} className 样式类名 / CSS class.
7
- * @param {string} [text] 纯文本 / Plain text.
8
- * @returns {HTMLElementTagNameMap[T]} 创建的元素 / Created element.
9
- */
10
- function element(tag, className, text) {
11
- const node = document.createElement(tag);
12
- node.className = className;
13
- if (text !== undefined) node.textContent = text;
14
- return node;
15
- }
16
-
17
- /**
18
- * 创建通用设置行;外部 CSS 可通过 pp 类名覆盖视觉样式。
19
- * Create a generic settings row whose appearance can be overridden through pp classes.
20
- * @template {"div" | "label"} T
21
- * @param {T} tag 行元素 / Row element.
22
- * @returns {HTMLElementTagNameMap[T]} 设置行 / Settings row.
23
- */
24
- function settingRow(tag) {
25
- return element(tag, "pp-row");
26
- }
27
-
28
- /**
29
- * 为标准 HTML 输入控件添加通用面板类名。
30
- * Add the generic panel class to a standard HTML input control.
31
- * @param {HTMLElement} control 已创建的原生控件 / Existing native control.
32
- * @returns {HTMLElement} 输入控件 / Input control.
33
- */
34
- function fieldControl(control) {
35
- control.classList.add("pp-editor");
36
- return control;
37
- }
38
-
39
- /**
40
- * 元数据地址只允许 HTTP(S) 和相对地址。
41
- * Allow only HTTP(S) and relative metadata addresses.
42
- * @param {string} value 元数据地址 / Metadata address.
43
- * @returns {string} 完整地址 / Absolute address.
44
- */
45
- function resourceURL(value) {
46
- const url = new URL(value, document.baseURI);
47
- if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Metadata URLs must use HTTP(S)");
48
- return url.href;
49
- }
50
-
51
- /**
52
- * 创建覆盖可用内容区的通用读取状态,失败时可附加重试动作。
53
- * Create a shared status view that fills the available content area and may include retry.
54
- * @param {string} message 状态文本 / Status message.
55
- * @param {(() => unknown) | undefined} [retry] 重试动作 / Retry action.
56
- * @returns {HTMLElement} 居中状态视图 / Centered status view.
57
- */
58
- function statusView(message, retry) {
59
- const view = element("section", "pp-status");
60
- view.setAttribute("role", "status");
61
- view.setAttribute("aria-live", "polite");
62
- const spinner = element("span", "pp-status-spinner");
63
- spinner.setAttribute("aria-hidden", "true");
64
- view.append(spinner, element("p", "pp-status-message", message));
65
- if (retry) {
66
- const button = element("button", "pp-status-action", "重新读取");
67
- button.type = "button";
68
- button.onclick = retry;
69
- view.append(button);
70
- }
71
- return view;
72
- }
73
-
74
- /**
75
- * 请求宿主确认;独立网页使用浏览器对话框。
76
- * Request confirmation from the host, using the browser dialog for standalone pages.
77
- * @param {Window} host 模块窗口 / Module window.
78
- * @param {string} message 确认内容 / Confirmation message.
79
- * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
80
- */
81
- function requestConfirmation(host, message) {
82
- return new Promise((resolve, reject) => {
83
- const frame = host.frameElement;
84
- if (frame) {
85
- const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
86
- if (!frame.dispatchEvent(event)) return;
87
- }
88
- resolve(host.confirm(message));
89
- });
90
- }
91
-
92
1
  /**
93
2
  * 校验原始路径片段,不进行 URL 编码转换。
94
3
  * Validate raw path segments without URL encoding conversion.
@@ -136,8 +45,8 @@ function normalizeBoxJs(config, module) {
136
45
  target.owners.add(app);
137
46
  }
138
47
  }
139
- if (modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
140
- const target = modules.values().next().value ;
48
+ if (module === undefined && modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
49
+ const target = module === undefined ? modules.values().next().value : modules.get(module);
141
50
  if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
142
51
  const metadata = normalizeMetadata(target.owners.size === 1 ? presentation([...target.owners][0]) : {});
143
52
  const fields = [];
@@ -274,6 +183,97 @@ function validValue(field, value) {
274
183
  return !field.options || (field.type === "array" ? value : [value]).every(item => field.options.some(option => option.key === item));
275
184
  }
276
185
 
186
+ /**
187
+ * 创建元素,所有展示文本通过 textContent 写入。
188
+ * Create elements and assign display text through textContent only.
189
+ * @template {keyof HTMLElementTagNameMap} T
190
+ * @param {T} tag 元素标签 / Element tag.
191
+ * @param {string} className 样式类名 / CSS class.
192
+ * @param {string} [text] 纯文本 / Plain text.
193
+ * @returns {HTMLElementTagNameMap[T]} 创建的元素 / Created element.
194
+ */
195
+ function element(tag, className, text) {
196
+ const node = document.createElement(tag);
197
+ node.className = className;
198
+ if (text !== undefined) node.textContent = text;
199
+ return node;
200
+ }
201
+
202
+ /**
203
+ * 创建通用设置行;外部 CSS 可通过 pp 类名覆盖视觉样式。
204
+ * Create a generic settings row whose appearance can be overridden through pp classes.
205
+ * @template {"div" | "label"} T
206
+ * @param {T} tag 行元素 / Row element.
207
+ * @returns {HTMLElementTagNameMap[T]} 设置行 / Settings row.
208
+ */
209
+ function settingRow(tag) {
210
+ return element(tag, "pp-row");
211
+ }
212
+
213
+ /**
214
+ * 为标准 HTML 输入控件添加通用面板类名。
215
+ * Add the generic panel class to a standard HTML input control.
216
+ * @param {HTMLElement} control 已创建的原生控件 / Existing native control.
217
+ * @returns {HTMLElement} 输入控件 / Input control.
218
+ */
219
+ function fieldControl(control) {
220
+ control.classList.add("pp-editor");
221
+ return control;
222
+ }
223
+
224
+ /**
225
+ * 元数据地址只允许 HTTP(S) 和相对地址。
226
+ * Allow only HTTP(S) and relative metadata addresses.
227
+ * @param {string} value 元数据地址 / Metadata address.
228
+ * @returns {string} 完整地址 / Absolute address.
229
+ */
230
+ function resourceURL(value) {
231
+ const url = new URL(value, document.baseURI);
232
+ if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Metadata URLs must use HTTP(S)");
233
+ return url.href;
234
+ }
235
+
236
+ /**
237
+ * 创建覆盖可用内容区的通用读取状态,失败时可附加重试动作。
238
+ * Create a shared status view that fills the available content area and may include retry.
239
+ * @param {string} message 状态文本 / Status message.
240
+ * @param {(() => unknown) | undefined} [retry] 重试动作 / Retry action.
241
+ * @returns {HTMLElement} 居中状态视图 / Centered status view.
242
+ */
243
+ function statusView(message, retry) {
244
+ const view = element("section", "pp-status");
245
+ view.setAttribute("role", "status");
246
+ view.setAttribute("aria-live", "polite");
247
+ const spinner = element("span", "pp-status-spinner");
248
+ spinner.setAttribute("aria-hidden", "true");
249
+ view.append(spinner, element("p", "pp-status-message", message));
250
+ if (retry) {
251
+ const button = element("button", "pp-status-action", "重新读取");
252
+ button.type = "button";
253
+ button.onclick = retry;
254
+ view.append(button);
255
+ }
256
+ return view;
257
+ }
258
+
259
+ /**
260
+ * 请求宿主确认;独立网页使用浏览器对话框。
261
+ * Request confirmation from the host, using the browser dialog for standalone pages.
262
+ * @param {Window} host 模块窗口 / Module window.
263
+ * @param {string} message 确认内容 / Confirmation message.
264
+ * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
265
+ */
266
+ function requestConfirmation(host, message) {
267
+ return new Promise((resolve, reject) => {
268
+ const frame = host.frameElement;
269
+ if (frame) {
270
+ const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
271
+ if (!frame.dispatchEvent(event)) return;
272
+ }
273
+ resolve(host.confirm(message));
274
+ });
275
+ }
276
+
277
277
  /**
278
278
  * 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
279
279
  * Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
@@ -489,7 +489,7 @@ class PreferencesClient {
489
489
  * @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
490
490
  */
491
491
  async readSettings() {
492
- const response = await this.#send("get", { scope: "settings" });
492
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
493
493
  return response.status === 404 ? undefined : response.json();
494
494
  }
495
495
 
@@ -499,7 +499,7 @@ class PreferencesClient {
499
499
  * @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
500
500
  */
501
501
  async readCaches() {
502
- const response = await this.#send("get", { scope: "caches" });
502
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
503
503
  return response.status === 404 ? undefined : response.json();
504
504
  }
505
505
 
@@ -509,7 +509,7 @@ class PreferencesClient {
509
509
  * @returns {Promise<void>} 操作完成 / Operation completion.
510
510
  */
511
511
  clearCaches() {
512
- return this.#change("delete", { scope: "caches" }, "clearCaches");
512
+ return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
513
513
  }
514
514
 
515
515
  /**
@@ -518,7 +518,7 @@ class PreferencesClient {
518
518
  * @returns {Promise<void>} 操作完成 / Operation completion.
519
519
  */
520
520
  reset() {
521
- return this.#change("delete", { scope: "module" }, "reset");
521
+ return this.#change("delete", this.#module, undefined, "reset");
522
522
  }
523
523
 
524
524
  /**
@@ -538,7 +538,7 @@ class PreferencesClient {
538
538
  * @returns {Promise<void>} 操作完成 / Operation completion.
539
539
  */
540
540
  set(key, value) {
541
- return this.#change("set", { key, value }, "write", key);
541
+ return this.#change("set", key, value, "write");
542
542
  }
543
543
 
544
544
  /**
@@ -548,30 +548,31 @@ class PreferencesClient {
548
548
  * @returns {Promise<void>} 操作完成 / Operation completion.
549
549
  */
550
550
  remove(key) {
551
- return this.#change("delete", { key }, "delete", key);
551
+ return this.#change("delete", key, undefined, "delete");
552
552
  }
553
553
 
554
554
  /**
555
- * 向模块 API 发送 JSON 动作。
556
- * Send a JSON action to the module API.
557
- * @param {"get" | "set" | "delete"} action 模块动作 / Module action.
558
- * @param {unknown} payload JSON 请求体 / JSON request body.
555
+ * 向固定存储 API 发送完整路径的 form 动作。
556
+ * Send a complete-path form action to the fixed storage API.
557
+ * @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
558
+ * @param {string} path 完整 @root.path / Complete @root.path.
559
+ * @param {unknown} [value] set 写入值 / Value written by set.
559
560
  * @returns {Promise<Response>} 原始响应 / Raw response.
560
561
  */
561
- async #send(action, payload) {
562
+ async #send(action, path, value) {
562
563
  const controller = new AbortController();
563
564
  const abort = () => controller.abort();
564
565
  if (this.#session.signal.aborted) abort();
565
566
  this.#session.signal.addEventListener("abort", abort, { once: true });
566
567
  const timer = setTimeout(abort, this.#timeout);
567
568
  try {
568
- const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
569
+ const response = await this.#request(`/api/${action}`, {
569
570
  method: "POST",
570
571
  credentials: "omit",
571
572
  cache: "no-store",
572
573
  signal: controller.signal,
573
- headers: { "Content-Type": "application/json" },
574
- body: JSON.stringify(payload),
574
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
575
+ body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
575
576
  });
576
577
  if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
577
578
  return response;
@@ -585,28 +586,28 @@ class PreferencesClient {
585
586
  * 执行写入动作;成功后只更新当前页面值。
586
587
  * Execute a mutation and update only the current page values after success.
587
588
  * @param {"set" | "delete"} action API 动作 / API action.
588
- * @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
589
+ * @param {string} key 不含存储根的路径 / Path without the storage root.
590
+ * @param {unknown} value set 写入值 / Value written by set.
589
591
  * @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
590
- * @param {string} [key] 字段路径 / Field path.
591
592
  * @returns {Promise<void>} 操作完成 / Operation completion.
592
593
  */
593
- async #change(action, payload, operation, key) {
594
+ async #change(action, key, value, operation) {
594
595
  if (this.#saving) throw new Error("A settings write is already in progress");
595
596
  this.#saving = true;
596
597
  try {
597
- if (operation === "write") {
598
- const field = this.#definition.fields.find(candidate => candidate.key === key);
599
- if (!field || !validValue(field, payload.value)) throw new TypeError("Invalid setting value");
598
+ let field;
599
+ if (operation === "write" || operation === "delete") {
600
+ field = this.#definition.fields.find(candidate => candidate.key === key);
601
+ if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
600
602
  }
601
- await this.#send(action, payload);
603
+ await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
602
604
  switch (operation) {
603
605
  case "write":
604
- this.#values[key] = structuredClone(payload.value);
606
+ this.#values[key] = structuredClone(value);
605
607
  break;
606
608
  case "delete": {
607
- const field = this.#definition.fields.find(candidate => candidate.key === key);
608
609
  delete this.#values[key];
609
- if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
+ if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
611
  break;
611
612
  }
612
613
  case "clearCaches":
@@ -1440,8 +1441,8 @@ class ModulePage {
1440
1441
  }
1441
1442
 
1442
1443
  /**
1443
- * 从规范模块路径读取 BoxJS JSON 并挂载通用前端。
1444
- * Read BoxJS JSON from the conventional module path and mount the generic frontend.
1444
+ * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
1445
+ * Read BoxJS JSON through the module API and mount the generic frontend.
1445
1446
  * @returns {Promise<void>} 启动完成 / Startup completion.
1446
1447
  */
1447
1448
  async start() {
@@ -1453,9 +1454,11 @@ class ModulePage {
1453
1454
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
1454
1455
  const module = embedded ?? match?.[1];
1455
1456
  if (!module) throw new TypeError("Open a concrete module URL");
1456
- const response = await fetch(`/configs/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1457
+ const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1457
1458
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
1458
- this.#view = mount(await response.json());
1459
+ const boxjs = await response.json();
1460
+ normalizeBoxJs(boxjs, module);
1461
+ this.#view = mount(boxjs);
1459
1462
  } catch (error) {
1460
1463
  this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
1461
1464
  }
@@ -489,7 +489,7 @@ class PreferencesClient {
489
489
  * @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
490
490
  */
491
491
  async readSettings() {
492
- const response = await this.#send("get", { scope: "settings" });
492
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
493
493
  return response.status === 404 ? undefined : response.json();
494
494
  }
495
495
 
@@ -499,7 +499,7 @@ class PreferencesClient {
499
499
  * @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
500
500
  */
501
501
  async readCaches() {
502
- const response = await this.#send("get", { scope: "caches" });
502
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
503
503
  return response.status === 404 ? undefined : response.json();
504
504
  }
505
505
 
@@ -509,7 +509,7 @@ class PreferencesClient {
509
509
  * @returns {Promise<void>} 操作完成 / Operation completion.
510
510
  */
511
511
  clearCaches() {
512
- return this.#change("delete", { scope: "caches" }, "clearCaches");
512
+ return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
513
513
  }
514
514
 
515
515
  /**
@@ -518,7 +518,7 @@ class PreferencesClient {
518
518
  * @returns {Promise<void>} 操作完成 / Operation completion.
519
519
  */
520
520
  reset() {
521
- return this.#change("delete", { scope: "module" }, "reset");
521
+ return this.#change("delete", this.#module, undefined, "reset");
522
522
  }
523
523
 
524
524
  /**
@@ -538,7 +538,7 @@ class PreferencesClient {
538
538
  * @returns {Promise<void>} 操作完成 / Operation completion.
539
539
  */
540
540
  set(key, value) {
541
- return this.#change("set", { key, value }, "write", key);
541
+ return this.#change("set", key, value, "write");
542
542
  }
543
543
 
544
544
  /**
@@ -548,30 +548,31 @@ class PreferencesClient {
548
548
  * @returns {Promise<void>} 操作完成 / Operation completion.
549
549
  */
550
550
  remove(key) {
551
- return this.#change("delete", { key }, "delete", key);
551
+ return this.#change("delete", key, undefined, "delete");
552
552
  }
553
553
 
554
554
  /**
555
- * 向模块 API 发送 JSON 动作。
556
- * Send a JSON action to the module API.
557
- * @param {"get" | "set" | "delete"} action 模块动作 / Module action.
558
- * @param {unknown} payload JSON 请求体 / JSON request body.
555
+ * 向固定存储 API 发送完整路径的 form 动作。
556
+ * Send a complete-path form action to the fixed storage API.
557
+ * @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
558
+ * @param {string} path 完整 @root.path / Complete @root.path.
559
+ * @param {unknown} [value] set 写入值 / Value written by set.
559
560
  * @returns {Promise<Response>} 原始响应 / Raw response.
560
561
  */
561
- async #send(action, payload) {
562
+ async #send(action, path, value) {
562
563
  const controller = new AbortController();
563
564
  const abort = () => controller.abort();
564
565
  if (this.#session.signal.aborted) abort();
565
566
  this.#session.signal.addEventListener("abort", abort, { once: true });
566
567
  const timer = setTimeout(abort, this.#timeout);
567
568
  try {
568
- const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
569
+ const response = await this.#request(`/api/${action}`, {
569
570
  method: "POST",
570
571
  credentials: "omit",
571
572
  cache: "no-store",
572
573
  signal: controller.signal,
573
- headers: { "Content-Type": "application/json" },
574
- body: JSON.stringify(payload),
574
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
575
+ body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
575
576
  });
576
577
  if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
577
578
  return response;
@@ -585,28 +586,28 @@ class PreferencesClient {
585
586
  * 执行写入动作;成功后只更新当前页面值。
586
587
  * Execute a mutation and update only the current page values after success.
587
588
  * @param {"set" | "delete"} action API 动作 / API action.
588
- * @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
589
+ * @param {string} key 不含存储根的路径 / Path without the storage root.
590
+ * @param {unknown} value set 写入值 / Value written by set.
589
591
  * @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
590
- * @param {string} [key] 字段路径 / Field path.
591
592
  * @returns {Promise<void>} 操作完成 / Operation completion.
592
593
  */
593
- async #change(action, payload, operation, key) {
594
+ async #change(action, key, value, operation) {
594
595
  if (this.#saving) throw new Error("A settings write is already in progress");
595
596
  this.#saving = true;
596
597
  try {
597
- if (operation === "write") {
598
- const field = this.#definition.fields.find(candidate => candidate.key === key);
599
- if (!field || !validValue(field, payload.value)) throw new TypeError("Invalid setting value");
598
+ let field;
599
+ if (operation === "write" || operation === "delete") {
600
+ field = this.#definition.fields.find(candidate => candidate.key === key);
601
+ if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
600
602
  }
601
- await this.#send(action, payload);
603
+ await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
602
604
  switch (operation) {
603
605
  case "write":
604
- this.#values[key] = structuredClone(payload.value);
606
+ this.#values[key] = structuredClone(value);
605
607
  break;
606
608
  case "delete": {
607
- const field = this.#definition.fields.find(candidate => candidate.key === key);
608
609
  delete this.#values[key];
609
- if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
+ if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
611
  break;
611
612
  }
612
613
  case "clearCaches":