@nsnanocat/preference-panes 1.0.0 → 1.1.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 +64 -44
- package/dist/api.js +11 -31
- package/dist/module/index.html +1 -1
- package/dist/module/{app.mjs → index.mjs} +764 -623
- package/dist/module/navigation.mjs +15 -32
- package/dist/preference-panes.mjs +703 -566
- package/dist/web.js +2 -23
- package/package.json +2 -2
- package/src/api.mjs +11 -31
- package/src/browser/ModuleFrame.mjs +13 -13
- package/src/browser/ModuleStatus.mjs +2 -3
- package/src/browser/Navigation.d.mts +2 -4
- package/src/browser/client.d.mts +10 -15
- package/src/browser/client.mjs +154 -48
- package/src/browser/index.d.ts +6 -7
- package/src/browser/index.mjs +59 -79
- package/src/browser/module.html +1 -1
- package/src/browser/mount.mjs +103 -0
- package/src/browser/panel.mjs +464 -444
- package/src/index.d.ts +0 -22
- package/src/index.mjs +3 -3
- package/src/web.mjs +1 -5
- package/src/browser/app.mjs +0 -49
- package/src/build.mjs +0 -20
- package/src/lib/page-inputs.mjs +0 -17
package/README.md
CHANGED
|
@@ -1,74 +1,94 @@
|
|
|
1
1
|
# @nsnanocat/preference-panes
|
|
2
2
|
|
|
3
|
-
PreferencePanes
|
|
3
|
+
PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理持久化 API。业务模块只发布自己的 `/configs/{module}` 和 `/api/{module}`;通用 `/settings/**` 前端只需要安装一次。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
在 Biliverse 中,Enhanced 是唯一安装 `web.js` 的模块。Global、Redirect、ADBlock 不携带 `web.js`,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 浏览器入口
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
浏览器包只公开一个入口:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```ts
|
|
12
|
+
function mount(boxjs: BoxJSInput): MountedPreferences;
|
|
13
|
+
interface MountedPreferences { destroy(): void }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { mount } from "@nsnanocat/preference-panes/browser";
|
|
18
|
+
|
|
19
|
+
const boxjs = await fetch("/configs/Module", {
|
|
20
|
+
cache: "no-store",
|
|
21
|
+
credentials: "omit",
|
|
22
|
+
}).then(response => response.json());
|
|
23
|
+
|
|
24
|
+
const preferences = mount(boxjs);
|
|
25
|
+
preferences.destroy();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mount()` 同步建立页面生命周期。BoxJS 规范化、默认值、已存值校验、加载状态和失败重试都由内部流程管理;不公开 API Model、视图 class、构建器或 CSS 第二参数。
|
|
29
|
+
|
|
30
|
+
前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
|
|
31
|
+
|
|
32
|
+
默认样式随包内置。嵌入页面只跟随宿主根元素的 `data-theme` 和 `--pp-keyboard-height`,不识别客户端 User-Agent,也不加载项目 CSS 或客户端 SDK。
|
|
33
|
+
|
|
34
|
+
## 页面与 API
|
|
35
|
+
|
|
36
|
+
`web.js` 只返回三类通用资源:
|
|
12
37
|
|
|
13
|
-
|
|
38
|
+
- `GET /settings/{module}`
|
|
39
|
+
- `GET /settings/assets/index.mjs`
|
|
40
|
+
- `GET /settings/assets/navigation.mjs`
|
|
14
41
|
|
|
15
|
-
|
|
42
|
+
模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,直接读取同源 `/configs/{module}`,然后调用 `mount(boxjs)`。页面不接受 JSON/CSS 查询参数、私有请求头或兼容资源别名。
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
`api.js` 只处理:
|
|
45
|
+
|
|
46
|
+
- `HEAD /api/{module}`:探测同源 `/configs/{module}`,透传状态与 `X-PreferencePanes-Version`。
|
|
47
|
+
- `POST /api/{module}/get`:读取声明字段或 Settings、Caches、module 子树。
|
|
48
|
+
- `POST /api/{module}/set`:写入当前 BoxJS 声明的字段。
|
|
49
|
+
- `POST /api/{module}/delete`:删除声明字段或模块子树。
|
|
50
|
+
|
|
51
|
+
`GET /api/{module}` 和其它未规定方法返回 `405`。API 每次动作都从 `/configs/{module}` 建立字段目录,只负责字段授权与 util `Storage` 读写,不解释控件、选项或默认值。
|
|
18
52
|
|
|
19
53
|
```js
|
|
20
54
|
await fetch("/api/Enhanced/set", {
|
|
21
55
|
method: "POST",
|
|
22
|
-
headers: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
key: "Enhanced.Settings.Home.Top_left",
|
|
59
|
+
value: "mine",
|
|
60
|
+
}),
|
|
27
61
|
});
|
|
28
62
|
```
|
|
29
63
|
|
|
30
|
-
|
|
64
|
+
页面初始化时只执行一次 `POST /api/{module}/get` 读取 Settings 子树。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 delete 动作完成。
|
|
31
65
|
|
|
32
|
-
##
|
|
66
|
+
## 宿主集成
|
|
33
67
|
|
|
34
68
|
```js
|
|
35
|
-
import {
|
|
36
|
-
const model = await fetch("/api/Module", {
|
|
37
|
-
headers: { "X-PreferencePanes-JSON": "/configs/Module" },
|
|
38
|
-
}).then(response => response.json());
|
|
39
|
-
const page = mount(model, ".pp-panel { --pp-accent: #16866a; }");
|
|
40
|
-
page.destroy();
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
API 支持字段数组、单 app 和 apps 订阅,只扫描 `@Root.Module.Settings.key` 形式的字段 ID。浏览器从 API 返回的同一份 BoxJS 解析控件、名称、图标和默认值;省略 CSS 使用内置样式。
|
|
69
|
+
import { ModuleFrame, ModuleStatus } from "@nsnanocat/preference-panes/navigation";
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
const status = new ModuleStatus(statusElement);
|
|
72
|
+
await status.check("/api/Module");
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
import { ModuleFrame, Navigation } from "@nsnanocat/preference-panes/navigation";
|
|
49
|
-
const frame = new ModuleFrame("/settings/Module", {
|
|
50
|
-
headers: { "X-PreferencePanes-JSON": "/configs/Module", "X-PreferencePanes-CSS": "/theme.css" },
|
|
51
|
-
signal,
|
|
52
|
-
});
|
|
74
|
+
const frame = new ModuleFrame("/settings/Module", { signal });
|
|
53
75
|
container.append(frame.element);
|
|
54
76
|
await frame.load();
|
|
55
|
-
frame.addEventListener("change", () => { title.textContent = frame.state.title; });
|
|
56
|
-
back.onclick = () => frame.back();
|
|
57
|
-
frame.destroy();
|
|
58
77
|
```
|
|
59
78
|
|
|
60
|
-
ModuleFrame
|
|
61
|
-
|
|
62
|
-
项目主页可按需使用导出的 `probeModule`、ModuleStatus、ModuleFrame 和 Navigation,也可以自行实现入口。`probeModule("/api/Module", { json: "/configs/Module" })` 只对模块 API 发送一次 HEAD,并原样返回浏览器 `Response`;代理 API 负责探测 BoxJS 上游并透传状态和 `X-PreferencePanes-Version`。ModuleStatus 仅负责把同一响应渲染到状态行。PreferencePanes 不提供主页运行脚本,不识别具体 App,不加载或调用任何客户端 Bridge SDK。调用方若运行在原生 WebView,应在自己的 HTML/页面脚本中直接接入该客户端的官方 SDK,再把 ModuleFrame 事件映射到原生界面。
|
|
63
|
-
|
|
64
|
-
每次进入模块通过 API 取得 JSON 和设置一次,同时读取可选 CSS。二级多选返回复用内存缓存,修改经 Web 校验后提交 API;成功提示、失败回滚由公共组件处理。Caches 按需通过 API 查看/清空,重置只删除指定模块子树。
|
|
65
|
-
|
|
66
|
-
标题栏只显示文字;嵌入模式隐藏模块自身标题栏,由宿主显示原生标题或自己的导航。模块数据操作位于标题栏右侧三点菜单,页面不再平铺维护按钮。`ActionMenu` 是共用的底部操作菜单:独立网页由其三点按钮打开;网页宿主或只有原生按钮、没有原生菜单的 WebView 宿主,将 `frame.state.actions` 传给 `menu.update(actions, busy)`,在宿主按钮点击时调用 `menu.open()`,选择后调用 `frame.perform(id)`。查看设置和缓存都会通过 `POST /api/{module}/get` 按需读取最新子树,再进入可返回的 JSON 详情子页;返回详情页不会触发额外读取,清空和重置设置通过 `/delete` 且仍要求确认。菜单组件提供遮罩、取消按钮、外部点击、Escape 与方向键操作,弹层挂载在文档根部,不依赖模块标题栏是否显示;宿主不访问 iframe 内部 DOM。
|
|
79
|
+
`ModuleFrame` 只携带模块身份和取消信号。宿主通过 `change`、`confirm`、`notice` 事件同步标题、操作菜单、确认框和提示,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
|
|
67
80
|
|
|
68
81
|
## 构建与验证
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
```sh
|
|
84
|
+
npm run build
|
|
85
|
+
npm run check
|
|
86
|
+
npm run apifox:check
|
|
87
|
+
npm pack --dry-run
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
构建生成 `dist/api.js`、`dist/web.js`、`dist/preference-panes.mjs` 和 `dist/module/` 通用页面资源。包不再提供按模块生成 HTML/CSS 的公开 `build()`。
|
|
71
91
|
|
|
72
|
-
`npm run preview`
|
|
92
|
+
`npm run preview` 启动 JSON 文件导入测试台,使用同一通用页面、同源配置和内存存储验证模块行为。
|
|
73
93
|
|
|
74
|
-
[接口规范](apifox/Specification.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
|
|
94
|
+
[接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
|
package/dist/api.js
CHANGED
|
@@ -1957,31 +1957,21 @@
|
|
|
1957
1957
|
const match = /^\/api\/([a-zA-Z0-9_-]+)(?:\/(get|set|delete))?\/?$/.exec(url.pathname);
|
|
1958
1958
|
if (!match) return;
|
|
1959
1959
|
const [, module, action] = match;
|
|
1960
|
-
const
|
|
1960
|
+
const configuration = `${url.origin}/configs/${module}`;
|
|
1961
1961
|
switch (true) {
|
|
1962
1962
|
case !action && request.method === "HEAD":
|
|
1963
|
-
return this.#probe(request,
|
|
1964
|
-
case !action && request.method === "GET":
|
|
1965
|
-
return this.#model(request, module, configURL);
|
|
1963
|
+
return this.#probe(request, configuration);
|
|
1966
1964
|
case Boolean(action) && request.method === "POST":
|
|
1967
|
-
return this.#action(request, module, action,
|
|
1965
|
+
return this.#action(request, module, action, configuration);
|
|
1968
1966
|
default:
|
|
1969
|
-
return this.#response(request, 405, { error: "Use
|
|
1967
|
+
return this.#response(request, 405, { error: "Use HEAD for module probes and POST for module actions" });
|
|
1970
1968
|
}
|
|
1971
1969
|
}
|
|
1972
1970
|
|
|
1973
|
-
#
|
|
1974
|
-
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
1975
|
-
const source = headers["x-preferencepanes-json"] ?? `/configs/${module}`;
|
|
1976
|
-
if (/^https?:\/\//i.test(source)) return source;
|
|
1977
|
-
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(source)) throw Object.assign(new TypeError("BoxJS resources must use HTTP(S) URLs"), { status: 400 });
|
|
1978
|
-
return `${url.origin}/${source.replace(/^\/+/, "")}`;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
async #probe(request, configURL) {
|
|
1971
|
+
async #probe(request, configuration) {
|
|
1982
1972
|
let result;
|
|
1983
1973
|
try {
|
|
1984
|
-
result = await fetch({ url:
|
|
1974
|
+
result = await fetch({ url: configuration, method: "HEAD", timeout: 5000, headers: { Accept: "application/json" } });
|
|
1985
1975
|
} catch (error) {
|
|
1986
1976
|
return this.#response(request, 502, { error: error.message });
|
|
1987
1977
|
}
|
|
@@ -1989,19 +1979,9 @@
|
|
|
1989
1979
|
return this.#response(request, result.statusCode ?? result.status, undefined, version ? { "X-PreferencePanes-Version": version } : {});
|
|
1990
1980
|
}
|
|
1991
1981
|
|
|
1992
|
-
async #
|
|
1993
|
-
const loaded = await this.#load(module, configURL);
|
|
1994
|
-
const values = {};
|
|
1995
|
-
for (const entry of loaded.entries) {
|
|
1996
|
-
const value = Storage.getItem(entry.id, MISSING);
|
|
1997
|
-
if (value !== MISSING) values[entry.id.slice(loaded.storageKey.length + 2)] = value;
|
|
1998
|
-
}
|
|
1999
|
-
return this.#response(request, 200, { module, boxjs: loaded.boxjs, values, configURL }, loaded.version ? { "X-PreferencePanes-Version": loaded.version } : {});
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
async #action(request, module, action, configURL) {
|
|
1982
|
+
async #action(request, module, action, configuration) {
|
|
2003
1983
|
const payload = this.#jsonBody(request);
|
|
2004
|
-
const target = await this.#load(module,
|
|
1984
|
+
const target = await this.#load(module, configuration);
|
|
2005
1985
|
switch (action) {
|
|
2006
1986
|
case "get": {
|
|
2007
1987
|
const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
|
|
@@ -2019,10 +1999,10 @@
|
|
|
2019
1999
|
}
|
|
2020
2000
|
}
|
|
2021
2001
|
|
|
2022
|
-
async #load(module,
|
|
2002
|
+
async #load(module, configuration) {
|
|
2023
2003
|
let result;
|
|
2024
2004
|
try {
|
|
2025
|
-
result = await fetch({ url:
|
|
2005
|
+
result = await fetch({ url: configuration, method: "GET", timeout: 5000, headers: { Accept: "application/json" } });
|
|
2026
2006
|
} catch (error) {
|
|
2027
2007
|
throw Object.assign(new Error(`Configuration request failed: ${error.message}`), { status: 502 });
|
|
2028
2008
|
}
|
|
@@ -2053,7 +2033,7 @@
|
|
|
2053
2033
|
}
|
|
2054
2034
|
}
|
|
2055
2035
|
if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
2056
|
-
return {
|
|
2036
|
+
return { entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
|
|
2057
2037
|
} catch (error) {
|
|
2058
2038
|
throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
|
|
2059
2039
|
}
|