@nsnanocat/preference-panes 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -76
- package/dist/api.js +1622 -0
- package/dist/module/app.mjs +169 -52
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +218 -4
- package/dist/preference-panes.mjs +168 -51
- package/package.json +1 -1
- package/src/Store.mjs +40 -53
- package/src/browser/ActionMenu.mjs +115 -0
- package/src/browser/ModuleFrame.mjs +13 -2
- package/src/browser/ModuleStatus.mjs +86 -0
- package/src/browser/Navigation.d.mts +99 -1
- package/src/browser/Navigation.mjs +2 -0
- package/src/browser/client.d.mts +2 -2
- package/src/browser/client.mjs +21 -21
- package/src/browser/panel.css +2 -15
- package/src/browser/panel.mjs +31 -21
- package/src/build.mjs +3 -13
- package/src/index.d.ts +6 -6
- package/src/lib/page-inputs.mjs +1 -1
- package/src/lib/settings-path.mjs +0 -18
- package/src/proxy/handler.mjs +9 -22
- package/dist/preference-panes.config.js +0 -842
- package/dist/preference-panes.proxy.js +0 -1754
- package/src/proxy/config.mjs +0 -14
package/dist/module/app.mjs
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 解析已经取得的 pathname,避免重复构造 URL。
|
|
3
|
-
* Parse an existing pathname without constructing another URL.
|
|
4
|
-
* @param {string} pathname 以 / 开头的 URL pathname / URL pathname beginning with /.
|
|
5
|
-
* @returns {string[] | undefined} 解码后的路径,非 API 路径不处理 / Decoded path, or undefined outside /api/.
|
|
6
|
-
* @throws {TypeError} 转义编码或路径片段非法 / Invalid percent encoding or path segments.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
/**
|
|
10
2
|
* 校验原始路径片段,不进行 URL 编码转换。
|
|
11
3
|
* Validate raw path segments without URL encoding conversion.
|
|
@@ -116,7 +108,7 @@ function pageInputs(url, headers = {}) {
|
|
|
116
108
|
const module = match[1];
|
|
117
109
|
const values = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
118
110
|
const json = values["x-preferencepanes-json"] ?? url.searchParams.get("json") ?? `/configs/${module}`;
|
|
119
|
-
const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ??
|
|
111
|
+
const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ?? "";
|
|
120
112
|
if (!json.trim()) throw new TypeError("JSON resource URL is required");
|
|
121
113
|
return { url: url.href, module, json, css };
|
|
122
114
|
}
|
|
@@ -181,7 +173,123 @@ function errorView(error, retry) {
|
|
|
181
173
|
return view;
|
|
182
174
|
}
|
|
183
175
|
|
|
184
|
-
var defaults = "/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n --pp-muted: #9499a0;\n --pp-accent: #fb7299;\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 min-height: 100vh;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\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: sticky;\n top: 0;\n z-index: 1;\n}\n.pp-title {\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-brand {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n text-align: center;\n}\n.pp-brand-icon {\n display: none;\n flex: none;\n width: 28px;\n height: 28px;\n}\n.pp-brand-icon:not(:empty) {\n display: block;\n}\n.pp-brand-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\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 height: calc(100vh - 52px - env(safe-area-inset-top));\n overflow: hidden;\n}\n:root[data-preference-panes-embedded] .pp-header {\n display: none;\n}\n:root[data-preference-panes-embedded] .pp-viewport {\n height: 100vh;\n}\n@supports (height: 100dvh) {\n .pp-viewport {\n height: calc(100dvh - 52px - env(safe-area-inset-top));\n }\n :root[data-preference-panes-embedded] .pp-viewport {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page {\n position: absolute;\n inset: 0;\n overflow: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));\n background: var(--pp-background);\n}\n.pp-panel .form-group {\n margin: 0 0 12px;\n}\n.pp-panel .form-group__title {\n font-size: 12px;\n line-height: 17px;\n font-weight: 400;\n color: var(--pp-muted);\n padding-left: 12px;\n margin: 12px 0 6px;\n}\n.pp-panel .form-group__row {\n border-radius: 8px;\n overflow: hidden;\n background: var(--pp-surface);\n}\n.pp-panel .form-row {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 46px;\n padding: 12px;\n border: 0;\n border-bottom: 1px solid var(--pp-border);\n background: var(--pp-surface);\n gap: 12px;\n}\n.pp-panel .form-row:last-child {\n border-bottom: 0;\n}\n.pp-panel .form-row__text {\n flex: 1;\n min-width: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.pp-panel .form-row__title {\n font-size: 15px;\n line-height: 22px;\n color: var(--pp-text);\n text-align: left;\n}\n.pp-panel .form-row__subtitle {\n font-size: 12px;\n line-height: 18px;\n color: var(--pp-muted);\n overflow-wrap: anywhere;\n margin-top: 2px;\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-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-input {\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-surface);\n border: 1px solid var(--pp-border);\n border-radius: 6px;\n padding: 8px;\n min-width: 0;\n max-width: 45%;\n width: 45%;\n}\nselect.pp-input {\n text-overflow: ellipsis;\n font-size: 13px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-input {\n max-width: 100%;\n width: 100%;\n margin-top: 10px;\n}\n.pp-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n flex: none;\n width: 32px;\n height: 20px;\n max-width: none;\n border: 0;\n border-radius: 15px;\n padding: 0;\n background: #c9ccd0;\n cursor: pointer;\n transition: background 0.2s;\n}\n.pp-switch::before {\n content: \"\";\n position: absolute;\n top: 3px;\n left: 3px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: white;\n transition: transform 0.2s;\n}\n.pp-switch:checked {\n background: var(--pp-accent);\n}\n.pp-switch:checked::before {\n transform: translateX(12px);\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-icon {\n width: 48px;\n height: 48px;\n object-fit: contain;\n flex: none;\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-
|
|
176
|
+
var defaults = "/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n --pp-muted: #9499a0;\n --pp-accent: #fb7299;\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 min-height: 100vh;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\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: sticky;\n top: 0;\n z-index: 1;\n}\n.pp-title {\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-brand {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n text-align: center;\n}\n.pp-brand-icon {\n display: none;\n flex: none;\n width: 28px;\n height: 28px;\n}\n.pp-brand-icon:not(:empty) {\n display: block;\n}\n.pp-brand-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\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 height: calc(100vh - 52px - env(safe-area-inset-top));\n overflow: hidden;\n}\n:root[data-preference-panes-embedded] .pp-header {\n display: none;\n}\n:root[data-preference-panes-embedded] .pp-viewport {\n height: 100vh;\n}\n@supports (height: 100dvh) {\n .pp-viewport {\n height: calc(100dvh - 52px - env(safe-area-inset-top));\n }\n :root[data-preference-panes-embedded] .pp-viewport {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n overflow: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));\n background: var(--pp-background);\n}\n.pp-panel .form-group {\n margin: 0 0 12px;\n}\n.pp-panel .form-group__title {\n font-size: 12px;\n line-height: 17px;\n font-weight: 400;\n color: var(--pp-muted);\n padding-left: 12px;\n margin: 12px 0 6px;\n}\n.pp-panel .form-group__row {\n border-radius: 8px;\n overflow: hidden;\n background: var(--pp-surface);\n}\n.pp-panel .form-row {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 46px;\n padding: 12px;\n border: 0;\n border-bottom: 1px solid var(--pp-border);\n background: var(--pp-surface);\n gap: 12px;\n}\n.pp-panel .form-row:last-child {\n border-bottom: 0;\n}\n.pp-panel .form-row__text {\n flex: 1;\n min-width: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.pp-panel .form-row__title {\n font-size: 15px;\n line-height: 22px;\n color: var(--pp-text);\n text-align: left;\n}\n.pp-panel .form-row__subtitle {\n font-size: 12px;\n line-height: 18px;\n color: var(--pp-muted);\n overflow-wrap: anywhere;\n margin-top: 2px;\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-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-input {\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-surface);\n border: 1px solid var(--pp-border);\n border-radius: 6px;\n padding: 8px;\n min-width: 0;\n max-width: 45%;\n width: 45%;\n}\nselect.pp-input {\n text-overflow: ellipsis;\n font-size: 13px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-input {\n max-width: 100%;\n width: 100%;\n margin-top: 10px;\n}\n.pp-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n flex: none;\n width: 32px;\n height: 20px;\n max-width: none;\n border: 0;\n border-radius: 15px;\n padding: 0;\n background: #c9ccd0;\n cursor: pointer;\n transition: background 0.2s;\n}\n.pp-switch::before {\n content: \"\";\n position: absolute;\n top: 3px;\n left: 3px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: white;\n transition: transform 0.2s;\n}\n.pp-switch:checked {\n background: var(--pp-accent);\n}\n.pp-switch:checked::before {\n transform: translateX(12px);\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-icon {\n width: 48px;\n height: 48px;\n object-fit: contain;\n flex: none;\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-error button {\n min-height: 44px;\n padding: 8px 12px;\n border-radius: 6px;\n background: var(--pp-surface);\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@media (prefers-color-scheme: dark) {\n .pp-panel {\n --pp-text: #e3e5e7;\n --pp-background: #17181a;\n --pp-surface: #232427;\n --pp-border: #343538;\n }\n}\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #e3e5e7;\n --pp-background: #17181a;\n --pp-surface: #232427;\n --pp-border: #343538;\n}\n:root[data-theme=\"light\"] .pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n}\n@media (prefers-reduced-motion: reduce) {\n .pp-panel .pp-switch,\n .pp-panel .pp-switch::before {\n transition: none;\n }\n}\n";
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 标题栏共用三点菜单;Shadow DOM 隔离项目样式,保留继承的主题色。
|
|
180
|
+
* Shared title-bar overflow menu; Shadow DOM isolates layout while inheriting theme colors.
|
|
181
|
+
*/
|
|
182
|
+
class ActionMenu {
|
|
183
|
+
#button;
|
|
184
|
+
#popup;
|
|
185
|
+
#backdrop;
|
|
186
|
+
#select;
|
|
187
|
+
#document;
|
|
188
|
+
#key = event => {
|
|
189
|
+
if (event.key === "Escape" && !this.#popup.hidden) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
this.close();
|
|
192
|
+
this.#button.focus();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 创建菜单,操作逻辑由调用方提供。
|
|
198
|
+
* Create a menu whose actions are handled by the caller.
|
|
199
|
+
* @param {(id: string) => void} select 菜单选择回调 / Selection callback.
|
|
200
|
+
*/
|
|
201
|
+
constructor(select) {
|
|
202
|
+
this.#document = document;
|
|
203
|
+
this.#select = select;
|
|
204
|
+
this.element = document.createElement("span");
|
|
205
|
+
const root = this.element.attachShadow({ mode: "open" });
|
|
206
|
+
root.innerHTML = `<style>
|
|
207
|
+
:host{display:inline-flex;position:relative;width:44px;height:44px;color:inherit}
|
|
208
|
+
:host([hidden]),[hidden]{display:none!important}
|
|
209
|
+
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
210
|
+
button:disabled{opacity:.4;cursor:default}
|
|
211
|
+
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
212
|
+
#trigger{width:44px;height:44px;padding:10px;position:relative;z-index:3}
|
|
213
|
+
svg{display:block;width:24px;height:24px;fill:currentColor}
|
|
214
|
+
#backdrop{position:fixed;inset:0;z-index:1}
|
|
215
|
+
#items{position:absolute;right:0;top:46px;z-index:2;min-width:160px;padding:6px;background:var(--pp-surface,Canvas);color:var(--pp-text,CanvasText);border:1px solid var(--pp-border,#8884);border-radius:12px;box-shadow:0 8px 28px #0003}
|
|
216
|
+
#items button{display:block;text-align:left;white-space:nowrap;width:100%;padding:11px 14px;border-radius:8px;font:14px/1.4 system-ui,sans-serif}
|
|
217
|
+
#items button:hover{background:#8882}
|
|
218
|
+
#items button[data-danger]{color:#e45656}
|
|
219
|
+
</style><button id="trigger" type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false" aria-controls="items"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="4" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="20" cy="12" r="2"/></svg></button><button id="backdrop" type="button" tabindex="-1" aria-label="关闭菜单" hidden></button><div id="items" role="menu" hidden></div>`;
|
|
220
|
+
this.#button = root.querySelector("#trigger");
|
|
221
|
+
this.#popup = root.querySelector("#items");
|
|
222
|
+
this.#backdrop = root.querySelector("#backdrop");
|
|
223
|
+
this.#button.onclick = () => {
|
|
224
|
+
const open = this.#popup.hidden;
|
|
225
|
+
this.#popup.hidden = this.#backdrop.hidden = !open;
|
|
226
|
+
this.#button.setAttribute("aria-expanded", String(open));
|
|
227
|
+
if (open) this.#popup.firstElementChild.focus();
|
|
228
|
+
};
|
|
229
|
+
this.#backdrop.onclick = () => this.close();
|
|
230
|
+
this.#popup.onkeydown = event => {
|
|
231
|
+
if (event.key === "Tab") {
|
|
232
|
+
this.close();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const items = [...this.#popup.children];
|
|
236
|
+
const index = items.indexOf(root.activeElement);
|
|
237
|
+
const offsets = { ArrowDown: 1, ArrowUp: -1 };
|
|
238
|
+
if (event.key in offsets) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
items[(index + offsets[event.key] + items.length) % items.length].focus();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
document.addEventListener("keydown", this.#key);
|
|
244
|
+
this.update([]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 同步可用操作和忙碌状态,不重建菜单触发按钮。
|
|
249
|
+
* Update actions and busy state without replacing the trigger button.
|
|
250
|
+
* @param {Array<{id: string, label: string, destructive?: boolean}>} items 操作列表 / Actions.
|
|
251
|
+
* @param {boolean} [disabled] 是否忙碌 / Whether operations are busy.
|
|
252
|
+
* @returns {void} 无返回值 / No return value.
|
|
253
|
+
*/
|
|
254
|
+
update(items, disabled = false) {
|
|
255
|
+
this.close();
|
|
256
|
+
this.#button.disabled = disabled || items.length === 0;
|
|
257
|
+
this.#popup.replaceChildren(
|
|
258
|
+
...items.map(item => {
|
|
259
|
+
const button = this.#document.createElement("button");
|
|
260
|
+
button.type = "button";
|
|
261
|
+
button.setAttribute("role", "menuitem");
|
|
262
|
+
button.textContent = item.label;
|
|
263
|
+
button.toggleAttribute("data-danger", Boolean(item.destructive));
|
|
264
|
+
button.onclick = () => {
|
|
265
|
+
this.close();
|
|
266
|
+
this.#select(item.id);
|
|
267
|
+
};
|
|
268
|
+
return button;
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 关闭菜单。
|
|
275
|
+
* Close the menu.
|
|
276
|
+
* @returns {void} 无返回值 / No return value.
|
|
277
|
+
*/
|
|
278
|
+
close() {
|
|
279
|
+
this.#popup.hidden = this.#backdrop.hidden = true;
|
|
280
|
+
this.#button.setAttribute("aria-expanded", "false");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 移除监听器与节点。
|
|
285
|
+
* Remove listeners and elements.
|
|
286
|
+
* @returns {void} 无返回值 / No return value.
|
|
287
|
+
*/
|
|
288
|
+
destroy() {
|
|
289
|
+
this.#document.removeEventListener("keydown", this.#key);
|
|
290
|
+
this.element.remove();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
185
293
|
|
|
186
294
|
/**
|
|
187
295
|
* 将 BoxJS 数组、app 或订阅转换为模块字段,保留原文件为唯一字段来源。
|
|
@@ -322,31 +430,31 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
322
430
|
*/
|
|
323
431
|
const sessions = new Map();
|
|
324
432
|
/**
|
|
325
|
-
*
|
|
326
|
-
* Send a
|
|
327
|
-
* @param {string} path
|
|
328
|
-
* @param {"
|
|
329
|
-
* @param {unknown} body
|
|
433
|
+
* 用 form 发送完整存储键;读取 404 交给调用方处理。
|
|
434
|
+
* Send a complete storage key as form data; callers handle missing reads.
|
|
435
|
+
* @param {string} path 完整 @root.path / Complete @root.path.
|
|
436
|
+
* @param {"get" | "set" | "delete"} action 存储操作 / Storage operation.
|
|
437
|
+
* @param {unknown} body set 值,其它操作忽略 / Set value, ignored by other operations.
|
|
330
438
|
* @param {AbortSignal | undefined} signal 会话取消信号 / Session cancellation signal.
|
|
331
439
|
* @returns {Promise<Response>} 未消费正文的响应 / Response with an unread body.
|
|
332
440
|
* @throws {Error} 非 200 且非数据 GET 404、超时、取消或网络错误 / Non-200 status except missing-data GETs, timeout, cancellation or network error.
|
|
333
441
|
*/
|
|
334
|
-
async function send(path,
|
|
442
|
+
async function send(path, action, body, signal) {
|
|
335
443
|
const controller = new AbortController();
|
|
336
444
|
const abort = () => controller.abort();
|
|
337
445
|
if (signal?.aborted) abort();
|
|
338
446
|
signal?.addEventListener("abort", abort, { once: true });
|
|
339
447
|
const timer = setTimeout(abort, timeout);
|
|
340
448
|
try {
|
|
341
|
-
const response = await request(
|
|
342
|
-
method,
|
|
449
|
+
const response = await request(`/api/${action}`, {
|
|
450
|
+
method: "POST",
|
|
343
451
|
credentials: "omit",
|
|
344
452
|
cache: "no-store",
|
|
345
453
|
signal: controller.signal,
|
|
346
|
-
headers: { "
|
|
347
|
-
|
|
454
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
455
|
+
body: new URLSearchParams([[path, action === "set" ? JSON.stringify(body) : ""]]).toString(),
|
|
348
456
|
});
|
|
349
|
-
if (response.status !== 200 && !(
|
|
457
|
+
if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
350
458
|
return response;
|
|
351
459
|
} finally {
|
|
352
460
|
clearTimeout(timer);
|
|
@@ -370,21 +478,21 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
370
478
|
* Serialize single-key mutations and update a still-active session only after success.
|
|
371
479
|
* @param {string} module 已打开模块 / Open module.
|
|
372
480
|
* @param {string} key 完整点分字段路径 / Complete dotted field path.
|
|
373
|
-
* @param {"
|
|
481
|
+
* @param {"set" | "delete"} action 写入或删除 / Write or delete.
|
|
374
482
|
* @param {unknown} value 写入值,删除时忽略 / Write value, ignored for deletion.
|
|
375
483
|
* @param {"write" | "delete" | "clearCaches" | "reset"} [operation] 操作类型 / Operation kind.
|
|
376
484
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
377
485
|
* @throws {Error} 会话、字段、值或请求错误 / Session, field, value or request error.
|
|
378
486
|
*/
|
|
379
|
-
async function change(module, key,
|
|
487
|
+
async function change(module, key, action, value, operation = action === "set" ? "write" : "delete") {
|
|
380
488
|
const state = sessions.get(module);
|
|
381
489
|
if (!state?.definition) throw new Error("Open the module first");
|
|
382
490
|
if (state.saving) throw new Error("A settings write is already in progress");
|
|
383
491
|
const field = state.definition.fields.find(field => field.key === key);
|
|
384
492
|
state.saving = true;
|
|
385
493
|
try {
|
|
386
|
-
if ((operation === "write" || operation === "delete") && (!field || (
|
|
387
|
-
await send(
|
|
494
|
+
if ((operation === "write" || operation === "delete") && (!field || (action === "set" && !validValue(field, value)))) throw new TypeError("Invalid setting value");
|
|
495
|
+
await send(`@${state.definition.storageKey}.${key}`, action, value);
|
|
388
496
|
if (sessions.get(module) === state) {
|
|
389
497
|
switch (operation) {
|
|
390
498
|
case "write":
|
|
@@ -427,7 +535,7 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
427
535
|
sessions.set(module, state);
|
|
428
536
|
try {
|
|
429
537
|
const definition = normalizeBoxJs(catalog.select(module), module);
|
|
430
|
-
const response = await send(
|
|
538
|
+
const response = await send(`@${definition.storageKey}.${definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
|
|
431
539
|
let subtree = response.status === 404 ? {} : await response.json();
|
|
432
540
|
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
433
541
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
@@ -457,7 +565,7 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
457
565
|
async readCaches(module) {
|
|
458
566
|
const state = sessions.get(module);
|
|
459
567
|
if (!state?.definition) throw new Error("Open the module first");
|
|
460
|
-
const response = await send(
|
|
568
|
+
const response = await send(`@${state.definition.storageKey}.${module}.Caches`, "get", undefined, state.controller.signal);
|
|
461
569
|
return response.status === 404 ? undefined : response.json();
|
|
462
570
|
},
|
|
463
571
|
/**
|
|
@@ -466,14 +574,14 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
466
574
|
* @param {string} module 已打开模块 / Open module.
|
|
467
575
|
* @returns {Promise<void>} 清理完成 / Cleanup completion.
|
|
468
576
|
*/
|
|
469
|
-
clearCaches: module => change(module, `${module}.Caches`, "
|
|
577
|
+
clearCaches: module => change(module, `${module}.Caches`, "delete", undefined, "clearCaches"),
|
|
470
578
|
/**
|
|
471
579
|
* 删除整个模块持久化节点,以当前 BoxJS 默认值重置页面缓存。
|
|
472
580
|
* Delete module persistence and reset the page cache using current BoxJS defaults.
|
|
473
581
|
* @param {string} module 已打开模块 / Open module.
|
|
474
582
|
* @returns {Promise<void>} 重置完成 / Reset completion.
|
|
475
583
|
*/
|
|
476
|
-
reset: module => change(module, module, "
|
|
584
|
+
reset: module => change(module, module, "delete", undefined, "reset"),
|
|
477
585
|
/**
|
|
478
586
|
* 取消读取并清除会话,不撤销已发送的写入。
|
|
479
587
|
* Abort reads and clear the session without undoing dispatched writes.
|
|
@@ -492,7 +600,7 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
492
600
|
* @param {import("../index.js").SettingsScalar | import("../index.js").SettingsScalar[]} value 字段值 / Field value.
|
|
493
601
|
* @returns {Promise<void>} 写入完成 / Write completion.
|
|
494
602
|
*/
|
|
495
|
-
set: (module, key, value) => change(module, key, "
|
|
603
|
+
set: (module, key, value) => change(module, key, "set", value),
|
|
496
604
|
/**
|
|
497
605
|
* 删除单键覆盖值并显示默认值。
|
|
498
606
|
* Delete one override and display its default value.
|
|
@@ -500,7 +608,7 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
500
608
|
* @param {string} key 点分字段路径 / Dotted field path.
|
|
501
609
|
* @returns {Promise<void>} 删除完成 / Delete completion.
|
|
502
610
|
*/
|
|
503
|
-
remove: (module, key) => change(module, key, "
|
|
611
|
+
remove: (module, key) => change(module, key, "delete"),
|
|
504
612
|
};
|
|
505
613
|
}
|
|
506
614
|
|
|
@@ -681,6 +789,15 @@ function mountPanel(root, catalog) {
|
|
|
681
789
|
back.setAttribute("aria-label", "返回");
|
|
682
790
|
back.type = "button";
|
|
683
791
|
const heading = element("h1", "pp-title", title);
|
|
792
|
+
const handlers = new Map();
|
|
793
|
+
const menuItems = [
|
|
794
|
+
{ id: "viewCaches", label: "查看缓存" },
|
|
795
|
+
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
796
|
+
{ id: "reset", label: "重置模块", destructive: true },
|
|
797
|
+
];
|
|
798
|
+
const menu = new ActionMenu(id => handlers.get(id)());
|
|
799
|
+
const trailing = element("span", "pp-nav-spacer");
|
|
800
|
+
trailing.append(menu.element);
|
|
684
801
|
const brand = element("div", "pp-brand");
|
|
685
802
|
const logo = element("span", "pp-brand-icon");
|
|
686
803
|
logo.setAttribute("aria-hidden", "true");
|
|
@@ -691,20 +808,26 @@ function mountPanel(root, catalog) {
|
|
|
691
808
|
const toast = element("div", "pp-toast");
|
|
692
809
|
toast.setAttribute("role", "status");
|
|
693
810
|
toast.hidden = true;
|
|
694
|
-
header.append(back, brand,
|
|
811
|
+
header.append(back, brand, trailing);
|
|
695
812
|
shell.append(header, viewport, toast);
|
|
696
813
|
root.append(shell);
|
|
697
814
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
698
815
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
699
816
|
const publishNavigation = () => {
|
|
817
|
+
const actions = handlers.size ? menuItems : [];
|
|
818
|
+
menu.update(actions, saving);
|
|
700
819
|
const frame = window.frameElement;
|
|
701
820
|
if (!frame?.dataset.preferencePanes) return;
|
|
702
821
|
frame.dispatchEvent(
|
|
703
822
|
new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
|
|
704
|
-
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled },
|
|
823
|
+
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled, actions },
|
|
705
824
|
}),
|
|
706
825
|
);
|
|
707
826
|
};
|
|
827
|
+
const onAction = event => {
|
|
828
|
+
if (!saving && handlers.has(event.detail)) handlers.get(event.detail)();
|
|
829
|
+
};
|
|
830
|
+
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
708
831
|
let timer,
|
|
709
832
|
navigation,
|
|
710
833
|
generation = 0,
|
|
@@ -1014,17 +1137,13 @@ function mountPanel(root, catalog) {
|
|
|
1014
1137
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
1015
1138
|
groups.get(group).append(row);
|
|
1016
1139
|
}
|
|
1017
|
-
const
|
|
1018
|
-
maintenance.append(element("h2", "pp-title", "模块数据"));
|
|
1019
|
-
const actions = element("div", "pp-actions");
|
|
1020
|
-
const cacheView = element("button", "", "查看 Caches");
|
|
1021
|
-
const cacheClear = element("button", "", "清空 Caches");
|
|
1022
|
-
const reset = element("button", "pp-danger", "重置模块");
|
|
1140
|
+
const cachePage = element("section", "pp-cache-page");
|
|
1023
1141
|
const output = element("pre", "pp-cache");
|
|
1024
|
-
output.
|
|
1142
|
+
output.textContent = "暂无缓存";
|
|
1025
1143
|
output.setAttribute("aria-label", "Caches 内容");
|
|
1026
|
-
|
|
1027
|
-
|
|
1144
|
+
cachePage.append(output);
|
|
1145
|
+
editors.set("$caches", { node: cachePage, title: "缓存" });
|
|
1146
|
+
handlers.set("viewCaches", () => {
|
|
1028
1147
|
if (saving) return;
|
|
1029
1148
|
let value;
|
|
1030
1149
|
return perform(
|
|
@@ -1038,12 +1157,11 @@ function mountPanel(root, catalog) {
|
|
|
1038
1157
|
},
|
|
1039
1158
|
() => {
|
|
1040
1159
|
output.textContent = value === undefined ? "暂无缓存" : JSON.stringify(value, null, 2);
|
|
1041
|
-
|
|
1042
|
-
cacheView.textContent = "刷新 Caches";
|
|
1160
|
+
navigation.open("$caches");
|
|
1043
1161
|
},
|
|
1044
1162
|
);
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1163
|
+
});
|
|
1164
|
+
handlers.set("clearCaches", () => {
|
|
1047
1165
|
if (saving) return;
|
|
1048
1166
|
if (!window.confirm(`清空 ${active} 的全部 Caches?`)) return;
|
|
1049
1167
|
return perform(
|
|
@@ -1052,15 +1170,12 @@ function mountPanel(root, catalog) {
|
|
|
1052
1170
|
output.textContent = "暂无缓存";
|
|
1053
1171
|
},
|
|
1054
1172
|
);
|
|
1055
|
-
};
|
|
1056
|
-
reset
|
|
1173
|
+
});
|
|
1174
|
+
handlers.set("reset", () => {
|
|
1057
1175
|
if (saving) return;
|
|
1058
1176
|
if (!window.confirm(`重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) return;
|
|
1059
1177
|
return perform(() => client.reset(active), controls);
|
|
1060
|
-
};
|
|
1061
|
-
actions.append(cacheView, cacheClear, reset);
|
|
1062
|
-
maintenance.append(actions, output);
|
|
1063
|
-
view.append(maintenance);
|
|
1178
|
+
});
|
|
1064
1179
|
navigation?.destroy();
|
|
1065
1180
|
navigation = new Navigation(viewport, view, key => editors.get(key)?.node);
|
|
1066
1181
|
navigation.addEventListener("change", updateNavigation);
|
|
@@ -1086,6 +1201,8 @@ function mountPanel(root, catalog) {
|
|
|
1086
1201
|
*/
|
|
1087
1202
|
destroy() {
|
|
1088
1203
|
destroyed = true;
|
|
1204
|
+
menu.destroy();
|
|
1205
|
+
window.frameElement?.removeEventListener("preferencepanes:action", onAction);
|
|
1089
1206
|
navigation?.destroy();
|
|
1090
1207
|
generation++;
|
|
1091
1208
|
if (active && !saving) client.leave(active);
|