@nsnanocat/preference-panes 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -124
- package/dist/module/app.mjs +1030 -0
- package/dist/module/index.html +14 -0
- package/dist/preference-panes.config.js +842 -0
- package/dist/preference-panes.mjs +935 -825
- package/dist/preference-panes.proxy.js +1726 -2121
- package/package.json +62 -66
- package/src/BoxJS.mjs +86 -0
- package/src/Store.mjs +127 -0
- package/src/browser/app.mjs +24 -148
- package/src/browser/client.d.mts +150 -0
- package/src/browser/client.mjs +191 -212
- package/src/browser/components.mjs +59 -0
- package/src/browser/index.d.ts +19 -152
- package/src/browser/index.mjs +60 -5
- package/src/browser/module.html +14 -0
- package/src/browser/panel.css +221 -221
- package/src/browser/panel.mjs +455 -514
- package/src/build.mjs +31 -0
- package/src/index.d.ts +227 -133
- package/src/index.mjs +3 -6
- package/src/lib/boxjs.mjs +77 -99
- package/src/lib/response.mjs +16 -0
- package/src/lib/settings-path.mjs +10 -23
- package/src/proxy/config.mjs +14 -0
- package/src/proxy/handler.mjs +40 -27
- package/src/proxy/response.mjs +16 -0
- package/dist/preference-panes.request.js +0 -2124
- package/dist/settings/app.mjs +0 -1044
- package/dist/settings/home.css +0 -134
- package/dist/settings/index.html +0 -15
- package/dist/settings/panel.css +0 -325
- package/src/PreferencesHandler.mjs +0 -50
- package/src/SettingsHandler.mjs +0 -142
- package/src/browser/home.css +0 -134
- package/src/browser/site.html +0 -15
- package/src/proxy/request.mjs +0 -5
package/dist/settings/home.css
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/* 通用菜单样式;项目可通过 JSON 引用官方组件样式。
|
|
2
|
-
* Generic menu styles; projects may reference official component styles through JSON. */
|
|
3
|
-
body {
|
|
4
|
-
margin: 0;
|
|
5
|
-
overflow-x: hidden;
|
|
6
|
-
font-family: system-ui, sans-serif;
|
|
7
|
-
background: #f6f7f8;
|
|
8
|
-
color: #18191c;
|
|
9
|
-
}
|
|
10
|
-
.pp-home {
|
|
11
|
-
max-width: 720px;
|
|
12
|
-
margin: auto;
|
|
13
|
-
padding: 32px 16px;
|
|
14
|
-
}
|
|
15
|
-
.brand-logo {
|
|
16
|
-
display: block;
|
|
17
|
-
width: 64px;
|
|
18
|
-
height: 64px;
|
|
19
|
-
margin: auto;
|
|
20
|
-
}
|
|
21
|
-
.brand-logo img {
|
|
22
|
-
display: block;
|
|
23
|
-
width: 100%;
|
|
24
|
-
height: 100%;
|
|
25
|
-
object-fit: contain;
|
|
26
|
-
}
|
|
27
|
-
.pp-home h1 {
|
|
28
|
-
text-align: center;
|
|
29
|
-
font-size: 26px;
|
|
30
|
-
margin: 12px 0 32px;
|
|
31
|
-
}
|
|
32
|
-
.self-panel {
|
|
33
|
-
padding: 12px;
|
|
34
|
-
background: #fff;
|
|
35
|
-
border-radius: 12px;
|
|
36
|
-
}
|
|
37
|
-
.self-panel .header {
|
|
38
|
-
font-size: 16px;
|
|
39
|
-
margin: 0;
|
|
40
|
-
padding: 0 6px;
|
|
41
|
-
}
|
|
42
|
-
.self-panel .container {
|
|
43
|
-
margin-top: 16px;
|
|
44
|
-
}
|
|
45
|
-
.self-panel .self-item {
|
|
46
|
-
width: 25%;
|
|
47
|
-
min-width: 0;
|
|
48
|
-
border: 0;
|
|
49
|
-
background: none;
|
|
50
|
-
padding: 12px 2px;
|
|
51
|
-
color: inherit;
|
|
52
|
-
cursor: pointer;
|
|
53
|
-
}
|
|
54
|
-
.self-panel .self-item .logo {
|
|
55
|
-
display: block;
|
|
56
|
-
width: 56px;
|
|
57
|
-
height: 56px;
|
|
58
|
-
padding: 10px;
|
|
59
|
-
box-sizing: border-box;
|
|
60
|
-
border-radius: 50%;
|
|
61
|
-
background: #f1f2f3;
|
|
62
|
-
}
|
|
63
|
-
.self-panel .logo img {
|
|
64
|
-
display: block;
|
|
65
|
-
width: 100%;
|
|
66
|
-
height: 100%;
|
|
67
|
-
object-fit: contain;
|
|
68
|
-
}
|
|
69
|
-
.self-panel .self-item.is-zh .name {
|
|
70
|
-
font-size: 13px;
|
|
71
|
-
line-height: 20px;
|
|
72
|
-
margin-top: 8px;
|
|
73
|
-
white-space: normal;
|
|
74
|
-
}
|
|
75
|
-
.module-status {
|
|
76
|
-
font-size: 11px;
|
|
77
|
-
line-height: 16px;
|
|
78
|
-
min-height: 16px;
|
|
79
|
-
color: #9499a0;
|
|
80
|
-
}
|
|
81
|
-
.self-item:disabled {
|
|
82
|
-
opacity: 0.45;
|
|
83
|
-
cursor: default;
|
|
84
|
-
}
|
|
85
|
-
.settings-note {
|
|
86
|
-
font-size: 13px;
|
|
87
|
-
line-height: 1.6;
|
|
88
|
-
color: #797f89;
|
|
89
|
-
padding: 16px 0;
|
|
90
|
-
}
|
|
91
|
-
@media (prefers-color-scheme: dark) {
|
|
92
|
-
body {
|
|
93
|
-
background: #101114;
|
|
94
|
-
color: #e3e5e7;
|
|
95
|
-
}
|
|
96
|
-
.self-panel {
|
|
97
|
-
background: #1c1d20;
|
|
98
|
-
}
|
|
99
|
-
.self-panel .self-item .logo {
|
|
100
|
-
background: #303136;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
:root[data-theme="dark"] body {
|
|
104
|
-
background: #101114;
|
|
105
|
-
color: #e3e5e7;
|
|
106
|
-
}
|
|
107
|
-
:root[data-theme="dark"] .self-panel {
|
|
108
|
-
background: #1c1d20;
|
|
109
|
-
}
|
|
110
|
-
:root[data-theme="dark"] .self-panel .self-item .logo {
|
|
111
|
-
background: #303136;
|
|
112
|
-
}
|
|
113
|
-
:root[data-theme="light"] body {
|
|
114
|
-
background: #f6f7f8;
|
|
115
|
-
color: #18191c;
|
|
116
|
-
}
|
|
117
|
-
:root[data-theme="light"] .self-panel {
|
|
118
|
-
background: #fff;
|
|
119
|
-
}
|
|
120
|
-
:root[data-theme="light"] .self-panel .self-item .logo {
|
|
121
|
-
background: #f1f2f3;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.self-panel .scroll {
|
|
125
|
-
display: flex;
|
|
126
|
-
}
|
|
127
|
-
.self-panel .self-item {
|
|
128
|
-
display: flex;
|
|
129
|
-
flex-direction: column;
|
|
130
|
-
align-items: center;
|
|
131
|
-
}
|
|
132
|
-
.pp-site-error {
|
|
133
|
-
padding: 16px;
|
|
134
|
-
}
|
package/dist/settings/index.html
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
|
-
<meta name="color-scheme" content="light dark">
|
|
7
|
-
<title>Preferences</title>
|
|
8
|
-
<link rel="stylesheet" href="/settings/assets/panel.css?v=0.5.0">
|
|
9
|
-
<link rel="stylesheet" href="/settings/assets/home.css?v=0.5.0">
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<main id="preferences"></main>
|
|
13
|
-
<script type="module" src="/settings/assets/app.mjs?v=0.5.0"></script>
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|
package/dist/settings/panel.css
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。
|
|
2
|
-
* Grouped rows follow the Bilibili settings layout, scoped to the panel. */
|
|
3
|
-
.pp-panel {
|
|
4
|
-
--pp-text: #18191c;
|
|
5
|
-
--pp-background: #f6f7f8;
|
|
6
|
-
--pp-surface: #fff;
|
|
7
|
-
--pp-border: #e3e5e7;
|
|
8
|
-
--pp-muted: #9499a0;
|
|
9
|
-
--pp-accent: #fb7299;
|
|
10
|
-
font:
|
|
11
|
-
15px / 1.5 -apple-system,
|
|
12
|
-
BlinkMacSystemFont,
|
|
13
|
-
"Segoe UI",
|
|
14
|
-
sans-serif;
|
|
15
|
-
color: var(--pp-text);
|
|
16
|
-
background: var(--pp-background);
|
|
17
|
-
position: relative;
|
|
18
|
-
min-height: 100vh;
|
|
19
|
-
}
|
|
20
|
-
.pp-panel * {
|
|
21
|
-
box-sizing: border-box;
|
|
22
|
-
letter-spacing: 0;
|
|
23
|
-
}
|
|
24
|
-
.pp-header {
|
|
25
|
-
height: calc(52px + env(safe-area-inset-top));
|
|
26
|
-
padding: env(safe-area-inset-top) 12px 0;
|
|
27
|
-
display: flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
background: var(--pp-surface);
|
|
30
|
-
border-bottom: 1px solid var(--pp-border);
|
|
31
|
-
position: relative;
|
|
32
|
-
}
|
|
33
|
-
.pp-title {
|
|
34
|
-
font-size: 17px;
|
|
35
|
-
font-weight: 500;
|
|
36
|
-
margin: 0;
|
|
37
|
-
min-width: 0;
|
|
38
|
-
overflow-wrap: anywhere;
|
|
39
|
-
}
|
|
40
|
-
.pp-header .pp-title {
|
|
41
|
-
flex: 1;
|
|
42
|
-
text-align: center;
|
|
43
|
-
}
|
|
44
|
-
.pp-nav-spacer {
|
|
45
|
-
width: 44px;
|
|
46
|
-
flex: none;
|
|
47
|
-
}
|
|
48
|
-
.pp-panel button {
|
|
49
|
-
font: inherit;
|
|
50
|
-
cursor: pointer;
|
|
51
|
-
border: 0;
|
|
52
|
-
background: none;
|
|
53
|
-
color: inherit;
|
|
54
|
-
}
|
|
55
|
-
.pp-panel .pp-back {
|
|
56
|
-
width: 44px;
|
|
57
|
-
height: 44px;
|
|
58
|
-
flex: none;
|
|
59
|
-
font-size: 34px;
|
|
60
|
-
line-height: 32px;
|
|
61
|
-
padding: 0;
|
|
62
|
-
}
|
|
63
|
-
.pp-panel button:disabled {
|
|
64
|
-
opacity: 0.5;
|
|
65
|
-
cursor: wait;
|
|
66
|
-
}
|
|
67
|
-
.pp-viewport {
|
|
68
|
-
position: relative;
|
|
69
|
-
height: calc(100vh - 52px - env(safe-area-inset-top));
|
|
70
|
-
overflow: hidden;
|
|
71
|
-
}
|
|
72
|
-
@supports (height: 100dvh) {
|
|
73
|
-
.pp-viewport {
|
|
74
|
-
height: calc(100dvh - 52px - env(safe-area-inset-top));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
.pp-fields,
|
|
78
|
-
.pp-choice-page {
|
|
79
|
-
position: absolute;
|
|
80
|
-
inset: 0;
|
|
81
|
-
overflow: auto;
|
|
82
|
-
padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));
|
|
83
|
-
background: var(--pp-background);
|
|
84
|
-
}
|
|
85
|
-
.pp-panel .form-group {
|
|
86
|
-
margin: 0 0 12px;
|
|
87
|
-
}
|
|
88
|
-
.pp-panel .form-group__title {
|
|
89
|
-
font-size: 12px;
|
|
90
|
-
line-height: 17px;
|
|
91
|
-
font-weight: 400;
|
|
92
|
-
color: var(--pp-muted);
|
|
93
|
-
padding-left: 12px;
|
|
94
|
-
margin: 12px 0 6px;
|
|
95
|
-
}
|
|
96
|
-
.pp-panel .form-group__row {
|
|
97
|
-
border-radius: 8px;
|
|
98
|
-
overflow: hidden;
|
|
99
|
-
background: var(--pp-surface);
|
|
100
|
-
}
|
|
101
|
-
.pp-panel .form-row {
|
|
102
|
-
position: relative;
|
|
103
|
-
display: flex;
|
|
104
|
-
align-items: center;
|
|
105
|
-
width: 100%;
|
|
106
|
-
min-height: 46px;
|
|
107
|
-
padding: 12px;
|
|
108
|
-
border: 0;
|
|
109
|
-
border-bottom: 1px solid var(--pp-border);
|
|
110
|
-
background: var(--pp-surface);
|
|
111
|
-
gap: 12px;
|
|
112
|
-
}
|
|
113
|
-
.pp-panel .form-row:last-child {
|
|
114
|
-
border-bottom: 0;
|
|
115
|
-
}
|
|
116
|
-
.pp-panel .form-row__text {
|
|
117
|
-
flex: 1;
|
|
118
|
-
min-width: 0;
|
|
119
|
-
margin: 0;
|
|
120
|
-
display: flex;
|
|
121
|
-
flex-direction: column;
|
|
122
|
-
}
|
|
123
|
-
.pp-panel .form-row__title {
|
|
124
|
-
font-size: 15px;
|
|
125
|
-
line-height: 22px;
|
|
126
|
-
color: var(--pp-text);
|
|
127
|
-
text-align: left;
|
|
128
|
-
}
|
|
129
|
-
.pp-panel .form-row__subtitle {
|
|
130
|
-
font-size: 12px;
|
|
131
|
-
line-height: 18px;
|
|
132
|
-
color: var(--pp-muted);
|
|
133
|
-
overflow-wrap: anywhere;
|
|
134
|
-
margin-top: 2px;
|
|
135
|
-
}
|
|
136
|
-
.pp-choice-link {
|
|
137
|
-
display: flex;
|
|
138
|
-
align-items: center;
|
|
139
|
-
justify-content: flex-end;
|
|
140
|
-
gap: 8px;
|
|
141
|
-
max-width: 45%;
|
|
142
|
-
min-width: 44px;
|
|
143
|
-
min-height: 44px;
|
|
144
|
-
padding: 0;
|
|
145
|
-
text-align: right;
|
|
146
|
-
flex: 1;
|
|
147
|
-
}
|
|
148
|
-
.pp-summary {
|
|
149
|
-
color: var(--pp-muted);
|
|
150
|
-
font-size: 13px;
|
|
151
|
-
line-height: 18px;
|
|
152
|
-
display: -webkit-box;
|
|
153
|
-
-webkit-line-clamp: 2;
|
|
154
|
-
-webkit-box-orient: vertical;
|
|
155
|
-
overflow: hidden;
|
|
156
|
-
overflow-wrap: anywhere;
|
|
157
|
-
}
|
|
158
|
-
.pp-chevron {
|
|
159
|
-
color: var(--pp-muted);
|
|
160
|
-
font-size: 22px;
|
|
161
|
-
flex: none;
|
|
162
|
-
}
|
|
163
|
-
.pp-input {
|
|
164
|
-
font: inherit;
|
|
165
|
-
color: var(--pp-text);
|
|
166
|
-
background: var(--pp-surface);
|
|
167
|
-
border: 1px solid var(--pp-border);
|
|
168
|
-
border-radius: 6px;
|
|
169
|
-
padding: 8px;
|
|
170
|
-
min-width: 0;
|
|
171
|
-
max-width: 45%;
|
|
172
|
-
width: 45%;
|
|
173
|
-
}
|
|
174
|
-
select.pp-input {
|
|
175
|
-
text-overflow: ellipsis;
|
|
176
|
-
font-size: 13px;
|
|
177
|
-
}
|
|
178
|
-
.pp-panel .pp-multiline {
|
|
179
|
-
display: block;
|
|
180
|
-
}
|
|
181
|
-
.pp-multiline .pp-input {
|
|
182
|
-
max-width: 100%;
|
|
183
|
-
width: 100%;
|
|
184
|
-
margin-top: 10px;
|
|
185
|
-
}
|
|
186
|
-
.pp-switch {
|
|
187
|
-
appearance: none;
|
|
188
|
-
-webkit-appearance: none;
|
|
189
|
-
position: relative;
|
|
190
|
-
flex: none;
|
|
191
|
-
width: 32px;
|
|
192
|
-
height: 20px;
|
|
193
|
-
max-width: none;
|
|
194
|
-
border: 0;
|
|
195
|
-
border-radius: 15px;
|
|
196
|
-
padding: 0;
|
|
197
|
-
background: #c9ccd0;
|
|
198
|
-
cursor: pointer;
|
|
199
|
-
transition: background 0.2s;
|
|
200
|
-
}
|
|
201
|
-
.pp-switch::before {
|
|
202
|
-
content: "";
|
|
203
|
-
position: absolute;
|
|
204
|
-
top: 3px;
|
|
205
|
-
left: 3px;
|
|
206
|
-
width: 14px;
|
|
207
|
-
height: 14px;
|
|
208
|
-
border-radius: 50%;
|
|
209
|
-
background: white;
|
|
210
|
-
transition: transform 0.2s;
|
|
211
|
-
}
|
|
212
|
-
.pp-switch:checked {
|
|
213
|
-
background: var(--pp-accent);
|
|
214
|
-
}
|
|
215
|
-
.pp-switch:checked::before {
|
|
216
|
-
transform: translateX(12px);
|
|
217
|
-
}
|
|
218
|
-
.pp-choice {
|
|
219
|
-
justify-content: space-between;
|
|
220
|
-
cursor: pointer;
|
|
221
|
-
}
|
|
222
|
-
.pp-choice input {
|
|
223
|
-
width: 20px;
|
|
224
|
-
height: 20px;
|
|
225
|
-
flex: none;
|
|
226
|
-
accent-color: var(--pp-accent);
|
|
227
|
-
margin: 0;
|
|
228
|
-
}
|
|
229
|
-
.pp-description {
|
|
230
|
-
font-size: 12px;
|
|
231
|
-
line-height: 1.6;
|
|
232
|
-
color: var(--pp-muted);
|
|
233
|
-
white-space: pre-wrap;
|
|
234
|
-
overflow-wrap: anywhere;
|
|
235
|
-
}
|
|
236
|
-
.pp-module-info {
|
|
237
|
-
display: flex;
|
|
238
|
-
gap: 12px;
|
|
239
|
-
margin: 12px 0;
|
|
240
|
-
}
|
|
241
|
-
.pp-module-icon {
|
|
242
|
-
width: 48px;
|
|
243
|
-
height: 48px;
|
|
244
|
-
object-fit: contain;
|
|
245
|
-
flex: none;
|
|
246
|
-
}
|
|
247
|
-
.pp-module-details {
|
|
248
|
-
min-width: 0;
|
|
249
|
-
overflow-wrap: anywhere;
|
|
250
|
-
}
|
|
251
|
-
.pp-module-source {
|
|
252
|
-
color: inherit;
|
|
253
|
-
text-decoration: underline;
|
|
254
|
-
}
|
|
255
|
-
.pp-maintenance {
|
|
256
|
-
margin-top: 24px;
|
|
257
|
-
}
|
|
258
|
-
.pp-actions {
|
|
259
|
-
display: flex;
|
|
260
|
-
flex-wrap: wrap;
|
|
261
|
-
gap: 8px;
|
|
262
|
-
}
|
|
263
|
-
.pp-actions button,
|
|
264
|
-
.pp-error button {
|
|
265
|
-
min-height: 44px;
|
|
266
|
-
padding: 8px 12px;
|
|
267
|
-
border-radius: 6px;
|
|
268
|
-
background: var(--pp-surface);
|
|
269
|
-
}
|
|
270
|
-
.pp-panel .pp-danger {
|
|
271
|
-
color: #e45656;
|
|
272
|
-
}
|
|
273
|
-
.pp-cache {
|
|
274
|
-
max-height: 320px;
|
|
275
|
-
overflow: auto;
|
|
276
|
-
white-space: pre-wrap;
|
|
277
|
-
overflow-wrap: anywhere;
|
|
278
|
-
}
|
|
279
|
-
.pp-toast {
|
|
280
|
-
pointer-events: none;
|
|
281
|
-
position: fixed;
|
|
282
|
-
bottom: calc(30px + env(safe-area-inset-bottom));
|
|
283
|
-
left: 50%;
|
|
284
|
-
transform: translateX(-50%);
|
|
285
|
-
max-width: 90vw;
|
|
286
|
-
padding: 10px 16px;
|
|
287
|
-
border-radius: 8px;
|
|
288
|
-
background: #333e;
|
|
289
|
-
color: white;
|
|
290
|
-
font-size: 13px;
|
|
291
|
-
z-index: 20;
|
|
292
|
-
}
|
|
293
|
-
.pp-toast[data-kind="error"] {
|
|
294
|
-
background: #8d2424;
|
|
295
|
-
}
|
|
296
|
-
.pp-panel :focus-visible {
|
|
297
|
-
outline: 2px solid var(--pp-accent);
|
|
298
|
-
outline-offset: -2px;
|
|
299
|
-
}
|
|
300
|
-
@media (prefers-color-scheme: dark) {
|
|
301
|
-
.pp-panel {
|
|
302
|
-
--pp-text: #e3e5e7;
|
|
303
|
-
--pp-background: #17181a;
|
|
304
|
-
--pp-surface: #232427;
|
|
305
|
-
--pp-border: #343538;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
:root[data-theme="dark"] .pp-panel {
|
|
309
|
-
--pp-text: #e3e5e7;
|
|
310
|
-
--pp-background: #17181a;
|
|
311
|
-
--pp-surface: #232427;
|
|
312
|
-
--pp-border: #343538;
|
|
313
|
-
}
|
|
314
|
-
:root[data-theme="light"] .pp-panel {
|
|
315
|
-
--pp-text: #18191c;
|
|
316
|
-
--pp-background: #f6f7f8;
|
|
317
|
-
--pp-surface: #fff;
|
|
318
|
-
--pp-border: #e3e5e7;
|
|
319
|
-
}
|
|
320
|
-
@media (prefers-reduced-motion: reduce) {
|
|
321
|
-
.pp-panel .pp-switch,
|
|
322
|
-
.pp-panel .pp-switch::before {
|
|
323
|
-
transition: none;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { URL } from "@nsnanocat/url";
|
|
2
|
-
import { fetch } from "@nsnanocat/util/polyfill/fetch";
|
|
3
|
-
import { SettingsHandler } from "./SettingsHandler.mjs";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 统一处理存储 API 和无原生 Mock 平台的静态资源请求。
|
|
7
|
-
* Handle storage APIs and static resources on hosts without native Mock support.
|
|
8
|
-
*/
|
|
9
|
-
export class PreferencesHandler extends SettingsHandler {
|
|
10
|
-
#origin;
|
|
11
|
-
#resources;
|
|
12
|
-
/**
|
|
13
|
-
* 根据安装 JSON 配置资源映射,不执行项目自定义代码。
|
|
14
|
-
* Configure resource mappings from installation JSON without project-specific code.
|
|
15
|
-
* @param {import("./index.js").PreferencesHandlerOptions} options 安装映射 / Installation mapping.
|
|
16
|
-
*/
|
|
17
|
-
constructor(options) {
|
|
18
|
-
super(options);
|
|
19
|
-
this.#origin = new URL(options.origin).origin;
|
|
20
|
-
this.#resources = options.resources.map(({ pattern, source, contentType }) => {
|
|
21
|
-
const url = new URL(source);
|
|
22
|
-
if (url.protocol !== "https:") throw new TypeError("Resource sources must use HTTPS");
|
|
23
|
-
if (typeof contentType !== "string" || /[\r\n]/.test(contentType)) throw new TypeError("Invalid resource content type");
|
|
24
|
-
return { pattern: new RegExp(pattern), source: url.href, contentType };
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* API 先交给存储桥接,只有资源路由才发出下载请求。
|
|
29
|
-
* Dispatch APIs to storage first; only resource routes perform downloads.
|
|
30
|
-
* @param {import("./index.js").SettingsRequest} request 宿主请求 / Host request.
|
|
31
|
-
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} 响应或非接管请求 / Response or unhandled request.
|
|
32
|
-
*/
|
|
33
|
-
async handle(request) {
|
|
34
|
-
const api = await super.handle(request);
|
|
35
|
-
if (api) return api;
|
|
36
|
-
const url = new URL(request.url);
|
|
37
|
-
if (url.origin !== this.#origin) return;
|
|
38
|
-
const resource = this.#resources.find(resource => resource.pattern.test(url.pathname));
|
|
39
|
-
if (!resource) return;
|
|
40
|
-
switch (request.method) {
|
|
41
|
-
case "GET":
|
|
42
|
-
case "HEAD": {
|
|
43
|
-
const response = await fetch({ url: resource.source, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
|
|
44
|
-
return { status: response.status, headers: { "Content-Type": `${resource.contentType}; charset=utf-8`, "Cache-Control": "no-store" }, body: request.method === "HEAD" ? "" : response.body };
|
|
45
|
-
}
|
|
46
|
-
default:
|
|
47
|
-
return { status: 405, headers: { Allow: "HEAD, GET" }, body: "" };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/SettingsHandler.mjs
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { URL } from "@nsnanocat/url";
|
|
2
|
-
import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
|
|
3
|
-
import { Storage } from "@nsnanocat/util/polyfill/Storage";
|
|
4
|
-
import { parseSettingsPathname, validatePathParts } from "./lib/settings-path.mjs";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 按插件声明的根和模块桥接持久化存储,不下载或解析 BoxJS。
|
|
8
|
-
* Bridge persistence within the installed root and module without downloading or parsing BoxJS.
|
|
9
|
-
*/
|
|
10
|
-
export class SettingsHandler {
|
|
11
|
-
/** @type {string} 接管来源 / Handled origin. */
|
|
12
|
-
#origin;
|
|
13
|
-
/** @type {string} 安装配置中的存储根 / Storage root from installation config. */
|
|
14
|
-
#storageKey;
|
|
15
|
-
/** @type {string} 安装配置中的模块 / Module from installation config. */
|
|
16
|
-
#module;
|
|
17
|
-
/** @type {string} 页面标记头 / Page marker header. */
|
|
18
|
-
#requestHeader;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 固定来源、存储根和模块,构造时不访问网络或存储。
|
|
22
|
-
* Fix the origin, storage root and module without network or storage access at construction.
|
|
23
|
-
* @param {import("./index.js").SettingsHandlerOptions} options 插件安装配置 / Plugin installation config.
|
|
24
|
-
* @throws {TypeError} 安装配置无效 / Invalid installation config.
|
|
25
|
-
*/
|
|
26
|
-
constructor({ origin, storageKey, module, requestHeader = "X-Settings-Client" }) {
|
|
27
|
-
const target = new URL(origin);
|
|
28
|
-
if (target.protocol !== "https:" || target.pathname !== "/" || target.search || target.hash || target.username || target.password) throw new TypeError("origin must be an HTTPS origin");
|
|
29
|
-
if (typeof storageKey !== "string" || !storageKey || storageKey.startsWith("@")) throw new TypeError("storageKey must be a literal root key");
|
|
30
|
-
validatePathParts([module]);
|
|
31
|
-
if (!/^[a-z][a-z0-9-]*$/i.test(requestHeader)) throw new TypeError("Invalid requestHeader");
|
|
32
|
-
this.#origin = target.origin;
|
|
33
|
-
this.#storageKey = storageKey;
|
|
34
|
-
this.#module = module;
|
|
35
|
-
this.#requestHeader = requestHeader;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* GET 返回指定值,POST 替换指定值,DELETE 删除指定键或整个模块。
|
|
40
|
-
* GET returns a value, POST replaces it, and DELETE removes a key or the entire module.
|
|
41
|
-
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
42
|
-
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} 响应或非接管请求 / Response, or undefined for an unhandled request.
|
|
43
|
-
*/
|
|
44
|
-
async handle(request) {
|
|
45
|
-
const url = new URL(request.url);
|
|
46
|
-
if (url.origin !== this.#origin || !url.pathname.startsWith("/api/")) return;
|
|
47
|
-
const headers = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
|
|
48
|
-
const reply = (status, data) => ({ status, headers, body: request.method === "HEAD" ? "" : JSON.stringify(data) });
|
|
49
|
-
let parts;
|
|
50
|
-
try {
|
|
51
|
-
parts = parseSettingsPathname(url.pathname);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
return reply(400, { error: error.message });
|
|
54
|
-
}
|
|
55
|
-
if (parts[0] !== this.#module) return reply(404, { error: "Module is not handled" });
|
|
56
|
-
const requestHeaders = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
57
|
-
if (requestHeaders[this.#requestHeader.toLowerCase()] !== "1" || (requestHeaders.origin && requestHeaders.origin !== this.#origin)) return reply(403, { error: "Forbidden settings client" });
|
|
58
|
-
let value;
|
|
59
|
-
switch (request.method) {
|
|
60
|
-
case "HEAD":
|
|
61
|
-
return reply(200, undefined);
|
|
62
|
-
case "GET":
|
|
63
|
-
case "DELETE":
|
|
64
|
-
break;
|
|
65
|
-
case "POST":
|
|
66
|
-
if (requestHeaders["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json") return reply(415, { error: "Expected application/json" });
|
|
67
|
-
if (typeof request.body !== "string") return reply(400, { error: "Expected a JSON string body" });
|
|
68
|
-
if (request.body.length > 65536) return reply(413, { error: "Body exceeds 65536 UTF-16 code units" });
|
|
69
|
-
try {
|
|
70
|
-
value = JSON.parse(request.body);
|
|
71
|
-
} catch {
|
|
72
|
-
return reply(400, { error: "Invalid JSON" });
|
|
73
|
-
}
|
|
74
|
-
break;
|
|
75
|
-
default:
|
|
76
|
-
return { ...reply(405, { error: "Method not allowed" }), headers: { ...headers, Allow: "HEAD, GET, POST, DELETE" } };
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
const root = Storage.getItem(this.#storageKey, {});
|
|
80
|
-
if (!isRecord(root)) throw new TypeError("stored root must be an object");
|
|
81
|
-
const parent = storageParent(root, parts, request.method === "POST");
|
|
82
|
-
const key = parts.at(-1);
|
|
83
|
-
switch (request.method) {
|
|
84
|
-
case "GET": {
|
|
85
|
-
const result = parent ? _.get(parent, [key]) : undefined;
|
|
86
|
-
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
87
|
-
}
|
|
88
|
-
case "POST":
|
|
89
|
-
_.set(parent, [key], value);
|
|
90
|
-
break;
|
|
91
|
-
case "DELETE":
|
|
92
|
-
if (parent) _.unset(parent, [key]);
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
if (!Storage.setItem(this.#storageKey, root)) throw new Error("Storage write failed");
|
|
96
|
-
return reply(200, request.method === "POST" ? { saved: true } : { deleted: true });
|
|
97
|
-
} catch (error) {
|
|
98
|
-
return reply(500, { error: error.message });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 判断根节点是否为普通对象。
|
|
105
|
-
* Determine whether a root node is a plain object.
|
|
106
|
-
* @param {unknown} value 待检查值 / Value to inspect.
|
|
107
|
-
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
108
|
-
*/
|
|
109
|
-
function isRecord(value) {
|
|
110
|
-
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 遍历父路径,兼容旧存储中 JSON 字符串形式的中间节点。
|
|
115
|
-
* Traverse parents, supporting legacy intermediate nodes serialized as JSON strings.
|
|
116
|
-
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
117
|
-
* @param {string[]} parts 完整路径 / Complete path.
|
|
118
|
-
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
119
|
-
* @returns {object | undefined} 父节点,缺失且不创建时为 undefined / Parent, or undefined when absent and not creating.
|
|
120
|
-
* @throws {TypeError} 无法继续遍历标量节点 / A scalar node cannot be traversed.
|
|
121
|
-
*/
|
|
122
|
-
function storageParent(root, parts, create) {
|
|
123
|
-
let parent = root;
|
|
124
|
-
for (const part of parts.slice(0, -1)) {
|
|
125
|
-
let next = _.get(parent, [part]);
|
|
126
|
-
switch (typeof next) {
|
|
127
|
-
case "undefined":
|
|
128
|
-
if (!create) return;
|
|
129
|
-
next = {};
|
|
130
|
-
break;
|
|
131
|
-
case "string":
|
|
132
|
-
next = JSON.parse(next);
|
|
133
|
-
break;
|
|
134
|
-
default:
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
138
|
-
_.set(parent, [part], next);
|
|
139
|
-
parent = next;
|
|
140
|
-
}
|
|
141
|
-
return parent;
|
|
142
|
-
}
|