@nsnanocat/preference-panes 1.2.4 → 1.3.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/CHANGELOG.md +18 -0
- package/README.md +64 -7
- package/dist/home.html +1032 -0
- package/dist/module/index.html +2 -3
- package/dist/module/index.mjs +127 -59
- package/dist/module/navigation.mjs +21 -5
- package/dist/preference-panes.mjs +111 -58
- package/package.json +3 -2
- package/src/browser/ModuleFrame.mjs +21 -5
- package/src/browser/Navigation.d.mts +3 -3
- package/src/browser/boxjs.mjs +38 -8
- package/src/browser/client.d.mts +2 -2
- package/src/browser/client.mjs +51 -21
- package/src/browser/home.d.mts +54 -0
- package/src/browser/home.html +16 -0
- package/src/browser/home.mjs +441 -0
- package/src/browser/index.d.ts +2 -0
- package/src/browser/index.mjs +16 -1
- package/src/browser/module.html +1 -2
- package/src/browser/panel.mjs +22 -29
- package/src/browser/{panel.css → styles.css} +245 -3
- package/src/index.d.ts +8 -0
- package/dist/web.js +0 -1232
- package/src/web.mjs +0 -99
package/dist/module/index.html
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
2
|
+
<html lang="zh-CN" class="pp-page">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
7
|
|
|
8
8
|
<title>Module Preferences</title>
|
|
9
|
-
<style>body { margin: 0; }</style>
|
|
10
9
|
|
|
11
10
|
</head>
|
|
12
11
|
<body>
|
|
13
12
|
<main id="preferences"></main>
|
|
14
|
-
<script type="module" src="/settings/assets/index.mjs?v=1.
|
|
13
|
+
<script type="module" src="/settings/assets/index.mjs?v=1.3.0"></script>
|
|
15
14
|
</body>
|
|
16
15
|
</html>
|
package/dist/module/index.mjs
CHANGED
|
@@ -27,6 +27,7 @@ function normalizeBoxJs(config, module) {
|
|
|
27
27
|
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
28
28
|
for (const entry of app.settings) {
|
|
29
29
|
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
30
|
+
entry.id = entry.id.replace(/\[(0|[1-9]\d*)\]$/, ".$1");
|
|
30
31
|
if (!entry.id.startsWith("@")) {
|
|
31
32
|
if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
32
33
|
continue;
|
|
@@ -68,7 +69,7 @@ function normalizeBoxJs(config, module) {
|
|
|
68
69
|
};
|
|
69
70
|
if (type === "select" && !["string", "number", "boolean"].includes(field.type)) throw new TypeError(`Select requires a scalar val: ${entry.id}`);
|
|
70
71
|
if (entry.items) field.options = entry.items.map(item => ({ key: item.key, label: item.label }));
|
|
71
|
-
if (Object.
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
|
|
72
73
|
if (
|
|
73
74
|
typeof field.name !== "string" ||
|
|
74
75
|
(field.placeholder !== undefined && typeof field.placeholder !== "string") ||
|
|
@@ -78,18 +79,42 @@ function normalizeBoxJs(config, module) {
|
|
|
78
79
|
)
|
|
79
80
|
throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
|
|
80
81
|
if (field.options && (new Set(field.options.map(item => item.key)).size !== field.options.length || field.options.some(item => !scalar(item.key) || typeof item.label !== "string"))) throw new TypeError(`Invalid options: ${entry.id}`);
|
|
81
|
-
if (Object.
|
|
82
|
-
if (field.control === "url" && !Object.
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
|
|
83
|
+
if (field.control === "url" && !Object.prototype.hasOwnProperty.call(field, "defaultValue")) throw new TypeError(`URL BoxJS settings require a val: ${entry.id}`);
|
|
83
84
|
fields.push(field);
|
|
84
85
|
}
|
|
85
86
|
if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${target.module}`);
|
|
86
|
-
const
|
|
87
|
+
for (const field of fields) {
|
|
88
|
+
const indexed = /^(.*)\.(\d+)$/.exec(field.key);
|
|
89
|
+
if (!indexed) continue;
|
|
90
|
+
const siblings = fields.filter(candidate => candidate.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(candidate.key.slice(indexed[1].length + 1)));
|
|
91
|
+
if (fields.some(candidate => candidate.key.startsWith(`${indexed[1]}.`) && !siblings.includes(candidate))) throw new TypeError(`Indexed settings cannot mix array entries and named children: ${indexed[1]}`);
|
|
92
|
+
if (siblings.some(candidate => !Object.prototype.hasOwnProperty.call(candidate, "defaultValue")) || !siblings.some(candidate => candidate.key === `${indexed[1]}.${siblings.length - 1}`) || siblings.some(candidate => Number(candidate.key.slice(indexed[1].length + 1)) >= siblings.length))
|
|
93
|
+
throw new TypeError(`Indexed settings require contiguous indices and defaults: ${indexed[1]}`);
|
|
94
|
+
}
|
|
95
|
+
const owner = target.owners.size === 1 ? [...target.owners][0] : undefined;
|
|
96
|
+
const cachePath = owner?.cachePath ?? `@${target.storageKey}.${target.module}.Caches`;
|
|
97
|
+
if (typeof cachePath !== "string" || !cachePath.startsWith(`@${target.storageKey}.`)) throw new TypeError("cachePath must use the module storage root");
|
|
98
|
+
const cacheParts = validatePathParts(cachePath.slice(target.storageKey.length + 2).split("."));
|
|
99
|
+
if (cacheParts.length !== 2 || cacheParts[1] !== "Caches") throw new TypeError("cachePath must identify a module Caches subtree");
|
|
100
|
+
if (owner?.resetPaths !== undefined) {
|
|
101
|
+
if (!Array.isArray(owner.resetPaths) || !owner.resetPaths.length) throw new TypeError("resetPaths must be a nonempty array");
|
|
102
|
+
for (const path of owner.resetPaths) {
|
|
103
|
+
if (typeof path !== "string" || !path.startsWith(`@${target.storageKey}.${target.module}.Settings.`)) throw new TypeError("resetPaths must stay in this module's Settings");
|
|
104
|
+
validatePathParts(path.slice(target.storageKey.length + 2).split("."));
|
|
105
|
+
}
|
|
106
|
+
if (fields.some(field => !owner.resetPaths.some(path => `@${target.storageKey}.${field.key}` === path || `@${target.storageKey}.${field.key}`.startsWith(`${path}.`)))) throw new TypeError("resetPaths must cover the page settings");
|
|
107
|
+
}
|
|
108
|
+
const firstIndexed = /^(.*)\.(\d+)$/.exec(fields[0].key);
|
|
109
|
+
const common = (firstIndexed ? firstIndexed[1] : fields[0].key).split(".").slice(0, -1);
|
|
87
110
|
for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
|
|
88
111
|
return {
|
|
89
112
|
module: target.module,
|
|
90
113
|
storageKey: target.storageKey,
|
|
91
114
|
fields,
|
|
92
115
|
settingsPath: common,
|
|
116
|
+
...(owner?.cachePath === undefined ? {} : { cachePath }),
|
|
117
|
+
...(owner?.resetPaths === undefined ? {} : { resetPaths: [...owner.resetPaths] }),
|
|
93
118
|
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
94
119
|
};
|
|
95
120
|
}
|
|
@@ -179,9 +204,14 @@ function scalar(value) {
|
|
|
179
204
|
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
180
205
|
*/
|
|
181
206
|
function validValueShape(field, value) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
207
|
+
switch (field.type) {
|
|
208
|
+
case "array":
|
|
209
|
+
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
if (typeof value !== field.type || !scalar(value)) return false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
185
215
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
186
216
|
return true;
|
|
187
217
|
}
|
|
@@ -211,7 +241,7 @@ function resolveStoredValue(field, stored) {
|
|
|
211
241
|
if (value === undefined) return {};
|
|
212
242
|
if (!validValueShape(field, value)) {
|
|
213
243
|
return {
|
|
214
|
-
...(Object.
|
|
244
|
+
...(Object.prototype.hasOwnProperty.call(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
215
245
|
warning: { kind: "invalid-value", values: [stored] },
|
|
216
246
|
};
|
|
217
247
|
}
|
|
@@ -497,6 +527,7 @@ class PreferencesClient {
|
|
|
497
527
|
#values = {};
|
|
498
528
|
#warnings = {};
|
|
499
529
|
#saving = false;
|
|
530
|
+
#arrays = new Map();
|
|
500
531
|
|
|
501
532
|
/**
|
|
502
533
|
* 创建从 BoxJS 定义读取和持久化设置的页面客户端。
|
|
@@ -523,13 +554,22 @@ class PreferencesClient {
|
|
|
523
554
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
524
555
|
const values = {};
|
|
525
556
|
const warnings = {};
|
|
557
|
+
this.#arrays.clear();
|
|
526
558
|
for (const field of this.#definition.fields) {
|
|
559
|
+
const indexed = /^(.*)\.(\d+)$/.exec(field.key);
|
|
560
|
+
if (indexed && !this.#arrays.has(indexed[1])) {
|
|
561
|
+
const storedArray = indexed[1]
|
|
562
|
+
.split(".")
|
|
563
|
+
.slice(this.#definition.settingsPath.length)
|
|
564
|
+
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
565
|
+
this.#arrays.set(indexed[1], Array.isArray(storedArray) ? [...storedArray] : []);
|
|
566
|
+
}
|
|
527
567
|
const stored = field.key
|
|
528
568
|
.split(".")
|
|
529
569
|
.slice(this.#definition.settingsPath.length)
|
|
530
570
|
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
531
571
|
const resolved = resolveStoredValue(field, stored);
|
|
532
|
-
if (Object.
|
|
572
|
+
if (Object.prototype.hasOwnProperty.call(resolved, "value")) values[field.key] = resolved.value;
|
|
533
573
|
if (resolved.warning) warnings[field.key] = resolved.warning;
|
|
534
574
|
}
|
|
535
575
|
this.#values = values;
|
|
@@ -543,7 +583,7 @@ class PreferencesClient {
|
|
|
543
583
|
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
544
584
|
*/
|
|
545
585
|
snapshot() {
|
|
546
|
-
return
|
|
586
|
+
return JSON.parse(JSON.stringify({ definition: this.#definition, values: this.#values, warnings: this.#warnings }));
|
|
547
587
|
}
|
|
548
588
|
|
|
549
589
|
/**
|
|
@@ -562,22 +602,23 @@ class PreferencesClient {
|
|
|
562
602
|
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
563
603
|
*/
|
|
564
604
|
async readCaches() {
|
|
565
|
-
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
605
|
+
const response = await this.#send("get", this.#definition.cachePath ?? `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
566
606
|
return response.status === 404 ? undefined : response.json();
|
|
567
607
|
}
|
|
568
608
|
|
|
569
609
|
/**
|
|
570
|
-
*
|
|
571
|
-
* Delete the current module Caches
|
|
610
|
+
* 删除声明的缓存子树,默认当前模块 Caches。
|
|
611
|
+
* Delete the declared cache subtree, defaulting to the current module Caches.
|
|
572
612
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
573
613
|
*/
|
|
574
614
|
clearCaches() {
|
|
575
|
-
|
|
615
|
+
const path = this.#definition.cachePath?.slice(this.#definition.storageKey.length + 2) ?? `${this.#module}.Caches`;
|
|
616
|
+
return this.#change("delete", path, undefined, "clearCaches");
|
|
576
617
|
}
|
|
577
618
|
|
|
578
619
|
/**
|
|
579
|
-
*
|
|
580
|
-
* Delete current module data and restore page defaults.
|
|
620
|
+
* 删除声明的设置子树,默认删除当前模块数据,并恢复页面默认值。
|
|
621
|
+
* Delete declared settings subtrees, defaulting to current module data, and restore page defaults.
|
|
581
622
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
582
623
|
*/
|
|
583
624
|
reset() {
|
|
@@ -658,30 +699,49 @@ class PreferencesClient {
|
|
|
658
699
|
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
659
700
|
this.#saving = true;
|
|
660
701
|
try {
|
|
661
|
-
let field;
|
|
662
|
-
if (operation === "write" || operation === "delete") {
|
|
663
|
-
field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
664
|
-
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
665
|
-
}
|
|
666
|
-
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
667
702
|
switch (operation) {
|
|
668
703
|
case "write":
|
|
669
|
-
this.#values[key] = structuredClone(value);
|
|
670
|
-
delete this.#warnings[key];
|
|
671
|
-
break;
|
|
672
704
|
case "delete": {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
705
|
+
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
706
|
+
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
707
|
+
const indexed = /^(.*)\.(\d+)$/.exec(key);
|
|
708
|
+
if (indexed) {
|
|
709
|
+
const array = [...(this.#arrays.get(indexed[1]) ?? [])];
|
|
710
|
+
for (const sibling of this.#definition.fields) {
|
|
711
|
+
if (sibling.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(sibling.key.slice(indexed[1].length + 1))) {
|
|
712
|
+
const index = Number(sibling.key.slice(indexed[1].length + 1));
|
|
713
|
+
if (array[index] === undefined) array[index] = this.#values[sibling.key] ?? sibling.defaultValue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
array[Number(indexed[2])] = operation === "delete" ? field.defaultValue : value;
|
|
717
|
+
await this.#send("set", `@${this.#definition.storageKey}.${indexed[1]}`, array);
|
|
718
|
+
this.#arrays.set(indexed[1], array);
|
|
719
|
+
} else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
720
|
+
switch (operation) {
|
|
721
|
+
case "write":
|
|
722
|
+
this.#values[key] = JSON.parse(JSON.stringify(value));
|
|
723
|
+
delete this.#warnings[key];
|
|
724
|
+
break;
|
|
725
|
+
case "delete":
|
|
726
|
+
delete this.#values[key];
|
|
727
|
+
delete this.#warnings[key];
|
|
728
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[key] = JSON.parse(JSON.stringify(field.defaultValue));
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
676
731
|
break;
|
|
677
732
|
}
|
|
678
733
|
case "clearCaches":
|
|
734
|
+
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
679
735
|
break;
|
|
680
736
|
case "reset":
|
|
737
|
+
if (this.#definition.resetPaths) {
|
|
738
|
+
for (const path of this.#definition.resetPaths) await this.#send("delete", path);
|
|
739
|
+
} else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
740
|
+
this.#arrays.clear();
|
|
681
741
|
this.#warnings = {};
|
|
682
742
|
for (const field of this.#definition.fields) {
|
|
683
743
|
delete this.#values[field.key];
|
|
684
|
-
if (Object.
|
|
744
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[field.key] = JSON.parse(JSON.stringify(field.defaultValue));
|
|
685
745
|
}
|
|
686
746
|
break;
|
|
687
747
|
}
|
|
@@ -962,17 +1022,17 @@ class PreferencesPanel {
|
|
|
962
1022
|
const notify = event => {
|
|
963
1023
|
if (destroyed) return;
|
|
964
1024
|
let message;
|
|
965
|
-
switch (
|
|
966
|
-
case
|
|
1025
|
+
switch (event.kind === "error" ? "error" : event.operation) {
|
|
1026
|
+
case "error":
|
|
967
1027
|
message = `操作失败:${event.message}`;
|
|
968
1028
|
break;
|
|
969
|
-
case
|
|
1029
|
+
case "delete":
|
|
970
1030
|
message = "删除成功";
|
|
971
1031
|
break;
|
|
972
|
-
case
|
|
1032
|
+
case "clearCaches":
|
|
973
1033
|
message = "Caches 已清空";
|
|
974
1034
|
break;
|
|
975
|
-
case
|
|
1035
|
+
case "reset":
|
|
976
1036
|
message = "设置已重置";
|
|
977
1037
|
break;
|
|
978
1038
|
default:
|
|
@@ -1095,21 +1155,6 @@ class PreferencesPanel {
|
|
|
1095
1155
|
});
|
|
1096
1156
|
return queue;
|
|
1097
1157
|
}
|
|
1098
|
-
const metadata = definition.metadata;
|
|
1099
|
-
if (metadata) {
|
|
1100
|
-
const info = element("div", "pp-module-info");
|
|
1101
|
-
const details = element("div", "pp-module-details");
|
|
1102
|
-
for (const description of [metadata.author, metadata.desc ?? metadata.description, ...(metadata.descs ?? [])]) if (description) details.append(element("p", "pp-description", description));
|
|
1103
|
-
if (metadata.repo) {
|
|
1104
|
-
const link = element("a", "pp-module-source", "项目主页");
|
|
1105
|
-
link.href = resourceURL(metadata.repo);
|
|
1106
|
-
link.target = "_blank";
|
|
1107
|
-
link.rel = "noopener noreferrer";
|
|
1108
|
-
details.append(link);
|
|
1109
|
-
}
|
|
1110
|
-
info.append(details);
|
|
1111
|
-
view.append(info);
|
|
1112
|
-
}
|
|
1113
1158
|
for (const field of definition.fields) {
|
|
1114
1159
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
|
|
1115
1160
|
const group = match?.[1] ?? "通用";
|
|
@@ -1150,8 +1195,8 @@ class PreferencesPanel {
|
|
|
1150
1195
|
let write;
|
|
1151
1196
|
let inputContainer = row;
|
|
1152
1197
|
let eventName = "change";
|
|
1153
|
-
switch (
|
|
1154
|
-
case
|
|
1198
|
+
switch (field.control === "url" ? "url" : field.options ? (field.type === "array" ? "checkboxes" : "selects") : field.type) {
|
|
1199
|
+
case "url": {
|
|
1155
1200
|
const link = element("a", "pp-choice-link", "打开");
|
|
1156
1201
|
link.setAttribute("aria-label", field.name);
|
|
1157
1202
|
link.rel = "noopener noreferrer";
|
|
@@ -1167,7 +1212,7 @@ class PreferencesPanel {
|
|
|
1167
1212
|
});
|
|
1168
1213
|
break;
|
|
1169
1214
|
}
|
|
1170
|
-
case
|
|
1215
|
+
case "selects": {
|
|
1171
1216
|
const select = element("select", "");
|
|
1172
1217
|
select.setAttribute("aria-label", field.name);
|
|
1173
1218
|
field.options.forEach((option, index) => {
|
|
@@ -1182,7 +1227,7 @@ class PreferencesPanel {
|
|
|
1182
1227
|
read = () => field.options[select.selectedIndex]?.key;
|
|
1183
1228
|
break;
|
|
1184
1229
|
}
|
|
1185
|
-
case
|
|
1230
|
+
case "checkboxes": {
|
|
1186
1231
|
const page = element("section", "pp-choice-page");
|
|
1187
1232
|
if (field.description) page.append(element("p", "pp-description", field.description));
|
|
1188
1233
|
const choices = element("div", "pp-rows");
|
|
@@ -1226,7 +1271,7 @@ class PreferencesPanel {
|
|
|
1226
1271
|
};
|
|
1227
1272
|
break;
|
|
1228
1273
|
}
|
|
1229
|
-
case
|
|
1274
|
+
case "boolean": {
|
|
1230
1275
|
const toggle = element("input", "pp-switch");
|
|
1231
1276
|
toggle.type = "checkbox";
|
|
1232
1277
|
toggle.setAttribute("switch", "");
|
|
@@ -1264,7 +1309,12 @@ class PreferencesPanel {
|
|
|
1264
1309
|
growingInputs.push(grow);
|
|
1265
1310
|
}
|
|
1266
1311
|
eventName = "input";
|
|
1267
|
-
if (!multiline)
|
|
1312
|
+
if (!multiline) {
|
|
1313
|
+
input.type = field.type === "number" ? "number" : "text";
|
|
1314
|
+
input.autocapitalize = "off";
|
|
1315
|
+
input.setAttribute("autocorrect", "off");
|
|
1316
|
+
input.spellcheck = false;
|
|
1317
|
+
}
|
|
1268
1318
|
write = value => {
|
|
1269
1319
|
input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
|
|
1270
1320
|
grow();
|
|
@@ -1366,7 +1416,9 @@ class PreferencesPanel {
|
|
|
1366
1416
|
});
|
|
1367
1417
|
handlers.set("clearCaches", async () => {
|
|
1368
1418
|
if (saving) return;
|
|
1369
|
-
|
|
1419
|
+
const shared = definition.cachePath && definition.cachePath !== `@${definition.storageKey}.${active}.Caches`;
|
|
1420
|
+
const message = shared ? `清空共享缓存 ${definition.cachePath}?使用该缓存的其它模块也会受影响。` : `清空 ${active} 的全部 Caches?`;
|
|
1421
|
+
if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
|
|
1370
1422
|
return perform(
|
|
1371
1423
|
() => client.clearCaches(),
|
|
1372
1424
|
() => {
|
|
@@ -1376,7 +1428,8 @@ class PreferencesPanel {
|
|
|
1376
1428
|
});
|
|
1377
1429
|
handlers.set("reset", async () => {
|
|
1378
1430
|
if (saving) return;
|
|
1379
|
-
|
|
1431
|
+
const message = definition.resetPaths ? `重置 ${definition.metadata?.name ?? active} 的设置?只删除本页面的设置,保留共享存储中的其它设置和缓存。` : `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`;
|
|
1432
|
+
if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
|
|
1380
1433
|
return perform(() => client.reset(), controls);
|
|
1381
1434
|
});
|
|
1382
1435
|
navigation?.destroy();
|
|
@@ -1419,7 +1472,7 @@ class PreferencesPanel {
|
|
|
1419
1472
|
}
|
|
1420
1473
|
}
|
|
1421
1474
|
|
|
1422
|
-
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1475
|
+
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n:root {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n --pp-host-height: 0px;\n --pp-keyboard-height: 0px;\n}\n.pp-panel {\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n font-size: 16px;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid var(--pp-border, #8884);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n\n/* 首页沿用表单的分组与说明样式,入口按屏幕宽度自动换行。\n * The landing page reuses form groups and descriptions, with entries wrapping by viewport width. */\n:root.pp-page {\n color-scheme: light dark;\n}\n.pp-page body {\n margin: 0;\n font:\n 16px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n background: var(--pp-background, Canvas);\n color: var(--pp-text, CanvasText);\n}\n[data-preference-panes-pages] {\n position: relative;\n min-height: calc(100vh - var(--pp-host-height));\n background: inherit;\n}\n[data-preference-panes-module-page],\n[data-preference-panes-link-page] {\n position: fixed;\n inset: var(--pp-host-height) 0 0;\n background: inherit;\n z-index: 1;\n box-shadow: -8px 0 24px #00000014;\n}\n[data-preference-panes-module-page] iframe,\n[data-preference-panes-link-page] iframe {\n display: block;\n border: 0;\n width: 100%;\n height: 100%;\n background: inherit;\n}\n[data-module-message] {\n position: absolute;\n inset: 0;\n margin: 0;\n padding: 20px;\n background: inherit;\n}\n[data-module-message][hidden] {\n display: none;\n}\n.pp-host-header {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: 44px minmax(0, 1fr) 44px;\n align-items: center;\n min-height: 44px;\n padding: env(safe-area-inset-top) max(12px, env(safe-area-inset-right)) 0 max(12px, env(safe-area-inset-left));\n border-bottom: 1px solid var(--pp-border, #8884);\n background: var(--pp-surface, Canvas);\n color: var(--pp-accent, Highlight);\n font-size: 17px;\n}\n.pp-host-title {\n text-align: center;\n color: var(--pp-text, CanvasText);\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-host-back {\n border: 0;\n background: none;\n color: inherit;\n width: 44px;\n height: 44px;\n padding: 8px;\n font:\n 32px / 1 -apple-system,\n sans-serif;\n cursor: pointer;\n}\n.pp-host-back[hidden] {\n visibility: hidden;\n display: block;\n}\n.pp-host-toast {\n position: fixed;\n z-index: 3;\n bottom: calc(24px + env(safe-area-inset-bottom) + var(--pp-keyboard-height));\n left: 50%;\n transform: translateX(-50%);\n max-width: calc(100% - 48px);\n padding: 10px 18px;\n border-radius: 12px;\n background: #222e;\n color: #fff;\n font-size: 14px;\n text-align: center;\n}\n.pp-host-toast[hidden] {\n display: none;\n}\n.pp-host-error {\n margin: 24px;\n padding: 16px;\n border: 1px solid var(--pp-danger, #e45656);\n border-radius: 12px;\n font-size: 16px;\n overflow-wrap: anywhere;\n}\n\n.pp-home {\n max-width: 1000px;\n margin: auto;\n padding: 24px max(16px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));\n}\n.pp-home-logo {\n display: block;\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n object-fit: contain;\n}\n.pp-home-entries {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-around;\n row-gap: 16px;\n padding: 16px 0;\n background: var(--pp-surface);\n}\n.pp-home-item {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 25%;\n min-width: 0;\n padding: 0 4px;\n border: 0;\n background: none;\n color: var(--pp-text);\n font:\n 12px / 1.5 -apple-system,\n sans-serif;\n cursor: pointer;\n text-decoration: none;\n}\n@media (min-width: 500px) {\n .pp-home-item {\n width: calc(100% / 6);\n }\n}\n@media (min-width: 700px) {\n .pp-home-item {\n width: 12.5%;\n }\n}\n.pp-home-icon {\n box-sizing: border-box;\n width: 56px;\n height: 56px;\n padding: 8px;\n border-radius: 50%;\n background: var(--pp-field);\n font-size: 32px;\n line-height: 40px;\n}\n.pp-home-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.pp-home-name {\n margin-top: 8px;\n}\n.pp-home-content {\n min-width: 0;\n width: 100%;\n}\n.pp-home-content > span {\n display: block;\n}\n.pp-home-name,\n.pp-home-item [data-module-status] {\n width: 100%;\n overflow: hidden;\n text-align: center;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-home-item [data-module-status] {\n min-height: 20px;\n}\n.pp-home-item:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.pp-home-entries[data-layout=\"list\"] {\n display: block;\n padding: 0;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item {\n flex-direction: row;\n gap: 12px;\n width: 100%;\n padding: 12px 16px;\n font-size: 15px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item + .pp-home-item {\n border-top: 1px solid var(--pp-border);\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-icon {\n flex: none;\n width: 40px;\n height: 40px;\n padding: 4px;\n font-size: 24px;\n line-height: 32px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-name {\n margin-top: 0;\n text-align: left;\n}\n.pp-home-arrow {\n color: var(--pp-muted);\n font-size: 24px;\n}\n.pp-home-footer {\n margin-top: 16px;\n text-align: center;\n white-space: normal;\n}\n.pp-home-footer p {\n margin: 8px 0 0;\n}\n";
|
|
1423
1476
|
|
|
1424
1477
|
const selector = "style[data-preference-panes-defaults]";
|
|
1425
1478
|
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|
|
@@ -1574,7 +1627,22 @@ class ModulePage {
|
|
|
1574
1627
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
|
|
1575
1628
|
const module = embedded ?? match?.[1];
|
|
1576
1629
|
if (!module) throw new TypeError("Open a concrete module URL");
|
|
1577
|
-
const
|
|
1630
|
+
const frame = this.#window.frameElement?.dataset;
|
|
1631
|
+
const base = frame?.preferencePanesBase ?? this.#window.location.href;
|
|
1632
|
+
this.#window.document.querySelector("link[data-preference-panes-stylesheet]")?.remove();
|
|
1633
|
+
const stylesheetSource = frame?.preferencePanesStylesheet ?? new URLSearchParams(this.#window.location.search).get("css")?.trim();
|
|
1634
|
+
if (stylesheetSource) {
|
|
1635
|
+
const stylesheet = new URL(stylesheetSource, base);
|
|
1636
|
+
if (!["http:", "https:"].includes(stylesheet.protocol)) throw new TypeError("CSS resource must use HTTP(S)");
|
|
1637
|
+
const link = this.#window.document.createElement("link");
|
|
1638
|
+
link.dataset.preferencePanesStylesheet = "true";
|
|
1639
|
+
link.rel = "stylesheet";
|
|
1640
|
+
link.href = stylesheet.href;
|
|
1641
|
+
this.#window.document.head.append(link);
|
|
1642
|
+
}
|
|
1643
|
+
const jsonSource = frame?.preferencePanesJson ?? new URLSearchParams(this.#window.location.search).get("json")?.trim();
|
|
1644
|
+
const source = new URL(jsonSource || `/api/${encodeURIComponent(module)}`, base);
|
|
1645
|
+
if (!["http:", "https:"].includes(source.protocol)) throw new TypeError("BoxJS resource must use HTTP(S)");
|
|
1578
1646
|
const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
|
|
1579
1647
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
1580
1648
|
const boxjs = await response.json();
|
|
@@ -147,8 +147,8 @@ class ActionMenu {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
* Module document container:
|
|
150
|
+
* 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
|
|
151
|
+
* Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
|
|
152
152
|
*/
|
|
153
153
|
class ModuleFrame extends EventTarget {
|
|
154
154
|
#url;
|
|
@@ -178,11 +178,27 @@ class ModuleFrame extends EventTarget {
|
|
|
178
178
|
this.#url = new URL(url, document.baseURI);
|
|
179
179
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
180
180
|
if (!match) throw new TypeError("Open a concrete module URL");
|
|
181
|
-
this.#options = { signal: options.signal
|
|
181
|
+
this.#options = { signal: options.signal };
|
|
182
182
|
this.element = document.createElement("iframe");
|
|
183
183
|
this.element.title = `${match[1]} 设置`;
|
|
184
184
|
this.element.dataset.preferencePanes = "true";
|
|
185
185
|
this.element.dataset.preferencePanesModule = match[1];
|
|
186
|
+
this.element.dataset.preferencePanesBase = this.#url.href;
|
|
187
|
+
const headers = new Headers(options.headers);
|
|
188
|
+
for (const [header, property, kind] of [
|
|
189
|
+
["X-PreferencePanes-JSON", "preferencePanesJson", "BoxJS"],
|
|
190
|
+
["X-PreferencePanes-CSS", "preferencePanesStylesheet", "CSS"],
|
|
191
|
+
]) {
|
|
192
|
+
if (!headers.has(header)) continue;
|
|
193
|
+
const source = headers.get(header)?.trim();
|
|
194
|
+
if (!source) {
|
|
195
|
+
if (kind === "BoxJS") throw new TypeError("BoxJS resource URL is required");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const resource = new URL(source, this.#url);
|
|
199
|
+
if (!["http:", "https:"].includes(resource.protocol)) throw new TypeError(`${kind} resource must use HTTP(S)`);
|
|
200
|
+
this.element.dataset[property] = resource.href;
|
|
201
|
+
}
|
|
186
202
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
187
203
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
188
204
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
@@ -208,10 +224,10 @@ class ModuleFrame extends EventTarget {
|
|
|
208
224
|
if (this.#options.signal?.aborted) this.destroy();
|
|
209
225
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
210
226
|
try {
|
|
211
|
-
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit",
|
|
227
|
+
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
212
228
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
213
229
|
const html = await response.text();
|
|
214
|
-
this.#controller.signal.
|
|
230
|
+
if (this.#controller.signal.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
215
231
|
this.element.srcdoc = html;
|
|
216
232
|
} finally {
|
|
217
233
|
clearTimeout(timer);
|