@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var defaults = "/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text:
|
|
1
|
+
var defaults = "@import url(\"https://hilo.bilibili.com/h5_common/theme.min.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/h5_common/b-style.min.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/app_settings/assets/messageSettingsLayout-ltzQ1gMi.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/app_settings/assets/message-settings-BD3N1lqQ.css\") layer(preference-panes);\n@layer preference-panes {\n/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: var(--text1);\n --pp-background: var(--bg2);\n --pp-surface: var(--bg1);\n --pp-border: var(--line_regular);\n --pp-muted: var(--text3);\n --pp-accent: var(--brand_pink);\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:not(.v-toggle) {\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) + 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-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}\n.pp-search {\n width: 100%;\n margin-bottom: 12px;\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-panel .v-toggle {\n border: 0;\n padding: 0;\n flex: none;\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\n}";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 校验原始路径片段,不进行 URL 编码转换。
|
|
@@ -109,10 +109,30 @@ function metadata(source) {
|
|
|
109
109
|
function element(tag, className, text) {
|
|
110
110
|
const node = document.createElement(tag);
|
|
111
111
|
node.className = className;
|
|
112
|
+
// 官方 AppSettings 1.1.2 的作用域标记与原版 CSS 一起固定版本。
|
|
113
|
+
// Pin official AppSettings 1.1.2 scope attributes together with its unmodified CSS.
|
|
114
|
+
if (/\bform-row(?:\b|__)/.test(className)) node.setAttribute("data-v-b69aa1ea", "");
|
|
115
|
+
if (/\bform-group(?:\b|__)/.test(className)) node.setAttribute("data-v-e590be47", "");
|
|
112
116
|
if (text !== undefined) node.textContent = text;
|
|
113
117
|
return node;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* 搜索、选择和文本控件共用官方 VField 的 DOM 结构。
|
|
122
|
+
* Share the official VField DOM structure across search, select and text controls.
|
|
123
|
+
* @param {HTMLElement} control 已创建的原生控件 / Existing native control.
|
|
124
|
+
* @param {boolean} [multiline] 是否为多行输入 / Whether the control is multiline.
|
|
125
|
+
* @returns {HTMLDivElement} 字段容器 / Field container.
|
|
126
|
+
*/
|
|
127
|
+
function fieldControl(control, multiline = false) {
|
|
128
|
+
const field = element("div", `v-field pp-editor${multiline ? " v-field--textarea" : ""}`);
|
|
129
|
+
const body = element("div", "v-field__body");
|
|
130
|
+
control.classList.add("v-field__control");
|
|
131
|
+
body.append(control);
|
|
132
|
+
field.append(body);
|
|
133
|
+
return field;
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
/**
|
|
117
137
|
* 元数据地址只允许 HTTP(S) 和相对地址。
|
|
118
138
|
* Allow only HTTP(S) and relative metadata addresses.
|
|
@@ -157,6 +177,24 @@ function errorView(error, retry) {
|
|
|
157
177
|
return view;
|
|
158
178
|
}
|
|
159
179
|
|
|
180
|
+
/**
|
|
181
|
+
* 请求宿主确认;独立网页使用浏览器对话框。
|
|
182
|
+
* Request confirmation from the host, using the browser dialog for standalone pages.
|
|
183
|
+
* @param {Window} host 模块窗口 / Module window.
|
|
184
|
+
* @param {string} message 确认内容 / Confirmation message.
|
|
185
|
+
* @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
|
|
186
|
+
*/
|
|
187
|
+
function requestConfirmation(host, message) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const frame = host.frameElement;
|
|
190
|
+
if (frame) {
|
|
191
|
+
const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
|
|
192
|
+
if (!frame.dispatchEvent(event)) return;
|
|
193
|
+
}
|
|
194
|
+
resolve(host.confirm(message));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
160
198
|
/**
|
|
161
199
|
* 标题栏共用三点菜单;Shadow DOM 隔离项目样式,保留继承的主题色。
|
|
162
200
|
* Shared title-bar overflow menu; Shadow DOM isolates layout while inheriting theme colors.
|
|
@@ -777,7 +815,7 @@ function mountPanel(root, catalog) {
|
|
|
777
815
|
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
778
816
|
{ id: "reset", label: "重置模块", destructive: true },
|
|
779
817
|
];
|
|
780
|
-
const menu = new ActionMenu(id =>
|
|
818
|
+
const menu = new ActionMenu(id => runAction(id));
|
|
781
819
|
const trailing = element("span", "pp-nav-spacer");
|
|
782
820
|
trailing.append(menu.element);
|
|
783
821
|
const brand = element("div", "pp-brand");
|
|
@@ -807,7 +845,7 @@ function mountPanel(root, catalog) {
|
|
|
807
845
|
);
|
|
808
846
|
};
|
|
809
847
|
const onAction = event => {
|
|
810
|
-
if (!saving && handlers.has(event.detail))
|
|
848
|
+
if (!saving && handlers.has(event.detail)) runAction(event.detail);
|
|
811
849
|
};
|
|
812
850
|
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
813
851
|
let timer,
|
|
@@ -849,6 +887,19 @@ function mountPanel(root, catalog) {
|
|
|
849
887
|
}, 2400);
|
|
850
888
|
};
|
|
851
889
|
const client = createPreferencesClient({ catalog, notify });
|
|
890
|
+
/**
|
|
891
|
+
* 两种菜单入口共用异步错误处理,包含宿主确认框错误。
|
|
892
|
+
* Share async error handling between both menus, including host-dialog errors.
|
|
893
|
+
* @param {string} id 操作标识 / Action identifier.
|
|
894
|
+
* @returns {Promise<void>} 操作已处理 / Action handled.
|
|
895
|
+
*/
|
|
896
|
+
async function runAction(id) {
|
|
897
|
+
try {
|
|
898
|
+
await handlers.get(id)();
|
|
899
|
+
} catch (error) {
|
|
900
|
+
notify({ kind: "error", message: error.message });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
852
903
|
/**
|
|
853
904
|
* 打开模块并忽略已过期的异步结果。
|
|
854
905
|
* Open a module and ignore stale asynchronous results.
|
|
@@ -880,6 +931,14 @@ function mountPanel(root, catalog) {
|
|
|
880
931
|
const { definition, values } = client.snapshot(active);
|
|
881
932
|
heading.textContent = definition.metadata?.name || active;
|
|
882
933
|
const view = element("section", "pp-fields");
|
|
934
|
+
const search = element("input", "");
|
|
935
|
+
search.type = "search";
|
|
936
|
+
search.placeholder = "搜索设置项";
|
|
937
|
+
search.setAttribute("aria-label", "搜索设置");
|
|
938
|
+
const searchField = fieldControl(search);
|
|
939
|
+
searchField.classList.add("pp-search");
|
|
940
|
+
view.append(searchField);
|
|
941
|
+
const searchRows = [];
|
|
883
942
|
/**
|
|
884
943
|
* 挂载后执行的多行高度更新
|
|
885
944
|
* Textarea sizing callbacks run after mounting.
|
|
@@ -954,7 +1013,7 @@ function mountPanel(root, catalog) {
|
|
|
954
1013
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
|
|
955
1014
|
const group = match?.[1] ?? "通用";
|
|
956
1015
|
if (!groups.has(group)) {
|
|
957
|
-
const section = element("section", "form-group");
|
|
1016
|
+
const section = element("section", "form-group form-group--has-title");
|
|
958
1017
|
const rows = element("div", "form-group__row");
|
|
959
1018
|
section.append(element("h2", "form-group__title", group), rows);
|
|
960
1019
|
groups.set(group, rows);
|
|
@@ -982,7 +1041,7 @@ function mountPanel(root, catalog) {
|
|
|
982
1041
|
let eventName = "change";
|
|
983
1042
|
switch (true) {
|
|
984
1043
|
case Boolean(field.options) && field.type !== "array": {
|
|
985
|
-
const select = element("select", "
|
|
1044
|
+
const select = element("select", "");
|
|
986
1045
|
select.setAttribute("aria-label", field.name);
|
|
987
1046
|
field.options.forEach((option, index) => {
|
|
988
1047
|
const item = element("option", "", option.label);
|
|
@@ -992,7 +1051,7 @@ function mountPanel(root, catalog) {
|
|
|
992
1051
|
write = value => {
|
|
993
1052
|
select.selectedIndex = field.options.findIndex(option => option.key === value);
|
|
994
1053
|
};
|
|
995
|
-
row.append(select);
|
|
1054
|
+
row.append(fieldControl(select));
|
|
996
1055
|
read = () => field.options[select.selectedIndex]?.key;
|
|
997
1056
|
break;
|
|
998
1057
|
}
|
|
@@ -1038,9 +1097,27 @@ function mountPanel(root, catalog) {
|
|
|
1038
1097
|
};
|
|
1039
1098
|
break;
|
|
1040
1099
|
}
|
|
1100
|
+
case field.type === "boolean": {
|
|
1101
|
+
const toggle = element("button", "v-toggle v-toggle--small form-row__toggle");
|
|
1102
|
+
toggle.type = "button";
|
|
1103
|
+
toggle.setAttribute("role", "switch");
|
|
1104
|
+
toggle.setAttribute("aria-label", field.name);
|
|
1105
|
+
toggle.append(element("span", "v-toggle__circle"));
|
|
1106
|
+
write = value => {
|
|
1107
|
+
toggle.setAttribute("aria-checked", String(value === true));
|
|
1108
|
+
toggle.classList.toggle("v-toggle--closed", value !== true);
|
|
1109
|
+
};
|
|
1110
|
+
read = () => toggle.getAttribute("aria-checked") === "true";
|
|
1111
|
+
toggle.onclick = () => {
|
|
1112
|
+
write(!read());
|
|
1113
|
+
toggle.dispatchEvent(new window.Event("change", { bubbles: true }));
|
|
1114
|
+
};
|
|
1115
|
+
row.append(toggle);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1041
1118
|
default: {
|
|
1042
1119
|
const multiline = field.control === "textarea" || field.type === "array";
|
|
1043
|
-
const input = element(multiline ? "textarea" : "input", "
|
|
1120
|
+
const input = element(multiline ? "textarea" : "input", "");
|
|
1044
1121
|
if (multiline) row.classList.add("pp-multiline");
|
|
1045
1122
|
input.setAttribute("aria-label", field.name);
|
|
1046
1123
|
if (field.placeholder) input.placeholder = field.placeholder;
|
|
@@ -1062,33 +1139,23 @@ function mountPanel(root, catalog) {
|
|
|
1062
1139
|
input.addEventListener("input", grow);
|
|
1063
1140
|
growingInputs.push(grow);
|
|
1064
1141
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
input.
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
case "array":
|
|
1083
|
-
return JSON.parse(input.value);
|
|
1084
|
-
case "number":
|
|
1085
|
-
return input.value === "" ? Number.NaN : Number(input.value);
|
|
1086
|
-
default:
|
|
1087
|
-
return input.value;
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
row.append(input);
|
|
1142
|
+
eventName = "input";
|
|
1143
|
+
if (!multiline) input.type = field.type === "number" ? "number" : "text";
|
|
1144
|
+
write = value => {
|
|
1145
|
+
input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
|
|
1146
|
+
grow();
|
|
1147
|
+
};
|
|
1148
|
+
read = () => {
|
|
1149
|
+
switch (field.type) {
|
|
1150
|
+
case "array":
|
|
1151
|
+
return JSON.parse(input.value);
|
|
1152
|
+
case "number":
|
|
1153
|
+
return input.value === "" ? Number.NaN : Number(input.value);
|
|
1154
|
+
default:
|
|
1155
|
+
return input.value;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
row.append(fieldControl(input, multiline));
|
|
1092
1159
|
break;
|
|
1093
1160
|
}
|
|
1094
1161
|
}
|
|
@@ -1118,7 +1185,18 @@ function mountPanel(root, catalog) {
|
|
|
1118
1185
|
});
|
|
1119
1186
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
1120
1187
|
groups.get(group).append(row);
|
|
1188
|
+
searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
|
|
1121
1189
|
}
|
|
1190
|
+
const empty = element("p", "pp-description", "没有匹配的设置项");
|
|
1191
|
+
empty.hidden = true;
|
|
1192
|
+
empty.setAttribute("role", "status");
|
|
1193
|
+
view.append(empty);
|
|
1194
|
+
search.oninput = () => {
|
|
1195
|
+
const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
|
|
1196
|
+
for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
|
|
1197
|
+
for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
|
|
1198
|
+
empty.hidden = searchRows.some(({ row }) => !row.hidden);
|
|
1199
|
+
};
|
|
1122
1200
|
const cachePage = element("section", "pp-cache-page");
|
|
1123
1201
|
const output = element("pre", "pp-cache");
|
|
1124
1202
|
output.textContent = "暂无缓存";
|
|
@@ -1143,9 +1221,9 @@ function mountPanel(root, catalog) {
|
|
|
1143
1221
|
},
|
|
1144
1222
|
);
|
|
1145
1223
|
});
|
|
1146
|
-
handlers.set("clearCaches", () => {
|
|
1224
|
+
handlers.set("clearCaches", async () => {
|
|
1147
1225
|
if (saving) return;
|
|
1148
|
-
if (!window
|
|
1226
|
+
if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
|
|
1149
1227
|
return perform(
|
|
1150
1228
|
() => client.clearCaches(active),
|
|
1151
1229
|
() => {
|
|
@@ -1153,9 +1231,9 @@ function mountPanel(root, catalog) {
|
|
|
1153
1231
|
},
|
|
1154
1232
|
);
|
|
1155
1233
|
});
|
|
1156
|
-
handlers.set("reset", () => {
|
|
1234
|
+
handlers.set("reset", async () => {
|
|
1157
1235
|
if (saving) return;
|
|
1158
|
-
if (!window
|
|
1236
|
+
if (!(await requestConfirmation(window, `重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
|
|
1159
1237
|
return perform(() => client.reset(active), controls);
|
|
1160
1238
|
});
|
|
1161
1239
|
navigation?.destroy();
|
|
@@ -1195,8 +1273,8 @@ function mountPanel(root, catalog) {
|
|
|
1195
1273
|
}
|
|
1196
1274
|
|
|
1197
1275
|
/**
|
|
1198
|
-
*
|
|
1199
|
-
* Mount
|
|
1276
|
+
* 挂载模块设置页;内置资源只引用官方地址,CSS 输入仅用于该页。
|
|
1277
|
+
* Mount the module page with official resource URLs and optional page-specific CSS.
|
|
1200
1278
|
* @param {import("../index.js").BoxJSInput} boxjs 单个模块的 BoxJS JSON / BoxJS JSON for one module.
|
|
1201
1279
|
* @param {string} [css] 可选 CSS 正文 / Optional CSS text.
|
|
1202
1280
|
* @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
|
|
@@ -1221,8 +1299,28 @@ function mount(boxjs, css = "") {
|
|
|
1221
1299
|
document.head.append(base, custom);
|
|
1222
1300
|
const previousTitle = document.title;
|
|
1223
1301
|
const previousTheme = document.documentElement.dataset.theme;
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1302
|
+
const previousDark = document.documentElement.classList.contains("bili_dark");
|
|
1303
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1304
|
+
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
1305
|
+
const host = window.frameElement?.ownerDocument.documentElement;
|
|
1306
|
+
/**
|
|
1307
|
+
* 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
|
|
1308
|
+
* Follow generic host appearance without detecting a business app or parsing its user agent.
|
|
1309
|
+
* @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
|
|
1310
|
+
*/
|
|
1311
|
+
const syncAppearance = () => {
|
|
1312
|
+
const theme = host?.dataset.theme ?? previousTheme ?? (systemTheme.matches ? "dark" : "light");
|
|
1313
|
+
document.documentElement.dataset.theme = theme;
|
|
1314
|
+
document.documentElement.classList.toggle("bili_dark", theme === "dark");
|
|
1315
|
+
if (host) document.documentElement.style.setProperty("--pp-keyboard-height", host.style.getPropertyValue("--pp-keyboard-height"));
|
|
1316
|
+
};
|
|
1317
|
+
let observer;
|
|
1318
|
+
syncAppearance();
|
|
1319
|
+
systemTheme.addEventListener("change", syncAppearance);
|
|
1320
|
+
if (host) {
|
|
1321
|
+
observer = new MutationObserver(syncAppearance);
|
|
1322
|
+
observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
1323
|
+
}
|
|
1226
1324
|
document.title = metadata.name ?? catalog.module.module;
|
|
1227
1325
|
let panel;
|
|
1228
1326
|
const view = {
|
|
@@ -1232,6 +1330,9 @@ function mount(boxjs, css = "") {
|
|
|
1232
1330
|
* @returns {void} 无返回值 / No return value.
|
|
1233
1331
|
*/
|
|
1234
1332
|
destroy() {
|
|
1333
|
+
observer?.disconnect();
|
|
1334
|
+
systemTheme.removeEventListener("change", syncAppearance);
|
|
1335
|
+
document.documentElement.classList.toggle("bili_dark", previousDark);
|
|
1235
1336
|
panel?.destroy();
|
|
1236
1337
|
base.remove();
|
|
1237
1338
|
custom.remove();
|
|
@@ -1240,6 +1341,7 @@ function mount(boxjs, css = "") {
|
|
|
1240
1341
|
document.title = previousTitle;
|
|
1241
1342
|
if (previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
1242
1343
|
else document.documentElement.dataset.theme = previousTheme;
|
|
1344
|
+
document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
|
|
1243
1345
|
},
|
|
1244
1346
|
};
|
|
1245
1347
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsnanocat/preference-panes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Shared settings API runtime for JavaScript proxy modules",
|
|
5
5
|
"author": "VirgilClyne <Virgil@nanocat.me>",
|
|
6
6
|
"homepage": "https://NSNanoCat.github.io/preference-panes",
|
|
@@ -14,6 +14,10 @@ export class ModuleFrame extends EventTarget {
|
|
|
14
14
|
this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
|
|
15
15
|
this.dispatchEvent(new Event("change"));
|
|
16
16
|
};
|
|
17
|
+
#confirmation = event => {
|
|
18
|
+
const request = new CustomEvent("confirm", { cancelable: true, detail: event.detail });
|
|
19
|
+
if (!this.dispatchEvent(request)) event.preventDefault();
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* 建立 iframe 与请求输入;调用方挂载 element 后调用 load。
|
|
@@ -30,6 +34,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
30
34
|
this.element.title = `${inputs.module} 设置`;
|
|
31
35
|
this.element.dataset.preferencePanes = JSON.stringify(inputs);
|
|
32
36
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
37
|
+
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
33
38
|
this.#state = { title: inputs.module, module: inputs.module, busy: false, canGoBack: true, actions: [] };
|
|
34
39
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
35
40
|
}
|
|
@@ -90,5 +95,6 @@ export class ModuleFrame extends EventTarget {
|
|
|
90
95
|
this.#controller.abort();
|
|
91
96
|
this.#options.signal?.removeEventListener("abort", this.#abort);
|
|
92
97
|
this.element.removeEventListener("preferencepanes:change", this.#change);
|
|
98
|
+
this.element.removeEventListener("preferencepanes:confirm", this.#confirmation);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 原始 HTML 的模块 iframe 容器;通过元素传递请求上下文。
|
|
3
3
|
* Iframe container preserving module HTML and carrying context on the element.
|
|
4
|
+
* confirm 事件可 preventDefault 后通过 detail.resolve/reject 完成宿主确认。
|
|
5
|
+
* Prevent default on confirm events and settle host dialogs through detail.resolve/reject.
|
|
4
6
|
*/
|
|
5
7
|
export class ModuleFrame extends EventTarget {
|
|
6
8
|
/**
|
|
@@ -47,6 +49,32 @@ export class ModuleFrame extends EventTarget {
|
|
|
47
49
|
destroy(): void;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 宿主确认事件的数据。
|
|
54
|
+
* Host confirmation event detail.
|
|
55
|
+
*/
|
|
56
|
+
export interface ConfirmationRequest {
|
|
57
|
+
/**
|
|
58
|
+
* 确认内容。
|
|
59
|
+
* Confirmation message.
|
|
60
|
+
*/
|
|
61
|
+
message: string;
|
|
62
|
+
/**
|
|
63
|
+
* 返回选择。
|
|
64
|
+
* Return the user's choice.
|
|
65
|
+
* @param confirmed 是否确认 / Whether confirmed.
|
|
66
|
+
* @returns 无返回值 / No return value.
|
|
67
|
+
*/
|
|
68
|
+
resolve(confirmed: boolean): void;
|
|
69
|
+
/**
|
|
70
|
+
* 返回失败。
|
|
71
|
+
* Return a failure.
|
|
72
|
+
* @param error 错误 / Error.
|
|
73
|
+
* @returns 无返回值 / No return value.
|
|
74
|
+
*/
|
|
75
|
+
reject(error: Error): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* 菜单操作描述。
|
|
52
80
|
* Menu action descriptor.
|
|
@@ -10,10 +10,30 @@
|
|
|
10
10
|
export function element(tag, className, text) {
|
|
11
11
|
const node = document.createElement(tag);
|
|
12
12
|
node.className = className;
|
|
13
|
+
// 官方 AppSettings 1.1.2 的作用域标记与原版 CSS 一起固定版本。
|
|
14
|
+
// Pin official AppSettings 1.1.2 scope attributes together with its unmodified CSS.
|
|
15
|
+
if (/\bform-row(?:\b|__)/.test(className)) node.setAttribute("data-v-b69aa1ea", "");
|
|
16
|
+
if (/\bform-group(?:\b|__)/.test(className)) node.setAttribute("data-v-e590be47", "");
|
|
13
17
|
if (text !== undefined) node.textContent = text;
|
|
14
18
|
return node;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 搜索、选择和文本控件共用官方 VField 的 DOM 结构。
|
|
23
|
+
* Share the official VField DOM structure across search, select and text controls.
|
|
24
|
+
* @param {HTMLElement} control 已创建的原生控件 / Existing native control.
|
|
25
|
+
* @param {boolean} [multiline] 是否为多行输入 / Whether the control is multiline.
|
|
26
|
+
* @returns {HTMLDivElement} 字段容器 / Field container.
|
|
27
|
+
*/
|
|
28
|
+
export function fieldControl(control, multiline = false) {
|
|
29
|
+
const field = element("div", `v-field pp-editor${multiline ? " v-field--textarea" : ""}`);
|
|
30
|
+
const body = element("div", "v-field__body");
|
|
31
|
+
control.classList.add("v-field__control");
|
|
32
|
+
body.append(control);
|
|
33
|
+
field.append(body);
|
|
34
|
+
return field;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
/**
|
|
18
38
|
* 元数据地址只允许 HTTP(S) 和相对地址。
|
|
19
39
|
* Allow only HTTP(S) and relative metadata addresses.
|
|
@@ -57,3 +77,21 @@ export function errorView(error, retry) {
|
|
|
57
77
|
view.append(element("p", "", `加载失败:${error.message}`), button);
|
|
58
78
|
return view;
|
|
59
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 请求宿主确认;独立网页使用浏览器对话框。
|
|
83
|
+
* Request confirmation from the host, using the browser dialog for standalone pages.
|
|
84
|
+
* @param {Window} host 模块窗口 / Module window.
|
|
85
|
+
* @param {string} message 确认内容 / Confirmation message.
|
|
86
|
+
* @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
|
|
87
|
+
*/
|
|
88
|
+
export function requestConfirmation(host, message) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const frame = host.frameElement;
|
|
91
|
+
if (frame) {
|
|
92
|
+
const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
|
|
93
|
+
if (!frame.dispatchEvent(event)) return;
|
|
94
|
+
}
|
|
95
|
+
resolve(host.confirm(message));
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/browser/index.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { element, resourceURL } from "./components.mjs";
|
|
|
4
4
|
import { mountPanel } from "./panel.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Mount
|
|
7
|
+
* 挂载模块设置页;内置资源只引用官方地址,CSS 输入仅用于该页。
|
|
8
|
+
* Mount the module page with official resource URLs and optional page-specific CSS.
|
|
9
9
|
* @param {import("../index.js").BoxJSInput} boxjs 单个模块的 BoxJS JSON / BoxJS JSON for one module.
|
|
10
10
|
* @param {string} [css] 可选 CSS 正文 / Optional CSS text.
|
|
11
11
|
* @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
|
|
@@ -30,8 +30,28 @@ export function mount(boxjs, css = "") {
|
|
|
30
30
|
document.head.append(base, custom);
|
|
31
31
|
const previousTitle = document.title;
|
|
32
32
|
const previousTheme = document.documentElement.dataset.theme;
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const previousDark = document.documentElement.classList.contains("bili_dark");
|
|
34
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
35
|
+
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
36
|
+
const host = window.frameElement?.ownerDocument.documentElement;
|
|
37
|
+
/**
|
|
38
|
+
* 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
|
|
39
|
+
* Follow generic host appearance without detecting a business app or parsing its user agent.
|
|
40
|
+
* @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
|
|
41
|
+
*/
|
|
42
|
+
const syncAppearance = () => {
|
|
43
|
+
const theme = host?.dataset.theme ?? previousTheme ?? (systemTheme.matches ? "dark" : "light");
|
|
44
|
+
document.documentElement.dataset.theme = theme;
|
|
45
|
+
document.documentElement.classList.toggle("bili_dark", theme === "dark");
|
|
46
|
+
if (host) document.documentElement.style.setProperty("--pp-keyboard-height", host.style.getPropertyValue("--pp-keyboard-height"));
|
|
47
|
+
};
|
|
48
|
+
let observer;
|
|
49
|
+
syncAppearance();
|
|
50
|
+
systemTheme.addEventListener("change", syncAppearance);
|
|
51
|
+
if (host) {
|
|
52
|
+
observer = new MutationObserver(syncAppearance);
|
|
53
|
+
observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
54
|
+
}
|
|
35
55
|
document.title = metadata.name ?? catalog.module.module;
|
|
36
56
|
let panel;
|
|
37
57
|
const view = {
|
|
@@ -41,6 +61,9 @@ export function mount(boxjs, css = "") {
|
|
|
41
61
|
* @returns {void} 无返回值 / No return value.
|
|
42
62
|
*/
|
|
43
63
|
destroy() {
|
|
64
|
+
observer?.disconnect();
|
|
65
|
+
systemTheme.removeEventListener("change", syncAppearance);
|
|
66
|
+
document.documentElement.classList.toggle("bili_dark", previousDark);
|
|
44
67
|
panel?.destroy();
|
|
45
68
|
base.remove();
|
|
46
69
|
custom.remove();
|
|
@@ -49,6 +72,7 @@ export function mount(boxjs, css = "") {
|
|
|
49
72
|
document.title = previousTitle;
|
|
50
73
|
if (previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
51
74
|
else document.documentElement.dataset.theme = previousTheme;
|
|
75
|
+
document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
|
|
52
76
|
},
|
|
53
77
|
};
|
|
54
78
|
try {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"https://hilo.bilibili.com/h5_common/theme.min.css": "theme.min.css",
|
|
3
|
+
"https://hilo.bilibili.com/h5_common/b-style.min.css": "b-style.min.css",
|
|
4
|
+
"https://hilo.bilibili.com/app_settings/assets/messageSettingsLayout-ltzQ1gMi.css": "messageSettingsLayout-ltzQ1gMi.css",
|
|
5
|
+
"https://hilo.bilibili.com/app_settings/assets/message-settings-BD3N1lqQ.css": "message-settings-BD3N1lqQ.css"
|
|
6
|
+
}
|