@nsnanocat/preference-panes 0.9.15 → 1.0.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.
@@ -1,21 +1,23 @@
1
1
  import { ActionMenu } from "./ActionMenu.mjs";
2
+ import { validValue } from "./boxjs.mjs";
2
3
  import { createPreferencesClient } from "./client.mjs";
3
4
  import { fieldControl, element as node, requestConfirmation, resourceURL, settingRow, statusView } from "./components.mjs";
4
5
  import { Navigation } from "./Navigation.mjs";
5
6
 
6
7
  /**
7
- * 挂载已导入 BoxJS 对应的模块表单和短暂通知。
8
- * Mount the imported BoxJS module form and transient notifications.
8
+ * 挂载 API 返回的模块模型表单和短暂通知。
9
+ * Mount the module model returned by the API and transient notifications.
9
10
  * @param {HTMLElement} root 包内挂载元素 / Internal mount element.
10
- * @param {import("../BoxJS.mjs").BoxJS} catalog 包内 BoxJS 目录 / Internal BoxJS catalog.
11
+ * @param {import("../index.js").ModuleModel} model API 返回的模块模型 / Module model returned by the API.
11
12
  * @returns {import("./index.js").MountedPreferences} 面板生命周期句柄 / Panel lifecycle handle.
12
13
  */
13
- export function mountPanel(root, catalog) {
14
- const title = catalog.module.metadata.name ?? catalog.module.module;
14
+ export function mountPanel(root, model) {
15
+ const { definition } = model;
16
+ const title = definition.metadata?.name ?? definition.module;
15
17
  const document = root.ownerDocument;
16
18
  const window = document.defaultView;
17
19
  const shell = node("div", "pp-panel");
18
- shell.dataset.module = catalog.module.module;
20
+ shell.dataset.module = definition.module;
19
21
  const header = node("header", "pp-header");
20
22
  const back = node("button", "pp-back", "‹");
21
23
  back.setAttribute("aria-label", "返回");
@@ -45,7 +47,7 @@ export function mountPanel(root, catalog) {
45
47
  if (!frame?.dataset.preferencePanes) return;
46
48
  frame.dispatchEvent(
47
49
  new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
48
- detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled, actions },
50
+ detail: { title: heading.textContent, module: definition.module, busy: saving, canGoBack: !back.disabled, actions },
49
51
  }),
50
52
  );
51
53
  };
@@ -102,7 +104,7 @@ export function mountPanel(root, catalog) {
102
104
  toast.hidden = true;
103
105
  }, 2400);
104
106
  };
105
- const client = createPreferencesClient({ catalog, notify });
107
+ const client = createPreferencesClient({ model, definition, notify });
106
108
  /**
107
109
  * 两种菜单入口共用异步错误处理,包含宿主确认框错误。
108
110
  * Share async error handling between both menus, including host-dialog errors.
@@ -130,7 +132,6 @@ export function mountPanel(root, catalog) {
130
132
  publishNavigation();
131
133
  viewport.replaceChildren(statusView("读取设置…"));
132
134
  try {
133
- await client.open(module);
134
135
  if (version === generation) controls();
135
136
  } catch (error) {
136
137
  if (version !== generation) return;
@@ -144,7 +145,7 @@ export function mountPanel(root, catalog) {
144
145
  * @returns {void} 无返回值 / No return value.
145
146
  */
146
147
  function controls() {
147
- const { definition, values } = client.snapshot(active);
148
+ const { definition, values } = client.snapshot();
148
149
  heading.textContent = definition.metadata?.name || active;
149
150
  const view = node("section", "pp-fields");
150
151
  /**
@@ -182,7 +183,7 @@ export function mountPanel(root, catalog) {
182
183
  saving = true;
183
184
  back.disabled = true;
184
185
  publishNavigation();
185
- return (queue = queue
186
+ queue = queue
186
187
  .then(action)
187
188
  .then(() => {
188
189
  if (!destroyed) success();
@@ -195,10 +196,11 @@ export function mountPanel(root, catalog) {
195
196
  .finally(() => {
196
197
  pendingWrites--;
197
198
  saving = pendingWrites > 0;
198
- if (destroyed && !saving) client.leave(active);
199
+ if (destroyed && !saving) client.leave();
199
200
  back.disabled = saving || !navigation.canGoBack;
200
201
  publishNavigation();
201
- }));
202
+ });
203
+ return queue;
202
204
  }
203
205
  const metadata = definition.metadata;
204
206
  if (metadata) {
@@ -276,7 +278,7 @@ export function mountPanel(root, catalog) {
276
278
  link.append(summary, node("span", "pp-chevron", "›"));
277
279
  row.append(link);
278
280
  const refresh = () => {
279
- const value = client.snapshot(active).values[field.key];
281
+ const value = client.snapshot().values[field.key];
280
282
  summary.textContent =
281
283
  field.options
282
284
  .filter(option => Array.isArray(value) && value.includes(option.key))
@@ -377,10 +379,17 @@ export function mountPanel(root, catalog) {
377
379
  return;
378
380
  }
379
381
  const restore = () => {
380
- if (version === inputVersion) write(client.snapshot(module).values[field.key]);
382
+ if (version === inputVersion) write(client.snapshot().values[field.key]);
381
383
  };
382
384
  perform(
383
- () => client.set(module, field.key, value),
385
+ () => {
386
+ if (!validValue(field, value)) {
387
+ const error = new TypeError("Invalid setting value");
388
+ notify({ kind: "error", operation: "write", module, key: field.key, message: error.message });
389
+ throw error;
390
+ }
391
+ return client.set(field.key, value);
392
+ },
384
393
  () => {
385
394
  for (const refresh of summaries) refresh();
386
395
  },
@@ -401,7 +410,7 @@ export function mountPanel(root, catalog) {
401
410
  return perform(
402
411
  async () => {
403
412
  try {
404
- value = await client.readSettings(active);
413
+ value = await client.readSettings();
405
414
  } catch (error) {
406
415
  notify({ kind: "error", message: error.message });
407
416
  throw error;
@@ -425,7 +434,7 @@ export function mountPanel(root, catalog) {
425
434
  return perform(
426
435
  async () => {
427
436
  try {
428
- value = await client.readCaches(active);
437
+ value = await client.readCaches();
429
438
  } catch (error) {
430
439
  notify({ kind: "error", message: error.message });
431
440
  throw error;
@@ -441,7 +450,7 @@ export function mountPanel(root, catalog) {
441
450
  if (saving) return;
442
451
  if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
443
452
  return perform(
444
- () => client.clearCaches(active),
453
+ () => client.clearCaches(),
445
454
  () => {
446
455
  output.textContent = "暂无缓存";
447
456
  },
@@ -450,7 +459,7 @@ export function mountPanel(root, catalog) {
450
459
  handlers.set("reset", async () => {
451
460
  if (saving) return;
452
461
  if (!(await requestConfirmation(window, `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
453
- return perform(() => client.reset(active), controls);
462
+ return perform(() => client.reset(), controls);
454
463
  });
455
464
  navigation?.destroy();
456
465
  navigation = new Navigation(viewport, view, key => editors.get(key)?.node);
@@ -468,7 +477,7 @@ export function mountPanel(root, catalog) {
468
477
  if (navigation) navigation.back();
469
478
  else window.history.back();
470
479
  };
471
- open(catalog.module.module);
480
+ open(definition.module);
472
481
  return {
473
482
  /**
474
483
  * 移除监听器、定时器、会话和挂载内容。
@@ -481,7 +490,7 @@ export function mountPanel(root, catalog) {
481
490
  window.frameElement?.removeEventListener("preferencepanes:action", onAction);
482
491
  navigation?.destroy();
483
492
  generation++;
484
- if (active && !saving) client.leave(active);
493
+ if (active && !saving) client.leave();
485
494
  clearTimeout(timer);
486
495
  shell.remove();
487
496
  },
package/src/build.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { BoxJS } from "./BoxJS.mjs";
2
+ import { normalizeBoxJs } from "./browser/boxjs.mjs";
3
3
 
4
4
  /**
5
5
  * 仅生成模块前端文件,不复制配置或生成绑定业务的读写脚本。
@@ -10,8 +10,7 @@ import { BoxJS } from "./BoxJS.mjs";
10
10
  */
11
11
  export async function build(boxjs, css = "") {
12
12
  if (typeof css !== "string") throw new TypeError("CSS must be a string");
13
- const catalog = new BoxJS(boxjs);
14
- const module = catalog.module.module;
13
+ const module = normalizeBoxJs(boxjs).module;
15
14
  const [html, app] = await Promise.all([readFile(new URL("../dist/module/index.html", import.meta.url), "utf8"), readFile(new URL("../dist/module/app.mjs", import.meta.url), "utf8")]);
16
15
  return {
17
16
  [`settings/${module}/index.html`]: html,
package/src/index.d.ts CHANGED
@@ -92,8 +92,8 @@ export interface SettingsRequest {
92
92
  */
93
93
  headers?: Record<string, string | undefined>;
94
94
  /**
95
- * POST 的正文为一个 form 字段,字段名是完整 @root.path
96
- * POST contains one form field whose name is the complete @root.path.
95
+ * 模块动作的 JSON 正文
96
+ * JSON body for a module action.
97
97
  */
98
98
  body?: string;
99
99
  }
@@ -268,6 +268,20 @@ export interface BoxJSSubscription extends BoxJSMetadata {
268
268
  * The sole data configuration input.
269
269
  */
270
270
  export type BoxJSInput = BoxJSSetting[] | BoxJSApp | BoxJSSubscription;
271
+ /**
272
+ * 模块 API 返回的原始 BoxJS 与当前值模型。
273
+ * Raw BoxJS and current-value model returned by the module API.
274
+ */
275
+ export interface ModuleModel {
276
+ /** 模块路径段 / Module path segment. */
277
+ module: string;
278
+ /** API 获取的原始 BoxJS JSON / Raw BoxJS JSON fetched by the API. */
279
+ boxjs: BoxJSInput;
280
+ /** API 读取到的原始已保存字段值 / Raw persisted field values read by the API. */
281
+ values: Record<string, JsonValue>;
282
+ /** 后续 API 动作使用的 BoxJS JSON 地址 / BoxJS JSON URL used by later API actions. */
283
+ configURL: string;
284
+ }
271
285
  /**
272
286
  * 生成模块前端文件,不复制配置、不生成绑定模块的代理脚本。
273
287
  * Build module frontend files without copying configuration or producing bound proxy scripts.
package/src/web.mjs ADDED
@@ -0,0 +1,52 @@
1
+ import { URL } from "@nsnanocat/url";
2
+ import { $app } from "@nsnanocat/util/lib/app.mjs";
3
+ import { done } from "@nsnanocat/util/lib/done.mjs";
4
+ import assets from "#assets";
5
+ import { pageInputs } from "./lib/page-inputs.mjs";
6
+
7
+ /**
8
+ * 返回模块页面及其公共浏览器资源,不处理 API、网络或持久化。
9
+ * Serve module pages and common browser assets without handling APIs, network access or persistence.
10
+ * @returns {void} 响应已交给代理宿主 / Response delivered to the proxy host.
11
+ */
12
+ function run() {
13
+ const request = globalThis.$request;
14
+ let result;
15
+ try {
16
+ const url = new URL(request.url);
17
+ if (/^\/settings\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname)) {
18
+ if (!["GET", "HEAD"].includes(request.method)) result = response(request, 405, { error: "Method not allowed" });
19
+ else {
20
+ const inputs = encodeURIComponent(JSON.stringify(pageInputs(url, request.headers)));
21
+ result = response(request, 200, assets.page.body.replace("</head>", `<meta name="preference-panes-inputs" content="${inputs}"></head>`), "text/html");
22
+ }
23
+ } else {
24
+ const asset = assets[url.pathname];
25
+ if (asset) result = ["GET", "HEAD"].includes(request.method) ? response(request, 200, asset.body, asset.type) : response(request, 405, { error: "Method not allowed" });
26
+ }
27
+ } catch (error) {
28
+ console.error(`PreferencePanes Web: ${error.message}`);
29
+ result = response(request, 500, { error: error.message });
30
+ }
31
+ if (!result) done({});
32
+ else done($app === "Quantumult X" ? result : { response: result });
33
+ }
34
+
35
+ /**
36
+ * 构造静态资源响应,HEAD 请求不返回正文。
37
+ * Build a static resource response without a body for HEAD requests.
38
+ * @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
39
+ * @param {number} status HTTP 状态 / HTTP status.
40
+ * @param {unknown} body 响应正文 / Response body.
41
+ * @param {string} [type] 媒体类型 / Media type.
42
+ * @returns {import("./index.js").SettingsResponse} 静态资源响应 / Static resource response.
43
+ */
44
+ function response(request, status, body, type = "application/json") {
45
+ return {
46
+ status,
47
+ headers: { "Content-Type": `${type}; charset=utf-8`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
48
+ body: request.method === "HEAD" ? "" : type === "application/json" ? JSON.stringify(body) : body,
49
+ };
50
+ }
51
+
52
+ run();
package/src/BoxJS.mjs DELETED
@@ -1,72 +0,0 @@
1
- import { validatePathParts } from "./lib/settings-path.mjs";
2
-
3
- /**
4
- * BoxJS 的共同目录:模块、存储根和展示元数据都来自同一份 JSON。
5
- * Shared BoxJS catalog deriving modules, storage roots and metadata from one JSON document.
6
- */
7
- export class BoxJS {
8
- /**
9
- * 建立路径索引,不解析控件类型,也不读写持久化存储。
10
- * Index field paths without interpreting controls or accessing persistence.
11
- * @param {unknown} input 字段数组、单个 app 或 apps 订阅 / Field array, app or apps subscription.
12
- */
13
- constructor(input) {
14
- if (!input || typeof input !== "object") throw new TypeError("Expected BoxJS JSON");
15
- const document = JSON.parse(JSON.stringify(input));
16
- const apps = Array.isArray(document) ? [{ settings: document }] : (document.apps ?? [document]);
17
- if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
18
- this.modules = new Map();
19
- for (const app of apps) {
20
- if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
21
- for (const entry of app.settings) {
22
- if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
23
- if (!entry.id.startsWith("@")) {
24
- if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
25
- continue;
26
- }
27
- const [storageKey, ...parts] = entry.id.slice(1).split(".");
28
- if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
29
- validatePathParts(parts);
30
- const module = parts[0];
31
- let target = this.modules.get(module);
32
- if (!target) {
33
- target = { module, storageKey, entries: [], owners: new Set() };
34
- this.modules.set(module, target);
35
- }
36
- if (target.storageKey !== storageKey) throw new TypeError(`A module must use one storage root: ${module}`);
37
- target.entries.push(entry);
38
- target.owners.add(app);
39
- }
40
- }
41
- this.metadata = metadata(Array.isArray(document) ? {} : document);
42
- for (const target of this.modules.values()) target.metadata = target.owners.size === 1 ? metadata([...target.owners][0]) : {};
43
- }
44
-
45
- /**
46
- * 取得本次导入的唯一模块,避免把模块数据变成项目目录。
47
- * Get the single imported module without turning module data into a project directory.
48
- * @returns {object} 唯一模块的目录项 / The single module entry.
49
- */
50
- get module() {
51
- if (this.modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
52
- return this.modules.values().next().value;
53
- }
54
- }
55
-
56
- /**
57
- * 保留标准 BoxJS 展示信息;script 仅为元数据,不执行。
58
- * Retain standard BoxJS presentation data; script is metadata only and never executed.
59
- * @param {object} source BoxJS app 或订阅 / BoxJS app or subscription.
60
- * @returns {object} 经过类型检查的展示信息 / Type-checked presentation metadata.
61
- */
62
- function metadata(source) {
63
- const result = {};
64
- for (const key of ["id", "name", "author", "repo", "script", "icon", "description", "desc", "icons", "descs"]) {
65
- if (source[key] === undefined) continue;
66
- const multiple = key === "icons" || key === "descs";
67
- const values = multiple ? source[key] : [source[key]];
68
- if (!Array.isArray(values) || values.some(item => typeof item !== "string")) throw new TypeError(`Invalid BoxJS app ${key}`);
69
- result[key] = multiple ? [...values] : source[key];
70
- }
71
- return result;
72
- }
package/src/Store.mjs DELETED
@@ -1,114 +0,0 @@
1
- import { URL } from "@nsnanocat/url";
2
- import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
3
- import { Storage } from "@nsnanocat/util/polyfill/Storage";
4
- import { response } from "./lib/response.mjs";
5
- import { validatePathParts } from "./lib/settings-path.mjs";
6
-
7
- /**
8
- * 无配置绑定的本地存储桥接;form 字段名就是完整 @root.path。
9
- * Unbound local storage bridge; the form field name is the complete @root.path.
10
- */
11
- export class Store {
12
- /**
13
- * POST /api/get、set、delete;不下载配置、不解析控件、不鉴权。
14
- * POST /api/get, set or delete without config downloads, control parsing or authentication.
15
- * @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
16
- * @param {URL} [url] 已解析地址 / Parsed URL.
17
- * @returns {Promise<import("./index.js").SettingsResponse | undefined>} 操作结果 / Operation result.
18
- */
19
- async handle(request, url = new URL(request.url)) {
20
- if (!url.pathname.startsWith("/api/")) return;
21
- const reply = (status, data) => response(request, status, data);
22
- const action = url.pathname.slice(5);
23
- if (!["get", "set", "delete"].includes(action)) return reply(404, { error: "Unknown action" });
24
- if (request.method !== "POST") return reply(405, { error: "Use POST with a form body" });
25
- const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
26
- if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") return reply(415, { error: "Expected application/x-www-form-urlencoded" });
27
- if (typeof request.body !== "string" || request.body.length > 65536) return reply(400, { error: "Expected a form body up to 65536 characters" });
28
- let parts, value;
29
- try {
30
- const fields = request.body.split("&");
31
- if (fields.length !== 1) throw new TypeError("Send exactly one storage key");
32
- const separator = fields[0].indexOf("=");
33
- if (separator < 0) throw new TypeError("Expected @root.path=value");
34
- const key = decodeURIComponent(fields[0].slice(0, separator).replace(/\+/g, " "));
35
- value = decodeURIComponent(fields[0].slice(separator + 1).replace(/\+/g, " "));
36
- if (!key.startsWith("@")) throw new TypeError("Storage keys must start with @");
37
- parts = validatePathParts(key.slice(1).split("."));
38
- if (parts.length < 2) throw new TypeError("Specify a storage root and child path");
39
- } catch (error) {
40
- return reply(400, { error: error.message });
41
- }
42
- if (action === "set") {
43
- try {
44
- value = JSON.parse(value);
45
- } catch (error) {
46
- if (!(error instanceof SyntaxError)) throw error;
47
- }
48
- }
49
- const [storageKey, ...path] = parts;
50
- try {
51
- const root = Storage.getItem(storageKey, {});
52
- if (!isRecord(root)) throw new TypeError("Stored root must be an object");
53
- const parent = storageParent(root, path, action === "set");
54
- const key = path.at(-1);
55
- switch (action) {
56
- case "get": {
57
- const result = parent ? _.get(parent, [key]) : undefined;
58
- return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
59
- }
60
- case "set":
61
- _.set(parent, [key], value);
62
- break;
63
- case "delete":
64
- if (parent) _.unset(parent, [key]);
65
- break;
66
- }
67
- if (!Storage.setItem(storageKey, root)) throw new Error("Storage write failed");
68
- return reply(200, action === "set" ? { saved: true } : { deleted: true });
69
- } catch (error) {
70
- return reply(500, { error: error.message });
71
- }
72
- }
73
- }
74
-
75
- /**
76
- * 判断根节点是否为普通对象。
77
- * Determine whether a root node is a plain object.
78
- * @param {unknown} value 待检查值 / Value to inspect.
79
- * @returns {boolean} 是否为普通对象 / Whether this is a plain object.
80
- */
81
- function isRecord(value) {
82
- return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
83
- }
84
-
85
- /**
86
- * 遍历父路径,兼容旧存储中 JSON 字符串形式的中间节点。
87
- * Traverse parents, supporting legacy intermediate nodes serialized as JSON strings.
88
- * @param {Record<string, unknown>} root 存储根 / Storage root.
89
- * @param {string[]} parts 完整路径 / Complete path.
90
- * @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
91
- * @returns {object | undefined} 父节点,缺失且不创建时为 undefined / Parent, or undefined when absent and not creating.
92
- * @throws {TypeError} 无法继续遍历标量节点 / A scalar node cannot be traversed.
93
- */
94
- function storageParent(root, parts, create) {
95
- let parent = root;
96
- for (const part of parts.slice(0, -1)) {
97
- let next = _.get(parent, [part]);
98
- switch (typeof next) {
99
- case "undefined":
100
- if (!create) return;
101
- next = {};
102
- break;
103
- case "string":
104
- next = JSON.parse(next);
105
- break;
106
- default:
107
- break;
108
- }
109
- if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
110
- _.set(parent, [part], next);
111
- parent = next;
112
- }
113
- return parent;
114
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * 统一生成不可缓存的响应,HEAD 始终省略正文。
3
- * Create an uncached response, always omitting the body for HEAD.
4
- * @param {import("../index.js").SettingsRequest} request 宿主请求 / Host request.
5
- * @param {number} status HTTP 状态 / HTTP status.
6
- * @param {unknown} body JSON 数据或资源正文 / JSON data or resource body.
7
- * @param {string} [type] 媒体类型 / Media type.
8
- * @returns {import("../index.js").SettingsResponse} 通用响应 / Common response.
9
- */
10
- export function response(request, status, body, type = "application/json") {
11
- return {
12
- status,
13
- headers: { "Content-Type": `${type}; charset=utf-8`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
14
- body: request.method === "HEAD" ? "" : type === "application/json" ? JSON.stringify(body) : body,
15
- };
16
- }
@@ -1,39 +0,0 @@
1
- import { URL } from "@nsnanocat/url";
2
- import assets from "#assets";
3
- import { pageInputs } from "../lib/page-inputs.mjs";
4
- import { response } from "../lib/response.mjs";
5
- import { Store } from "../Store.mjs";
6
- import { complete } from "./response.mjs";
7
-
8
- /**
9
- * 通用代理入口:提供无鉴权 form 存储 API 及模块页面,不含业务配置。
10
- * Generic proxy entry serving unauthenticated form storage and module pages without business configuration.
11
- * @returns {Promise<void>} 已交给代理宿主的响应 / Response delivered to the proxy host.
12
- */
13
- async function run() {
14
- const request = globalThis.$request;
15
- let result;
16
- try {
17
- const url = new URL(request.url);
18
- switch (true) {
19
- case url.pathname.startsWith("/api/"):
20
- result = await new Store().handle(request, url);
21
- break;
22
- case /^\/settings\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname): {
23
- const inputs = encodeURIComponent(JSON.stringify(pageInputs(url, request.headers)));
24
- result = response(request, 200, assets.page.body.replace("</head>", `<meta name="preference-panes-inputs" content="${inputs}"></head>`), "text/html");
25
- break;
26
- }
27
- default: {
28
- const asset = assets[url.pathname];
29
- if (asset) result = response(request, 200, asset.body, asset.type);
30
- }
31
- }
32
- if (result && !url.pathname.startsWith("/api/") && !["GET", "HEAD"].includes(request.method)) result = response(request, 405, { error: "Method not allowed" });
33
- } catch (error) {
34
- console.error(`PreferencePanes: ${error.message}`);
35
- result = response(request, 500, { error: error.message });
36
- }
37
- complete(result);
38
- }
39
- run();
@@ -1,16 +0,0 @@
1
- import { $app } from "@nsnanocat/util/lib/app.mjs";
2
- import { done } from "@nsnanocat/util/lib/done.mjs";
3
-
4
- /**
5
- * 统一适配代理的完成格式;未接管的请求原样继续。
6
- * Adapt the host completion format and pass through unhandled requests.
7
- * @param {import("../index.js").SettingsResponse | undefined} result 通用响应 / Common response.
8
- * @returns {void} 响应已交给宿主 / Response delivered to the host.
9
- */
10
- export function complete(result) {
11
- if (!result) {
12
- done({});
13
- return;
14
- }
15
- done($app === "Quantumult X" ? result : { response: result });
16
- }