@nsnanocat/preference-panes 0.8.1 → 0.9.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 +10 -0
- package/dist/api.js +1 -1
- package/dist/module/app.mjs +144 -42
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +6 -0
- package/dist/preference-panes.mjs +144 -42
- package/package.json +1 -1
- package/src/browser/ModuleFrame.mjs +6 -0
- package/src/browser/Navigation.d.mts +28 -0
- package/src/browser/components.mjs +38 -0
- package/src/browser/index.mjs +28 -4
- package/src/browser/official-styles.json +6 -0
- package/src/browser/panel.css +21 -128
- package/src/browser/panel.mjs +78 -38
package/src/browser/panel.css
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。
|
|
2
2
|
* Grouped rows follow the Bilibili settings layout, scoped to the panel. */
|
|
3
3
|
.pp-panel {
|
|
4
|
-
--pp-text:
|
|
5
|
-
--pp-background:
|
|
6
|
-
--pp-surface:
|
|
7
|
-
--pp-border:
|
|
8
|
-
--pp-muted:
|
|
9
|
-
--pp-accent:
|
|
4
|
+
--pp-text: var(--text1);
|
|
5
|
+
--pp-background: var(--bg2);
|
|
6
|
+
--pp-surface: var(--bg1);
|
|
7
|
+
--pp-border: var(--line_regular);
|
|
8
|
+
--pp-muted: var(--text3);
|
|
9
|
+
--pp-accent: var(--brand_pink);
|
|
10
10
|
font:
|
|
11
11
|
15px / 1.5 -apple-system,
|
|
12
12
|
BlinkMacSystemFont,
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
width: 44px;
|
|
68
68
|
flex: none;
|
|
69
69
|
}
|
|
70
|
-
.pp-panel button {
|
|
70
|
+
.pp-panel button:not(.v-toggle) {
|
|
71
71
|
font: inherit;
|
|
72
72
|
cursor: pointer;
|
|
73
73
|
border: 0;
|
|
@@ -111,60 +111,10 @@
|
|
|
111
111
|
position: absolute;
|
|
112
112
|
inset: 0;
|
|
113
113
|
overflow: auto;
|
|
114
|
-
padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));
|
|
114
|
+
padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));
|
|
115
|
+
scroll-padding-bottom: var(--pp-keyboard-height, 0px);
|
|
115
116
|
background: var(--pp-background);
|
|
116
117
|
}
|
|
117
|
-
.pp-panel .form-group {
|
|
118
|
-
margin: 0 0 12px;
|
|
119
|
-
}
|
|
120
|
-
.pp-panel .form-group__title {
|
|
121
|
-
font-size: 12px;
|
|
122
|
-
line-height: 17px;
|
|
123
|
-
font-weight: 400;
|
|
124
|
-
color: var(--pp-muted);
|
|
125
|
-
padding-left: 12px;
|
|
126
|
-
margin: 12px 0 6px;
|
|
127
|
-
}
|
|
128
|
-
.pp-panel .form-group__row {
|
|
129
|
-
border-radius: 8px;
|
|
130
|
-
overflow: hidden;
|
|
131
|
-
background: var(--pp-surface);
|
|
132
|
-
}
|
|
133
|
-
.pp-panel .form-row {
|
|
134
|
-
position: relative;
|
|
135
|
-
display: flex;
|
|
136
|
-
align-items: center;
|
|
137
|
-
width: 100%;
|
|
138
|
-
min-height: 46px;
|
|
139
|
-
padding: 12px;
|
|
140
|
-
border: 0;
|
|
141
|
-
border-bottom: 1px solid var(--pp-border);
|
|
142
|
-
background: var(--pp-surface);
|
|
143
|
-
gap: 12px;
|
|
144
|
-
}
|
|
145
|
-
.pp-panel .form-row:last-child {
|
|
146
|
-
border-bottom: 0;
|
|
147
|
-
}
|
|
148
|
-
.pp-panel .form-row__text {
|
|
149
|
-
flex: 1;
|
|
150
|
-
min-width: 0;
|
|
151
|
-
margin: 0;
|
|
152
|
-
display: flex;
|
|
153
|
-
flex-direction: column;
|
|
154
|
-
}
|
|
155
|
-
.pp-panel .form-row__title {
|
|
156
|
-
font-size: 15px;
|
|
157
|
-
line-height: 22px;
|
|
158
|
-
color: var(--pp-text);
|
|
159
|
-
text-align: left;
|
|
160
|
-
}
|
|
161
|
-
.pp-panel .form-row__subtitle {
|
|
162
|
-
font-size: 12px;
|
|
163
|
-
line-height: 18px;
|
|
164
|
-
color: var(--pp-muted);
|
|
165
|
-
overflow-wrap: anywhere;
|
|
166
|
-
margin-top: 2px;
|
|
167
|
-
}
|
|
168
118
|
.pp-choice-link {
|
|
169
119
|
display: flex;
|
|
170
120
|
align-items: center;
|
|
@@ -192,60 +142,29 @@
|
|
|
192
142
|
font-size: 22px;
|
|
193
143
|
flex: none;
|
|
194
144
|
}
|
|
195
|
-
.pp-
|
|
196
|
-
|
|
197
|
-
color: var(--pp-text);
|
|
198
|
-
background: var(--pp-surface);
|
|
199
|
-
border: 1px solid var(--pp-border);
|
|
200
|
-
border-radius: 6px;
|
|
201
|
-
padding: 8px;
|
|
202
|
-
min-width: 0;
|
|
203
|
-
max-width: 45%;
|
|
145
|
+
.pp-editor {
|
|
146
|
+
flex: none;
|
|
204
147
|
width: 45%;
|
|
148
|
+
min-width: 0;
|
|
205
149
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
150
|
+
.pp-search {
|
|
151
|
+
width: 100%;
|
|
152
|
+
margin-bottom: 12px;
|
|
209
153
|
}
|
|
210
154
|
.pp-panel .pp-multiline {
|
|
211
155
|
display: block;
|
|
212
156
|
}
|
|
213
|
-
.pp-multiline .pp-
|
|
214
|
-
max-width: 100%;
|
|
157
|
+
.pp-multiline .pp-editor {
|
|
215
158
|
width: 100%;
|
|
216
159
|
margin-top: 10px;
|
|
217
160
|
}
|
|
218
|
-
.pp-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
flex: none;
|
|
223
|
-
width: 32px;
|
|
224
|
-
height: 20px;
|
|
225
|
-
max-width: none;
|
|
161
|
+
.pp-panel [hidden] {
|
|
162
|
+
display: none !important;
|
|
163
|
+
}
|
|
164
|
+
.pp-panel .v-toggle {
|
|
226
165
|
border: 0;
|
|
227
|
-
border-radius: 15px;
|
|
228
166
|
padding: 0;
|
|
229
|
-
|
|
230
|
-
cursor: pointer;
|
|
231
|
-
transition: background 0.2s;
|
|
232
|
-
}
|
|
233
|
-
.pp-switch::before {
|
|
234
|
-
content: "";
|
|
235
|
-
position: absolute;
|
|
236
|
-
top: 3px;
|
|
237
|
-
left: 3px;
|
|
238
|
-
width: 14px;
|
|
239
|
-
height: 14px;
|
|
240
|
-
border-radius: 50%;
|
|
241
|
-
background: white;
|
|
242
|
-
transition: transform 0.2s;
|
|
243
|
-
}
|
|
244
|
-
.pp-switch:checked {
|
|
245
|
-
background: var(--pp-accent);
|
|
246
|
-
}
|
|
247
|
-
.pp-switch:checked::before {
|
|
248
|
-
transform: translateX(12px);
|
|
167
|
+
flex: none;
|
|
249
168
|
}
|
|
250
169
|
.pp-choice {
|
|
251
170
|
justify-content: space-between;
|
|
@@ -315,29 +234,3 @@ select.pp-input {
|
|
|
315
234
|
outline: 2px solid var(--pp-accent);
|
|
316
235
|
outline-offset: -2px;
|
|
317
236
|
}
|
|
318
|
-
@media (prefers-color-scheme: dark) {
|
|
319
|
-
.pp-panel {
|
|
320
|
-
--pp-text: #e3e5e7;
|
|
321
|
-
--pp-background: #17181a;
|
|
322
|
-
--pp-surface: #232427;
|
|
323
|
-
--pp-border: #343538;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
:root[data-theme="dark"] .pp-panel {
|
|
327
|
-
--pp-text: #e3e5e7;
|
|
328
|
-
--pp-background: #17181a;
|
|
329
|
-
--pp-surface: #232427;
|
|
330
|
-
--pp-border: #343538;
|
|
331
|
-
}
|
|
332
|
-
:root[data-theme="light"] .pp-panel {
|
|
333
|
-
--pp-text: #18191c;
|
|
334
|
-
--pp-background: #f6f7f8;
|
|
335
|
-
--pp-surface: #fff;
|
|
336
|
-
--pp-border: #e3e5e7;
|
|
337
|
-
}
|
|
338
|
-
@media (prefers-reduced-motion: reduce) {
|
|
339
|
-
.pp-panel .pp-switch,
|
|
340
|
-
.pp-panel .pp-switch::before {
|
|
341
|
-
transition: none;
|
|
342
|
-
}
|
|
343
|
-
}
|
package/src/browser/panel.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionMenu } from "./ActionMenu.mjs";
|
|
2
2
|
import { createPreferencesClient } from "./client.mjs";
|
|
3
|
-
import { errorView, icon, element as node, resourceURL } from "./components.mjs";
|
|
3
|
+
import { errorView, fieldControl, icon, element as node, requestConfirmation, resourceURL } from "./components.mjs";
|
|
4
4
|
import { Navigation } from "./Navigation.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -27,7 +27,7 @@ export function mountPanel(root, catalog) {
|
|
|
27
27
|
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
28
28
|
{ id: "reset", label: "重置模块", destructive: true },
|
|
29
29
|
];
|
|
30
|
-
const menu = new ActionMenu(id =>
|
|
30
|
+
const menu = new ActionMenu(id => runAction(id));
|
|
31
31
|
const trailing = node("span", "pp-nav-spacer");
|
|
32
32
|
trailing.append(menu.element);
|
|
33
33
|
const brand = node("div", "pp-brand");
|
|
@@ -57,7 +57,7 @@ export function mountPanel(root, catalog) {
|
|
|
57
57
|
);
|
|
58
58
|
};
|
|
59
59
|
const onAction = event => {
|
|
60
|
-
if (!saving && handlers.has(event.detail))
|
|
60
|
+
if (!saving && handlers.has(event.detail)) runAction(event.detail);
|
|
61
61
|
};
|
|
62
62
|
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
63
63
|
let timer,
|
|
@@ -99,6 +99,19 @@ export function mountPanel(root, catalog) {
|
|
|
99
99
|
}, 2400);
|
|
100
100
|
};
|
|
101
101
|
const client = createPreferencesClient({ catalog, notify });
|
|
102
|
+
/**
|
|
103
|
+
* 两种菜单入口共用异步错误处理,包含宿主确认框错误。
|
|
104
|
+
* Share async error handling between both menus, including host-dialog errors.
|
|
105
|
+
* @param {string} id 操作标识 / Action identifier.
|
|
106
|
+
* @returns {Promise<void>} 操作已处理 / Action handled.
|
|
107
|
+
*/
|
|
108
|
+
async function runAction(id) {
|
|
109
|
+
try {
|
|
110
|
+
await handlers.get(id)();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
notify({ kind: "error", message: error.message });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
/**
|
|
103
116
|
* 打开模块并忽略已过期的异步结果。
|
|
104
117
|
* Open a module and ignore stale asynchronous results.
|
|
@@ -130,6 +143,14 @@ export function mountPanel(root, catalog) {
|
|
|
130
143
|
const { definition, values } = client.snapshot(active);
|
|
131
144
|
heading.textContent = definition.metadata?.name || active;
|
|
132
145
|
const view = node("section", "pp-fields");
|
|
146
|
+
const search = node("input", "");
|
|
147
|
+
search.type = "search";
|
|
148
|
+
search.placeholder = "搜索设置项";
|
|
149
|
+
search.setAttribute("aria-label", "搜索设置");
|
|
150
|
+
const searchField = fieldControl(search);
|
|
151
|
+
searchField.classList.add("pp-search");
|
|
152
|
+
view.append(searchField);
|
|
153
|
+
const searchRows = [];
|
|
133
154
|
/**
|
|
134
155
|
* 挂载后执行的多行高度更新
|
|
135
156
|
* Textarea sizing callbacks run after mounting.
|
|
@@ -204,7 +225,7 @@ export function mountPanel(root, catalog) {
|
|
|
204
225
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
|
|
205
226
|
const group = match?.[1] ?? "通用";
|
|
206
227
|
if (!groups.has(group)) {
|
|
207
|
-
const section = node("section", "form-group");
|
|
228
|
+
const section = node("section", "form-group form-group--has-title");
|
|
208
229
|
const rows = node("div", "form-group__row");
|
|
209
230
|
section.append(node("h2", "form-group__title", group), rows);
|
|
210
231
|
groups.set(group, rows);
|
|
@@ -232,7 +253,7 @@ export function mountPanel(root, catalog) {
|
|
|
232
253
|
let eventName = "change";
|
|
233
254
|
switch (true) {
|
|
234
255
|
case Boolean(field.options) && field.type !== "array": {
|
|
235
|
-
const select = node("select", "
|
|
256
|
+
const select = node("select", "");
|
|
236
257
|
select.setAttribute("aria-label", field.name);
|
|
237
258
|
field.options.forEach((option, index) => {
|
|
238
259
|
const item = node("option", "", option.label);
|
|
@@ -242,7 +263,7 @@ export function mountPanel(root, catalog) {
|
|
|
242
263
|
write = value => {
|
|
243
264
|
select.selectedIndex = field.options.findIndex(option => option.key === value);
|
|
244
265
|
};
|
|
245
|
-
row.append(select);
|
|
266
|
+
row.append(fieldControl(select));
|
|
246
267
|
read = () => field.options[select.selectedIndex]?.key;
|
|
247
268
|
break;
|
|
248
269
|
}
|
|
@@ -288,9 +309,27 @@ export function mountPanel(root, catalog) {
|
|
|
288
309
|
};
|
|
289
310
|
break;
|
|
290
311
|
}
|
|
312
|
+
case field.type === "boolean": {
|
|
313
|
+
const toggle = node("button", "v-toggle v-toggle--small form-row__toggle");
|
|
314
|
+
toggle.type = "button";
|
|
315
|
+
toggle.setAttribute("role", "switch");
|
|
316
|
+
toggle.setAttribute("aria-label", field.name);
|
|
317
|
+
toggle.append(node("span", "v-toggle__circle"));
|
|
318
|
+
write = value => {
|
|
319
|
+
toggle.setAttribute("aria-checked", String(value === true));
|
|
320
|
+
toggle.classList.toggle("v-toggle--closed", value !== true);
|
|
321
|
+
};
|
|
322
|
+
read = () => toggle.getAttribute("aria-checked") === "true";
|
|
323
|
+
toggle.onclick = () => {
|
|
324
|
+
write(!read());
|
|
325
|
+
toggle.dispatchEvent(new window.Event("change", { bubbles: true }));
|
|
326
|
+
};
|
|
327
|
+
row.append(toggle);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
291
330
|
default: {
|
|
292
331
|
const multiline = field.control === "textarea" || field.type === "array";
|
|
293
|
-
const input = node(multiline ? "textarea" : "input", "
|
|
332
|
+
const input = node(multiline ? "textarea" : "input", "");
|
|
294
333
|
if (multiline) row.classList.add("pp-multiline");
|
|
295
334
|
input.setAttribute("aria-label", field.name);
|
|
296
335
|
if (field.placeholder) input.placeholder = field.placeholder;
|
|
@@ -312,33 +351,23 @@ export function mountPanel(root, catalog) {
|
|
|
312
351
|
input.addEventListener("input", grow);
|
|
313
352
|
growingInputs.push(grow);
|
|
314
353
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
input.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
case "array":
|
|
333
|
-
return JSON.parse(input.value);
|
|
334
|
-
case "number":
|
|
335
|
-
return input.value === "" ? Number.NaN : Number(input.value);
|
|
336
|
-
default:
|
|
337
|
-
return input.value;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
row.append(input);
|
|
354
|
+
eventName = "input";
|
|
355
|
+
if (!multiline) input.type = field.type === "number" ? "number" : "text";
|
|
356
|
+
write = value => {
|
|
357
|
+
input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
|
|
358
|
+
grow();
|
|
359
|
+
};
|
|
360
|
+
read = () => {
|
|
361
|
+
switch (field.type) {
|
|
362
|
+
case "array":
|
|
363
|
+
return JSON.parse(input.value);
|
|
364
|
+
case "number":
|
|
365
|
+
return input.value === "" ? Number.NaN : Number(input.value);
|
|
366
|
+
default:
|
|
367
|
+
return input.value;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
row.append(fieldControl(input, multiline));
|
|
342
371
|
break;
|
|
343
372
|
}
|
|
344
373
|
}
|
|
@@ -368,7 +397,18 @@ export function mountPanel(root, catalog) {
|
|
|
368
397
|
});
|
|
369
398
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
370
399
|
groups.get(group).append(row);
|
|
400
|
+
searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
|
|
371
401
|
}
|
|
402
|
+
const empty = node("p", "pp-description", "没有匹配的设置项");
|
|
403
|
+
empty.hidden = true;
|
|
404
|
+
empty.setAttribute("role", "status");
|
|
405
|
+
view.append(empty);
|
|
406
|
+
search.oninput = () => {
|
|
407
|
+
const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
|
|
408
|
+
for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
|
|
409
|
+
for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
|
|
410
|
+
empty.hidden = searchRows.some(({ row }) => !row.hidden);
|
|
411
|
+
};
|
|
372
412
|
const cachePage = node("section", "pp-cache-page");
|
|
373
413
|
const output = node("pre", "pp-cache");
|
|
374
414
|
output.textContent = "暂无缓存";
|
|
@@ -393,9 +433,9 @@ export function mountPanel(root, catalog) {
|
|
|
393
433
|
},
|
|
394
434
|
);
|
|
395
435
|
});
|
|
396
|
-
handlers.set("clearCaches", () => {
|
|
436
|
+
handlers.set("clearCaches", async () => {
|
|
397
437
|
if (saving) return;
|
|
398
|
-
if (!window
|
|
438
|
+
if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
|
|
399
439
|
return perform(
|
|
400
440
|
() => client.clearCaches(active),
|
|
401
441
|
() => {
|
|
@@ -403,9 +443,9 @@ export function mountPanel(root, catalog) {
|
|
|
403
443
|
},
|
|
404
444
|
);
|
|
405
445
|
});
|
|
406
|
-
handlers.set("reset", () => {
|
|
446
|
+
handlers.set("reset", async () => {
|
|
407
447
|
if (saving) return;
|
|
408
|
-
if (!window
|
|
448
|
+
if (!(await requestConfirmation(window, `重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
|
|
409
449
|
return perform(() => client.reset(active), controls);
|
|
410
450
|
});
|
|
411
451
|
navigation?.destroy();
|