dsh-bill-ilife 0.2.2 → 0.3.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/dist/client.js +168 -47
- package/dist/client.js.map +1 -1
- package/dist/dsh-ctx.d.ts +47 -1
- package/dist/dsh-ctx.d.ts.map +1 -1
- package/dist/dsh-ctx.js +4 -2
- package/dist/dsh-ctx.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +11 -5
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +16 -12
- package/dist/settings.js.map +1 -1
- package/dist/skill-tool.d.ts +21 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +109 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/slot.d.ts +1 -1
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +1 -1
- package/dist/slot.js.map +1 -1
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/slot.ts
|
|
54
|
-
const SLOT_TITLE = "
|
|
54
|
+
const SLOT_TITLE = "饼干记账";
|
|
55
55
|
const PLUGIN = "dsh-bill-ilife";
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/settings.ts
|
|
@@ -61,29 +61,30 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
key: "db.dir",
|
|
62
62
|
title: "数据目录",
|
|
63
63
|
tier: "common",
|
|
64
|
-
control: "
|
|
65
|
-
|
|
64
|
+
control: "directory",
|
|
65
|
+
prefillFrom: "dataDir",
|
|
66
|
+
hint: "库文件与备份的根目录。留空=用默认目录。"
|
|
66
67
|
},
|
|
67
68
|
{
|
|
68
69
|
key: "db.name",
|
|
69
70
|
title: "库文件名",
|
|
70
71
|
tier: "common",
|
|
71
72
|
control: "text",
|
|
72
|
-
hint: "
|
|
73
|
+
hint: "数据目录下的数据库文件名。改名=换库,旧数据不会被读入。"
|
|
73
74
|
},
|
|
74
75
|
{
|
|
75
76
|
key: "db.goals",
|
|
76
77
|
title: "预算/账户文件名",
|
|
77
78
|
tier: "common",
|
|
78
79
|
control: "text",
|
|
79
|
-
hint: "
|
|
80
|
+
hint: "与库同目录的预算与账户数据文件名。改名后旧文件不会再被读取。"
|
|
80
81
|
},
|
|
81
82
|
{
|
|
82
83
|
key: "html.dir",
|
|
83
84
|
title: "HELP 产物目录名",
|
|
84
85
|
tier: "common",
|
|
85
86
|
control: "text",
|
|
86
|
-
hint: "
|
|
87
|
+
hint: "数据目录下存放 HELP 与速查表的子目录名。改名后新产物落新目录。"
|
|
87
88
|
}
|
|
88
89
|
];
|
|
89
90
|
/** 高级项:默认收起。改错了多半只是自己撞上麻烦,不确定就别动。 */
|
|
@@ -92,29 +93,29 @@ window.__ModuleLoader__.load({
|
|
|
92
93
|
key: "backup.dir",
|
|
93
94
|
title: "备份目录",
|
|
94
95
|
tier: "advanced",
|
|
95
|
-
control: "
|
|
96
|
-
hint: "
|
|
96
|
+
control: "directory",
|
|
97
|
+
hint: "备份与恢复的读写目录。留空=库目录下的 backups。"
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
100
|
key: "backup.stem",
|
|
100
101
|
title: "备份名主体",
|
|
101
102
|
tier: "advanced",
|
|
102
103
|
control: "text",
|
|
103
|
-
hint: "
|
|
104
|
+
hint: "备份文件名的前缀,后面自动加时间戳。"
|
|
104
105
|
},
|
|
105
106
|
{
|
|
106
107
|
key: "html.helpStem",
|
|
107
108
|
title: "产物名主体 · HELP",
|
|
108
109
|
tier: "advanced",
|
|
109
110
|
control: "text",
|
|
110
|
-
hint: "HELP
|
|
111
|
+
hint: "HELP 文件名前缀。"
|
|
111
112
|
},
|
|
112
113
|
{
|
|
113
114
|
key: "html.quickRefStem",
|
|
114
115
|
title: "产物名主体 · 速查表",
|
|
115
116
|
tier: "advanced",
|
|
116
117
|
control: "text",
|
|
117
|
-
hint: "
|
|
118
|
+
hint: "速查表文件名前缀。"
|
|
118
119
|
}
|
|
119
120
|
];
|
|
120
121
|
/** 设置页的行表,顺序即页面顺序。 */
|
|
@@ -123,7 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
123
124
|
const COMMON_ITEM_COUNT = COMMON.length;
|
|
124
125
|
/** 高级组标题与副文案。 */
|
|
125
126
|
const ADVANCED_GROUP_TITLE = "高级";
|
|
126
|
-
const ADVANCED_GROUP_NOTE = "
|
|
127
|
+
const ADVANCED_GROUP_NOTE = "不常改。留空=用默认值。";
|
|
127
128
|
/** 按 `a.b` 路径从配置取值里读(缺层或类型不符一律回 undefined)。 */
|
|
128
129
|
function readPath(values, key) {
|
|
129
130
|
const parts = key.split(".");
|
|
@@ -147,6 +148,10 @@ window.__ModuleLoader__.load({
|
|
|
147
148
|
cur[parts[parts.length - 1]] = value;
|
|
148
149
|
}
|
|
149
150
|
//#endregion
|
|
151
|
+
//#region src/dsh-ctx.ts
|
|
152
|
+
/** 宿主目录选择命名空间的服务名(#736;出处与禁令见 cookbook §13)。 */
|
|
153
|
+
const REMOTE_DIRECTORY_PICKER = "remote.directoryPicker";
|
|
154
|
+
//#endregion
|
|
150
155
|
//#region src/client.ts
|
|
151
156
|
/** dsh-bill-ilife client 适配器(六边形:port=contract,adapter=本文件)。
|
|
152
157
|
*
|
|
@@ -163,6 +168,8 @@ window.__ModuleLoader__.load({
|
|
|
163
168
|
* 宿主再 spawn 技能 CLI 的 `bill.config.*` 三个 key。本文件仍禁 node:不读盘、不算默认值。
|
|
164
169
|
* 照 CONTEXT.md,「技能设置页」只配置、不干活:本页没有查账、记一笔之类的入口。
|
|
165
170
|
*/
|
|
171
|
+
/** client 短名声明:只有这两个(#736 的目录选择走**可选查找**,不写进来——
|
|
172
|
+
* 写进来=硬依赖,提供方缺席时整包被停靠,设置页会跟着装不上;见 cookbook §13)。 */
|
|
166
173
|
const inject = ["slots", "connection"];
|
|
167
174
|
/** 面板视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退)。 */
|
|
168
175
|
const S = {
|
|
@@ -172,26 +179,25 @@ window.__ModuleLoader__.load({
|
|
|
172
179
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
|
|
173
180
|
background: "var(--dsw-alias-bg-base, transparent)",
|
|
174
181
|
color: "var(--dsw-alias-label-primary, inherit)",
|
|
175
|
-
fontSize: 13,
|
|
176
182
|
lineHeight: 1.6
|
|
177
183
|
},
|
|
178
184
|
title: {
|
|
179
|
-
fontSize:
|
|
185
|
+
fontSize: "1.08em",
|
|
180
186
|
fontWeight: 700,
|
|
181
187
|
marginBottom: 8
|
|
182
188
|
},
|
|
183
189
|
muted: {
|
|
184
190
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)",
|
|
185
|
-
fontSize:
|
|
191
|
+
fontSize: "0.92em"
|
|
186
192
|
},
|
|
187
193
|
error: {
|
|
188
194
|
color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
|
|
189
|
-
fontSize:
|
|
195
|
+
fontSize: "1em",
|
|
190
196
|
whiteSpace: "pre-wrap"
|
|
191
197
|
},
|
|
192
198
|
okText: {
|
|
193
199
|
color: "var(--dsw-alias-state-success-primary, #12805c)",
|
|
194
|
-
fontSize:
|
|
200
|
+
fontSize: "0.96em"
|
|
195
201
|
},
|
|
196
202
|
rows: {
|
|
197
203
|
marginTop: 8,
|
|
@@ -203,22 +209,18 @@ window.__ModuleLoader__.load({
|
|
|
203
209
|
paddingTop: 8,
|
|
204
210
|
borderTop: "1px dashed var(--dsw-alias-border, rgba(128,128,128,.25))",
|
|
205
211
|
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
206
|
-
fontSize:
|
|
212
|
+
fontSize: "0.92em"
|
|
207
213
|
},
|
|
208
214
|
info: {
|
|
209
|
-
|
|
210
|
-
font: "12px/1.5 ui-monospace,Consolas,monospace",
|
|
215
|
+
fontSize: "0.92em",
|
|
211
216
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)",
|
|
212
|
-
|
|
217
|
+
overflowWrap: "anywhere"
|
|
213
218
|
},
|
|
214
219
|
row: { marginTop: 10 },
|
|
215
|
-
label: {
|
|
216
|
-
fontSize: 12.5,
|
|
217
|
-
fontWeight: 600
|
|
218
|
-
},
|
|
220
|
+
label: { fontWeight: 600 },
|
|
219
221
|
hint: {
|
|
220
222
|
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
221
|
-
fontSize:
|
|
223
|
+
fontSize: "0.92em",
|
|
222
224
|
margin: "1px 0 4px"
|
|
223
225
|
},
|
|
224
226
|
input: {
|
|
@@ -228,8 +230,22 @@ window.__ModuleLoader__.load({
|
|
|
228
230
|
borderRadius: 6,
|
|
229
231
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
|
|
230
232
|
background: "var(--dsw-alias-bg-base, transparent)",
|
|
233
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
234
|
+
},
|
|
235
|
+
pickRow: {
|
|
236
|
+
display: "flex",
|
|
237
|
+
gap: 6,
|
|
238
|
+
alignItems: "center"
|
|
239
|
+
},
|
|
240
|
+
btnPick: {
|
|
241
|
+
flex: "0 0 auto",
|
|
242
|
+
padding: "4px 10px",
|
|
243
|
+
borderRadius: 6,
|
|
244
|
+
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
|
|
245
|
+
background: "var(--dsw-alias-bg-base, transparent)",
|
|
231
246
|
color: "var(--dsw-alias-label-primary, inherit)",
|
|
232
|
-
|
|
247
|
+
whiteSpace: "nowrap",
|
|
248
|
+
cursor: "pointer"
|
|
233
249
|
},
|
|
234
250
|
bar: {
|
|
235
251
|
marginTop: 14,
|
|
@@ -244,7 +260,6 @@ window.__ModuleLoader__.load({
|
|
|
244
260
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
|
|
245
261
|
background: "var(--dsw-alias-bg-base, transparent)",
|
|
246
262
|
color: "var(--dsw-alias-label-primary, inherit)",
|
|
247
|
-
fontSize: 12.5,
|
|
248
263
|
cursor: "pointer"
|
|
249
264
|
},
|
|
250
265
|
btnPrimary: {
|
|
@@ -253,7 +268,6 @@ window.__ModuleLoader__.load({
|
|
|
253
268
|
border: "1px solid var(--dsw-alias-brand-primary, #2f6fed)",
|
|
254
269
|
background: "var(--dsw-alias-brand-primary, #2f6fed)",
|
|
255
270
|
color: "#fff",
|
|
256
|
-
fontSize: 12.5,
|
|
257
271
|
cursor: "pointer"
|
|
258
272
|
},
|
|
259
273
|
adv: {
|
|
@@ -263,7 +277,6 @@ window.__ModuleLoader__.load({
|
|
|
263
277
|
},
|
|
264
278
|
advSummary: {
|
|
265
279
|
cursor: "pointer",
|
|
266
|
-
fontSize: 12.5,
|
|
267
280
|
fontWeight: 600
|
|
268
281
|
}
|
|
269
282
|
};
|
|
@@ -294,11 +307,11 @@ window.__ModuleLoader__.load({
|
|
|
294
307
|
*/
|
|
295
308
|
function humanizeConfigFailure(code, message) {
|
|
296
309
|
const text = message.trim().length > 0 ? message.trim() : `(${code},宿主未给报文)`;
|
|
297
|
-
if (/解析|YAML|parse/i.test(text)) return `${text}\n改回「键:
|
|
298
|
-
if (/不认识|未知键|UNKNOWN_KEY/i.test(text)) return `${text}\n
|
|
299
|
-
if (/类型|TYPE_MISMATCH/i.test(text)) return `${text}\n
|
|
300
|
-
if (/缺席|missing-cli/.test(text)) return `${text}\n
|
|
301
|
-
return `${text}\n
|
|
310
|
+
if (/解析|YAML|parse/i.test(text)) return `${text}\n改回「键: 值」的写法,或点「重置为默认」。`;
|
|
311
|
+
if (/不认识|未知键|UNKNOWN_KEY/i.test(text)) return `${text}\n删掉页面上没有的行,或点「重置为默认」。`;
|
|
312
|
+
if (/类型|TYPE_MISMATCH/i.test(text)) return `${text}\n按本页的控件形状填,或点「重置为默认」。`;
|
|
313
|
+
if (/缺席|missing-cli/.test(text)) return `${text}\n技能出口没找到:先确认技能包已装好,再点「重新读取」。`;
|
|
314
|
+
return `${text}\n改不动就点「重置为默认」,或照上面那条路径手工改配置文件。`;
|
|
302
315
|
}
|
|
303
316
|
/** 一次配置 RPC(永不抛,失败落字;UI 因此不转圈、不返空)。 */
|
|
304
317
|
async function configRpc(call, method, payload) {
|
|
@@ -353,12 +366,17 @@ window.__ModuleLoader__.load({
|
|
|
353
366
|
if (!r.ok) return r;
|
|
354
367
|
return fetchConfigSurface(call);
|
|
355
368
|
}
|
|
356
|
-
/** 把配置取值铺成「行键 → 输入框文本」(页面表单态;值缺项即空串,不返空留白)。
|
|
357
|
-
|
|
369
|
+
/** 把配置取值铺成「行键 → 输入框文本」(页面表单态;值缺项即空串,不返空留白)。
|
|
370
|
+
*
|
|
371
|
+
* `prefill` 是**落点回执**(配置面那几格解析出来的绝对路径):标了 `prefillFrom` 的行在取值空着时
|
|
372
|
+
* 直接显示那条绝对路径——用户不必自己拼路径(#743),显示的就是技能真会用的那个目录(逐字相同)。 */
|
|
373
|
+
function toDraft(values, prefill = {}) {
|
|
358
374
|
const draft = {};
|
|
359
375
|
for (const item of CONFIG_ITEMS) {
|
|
360
376
|
const v = readPath(values, item.key);
|
|
361
|
-
|
|
377
|
+
const raw = v === void 0 || v === null ? "" : String(v);
|
|
378
|
+
const fallback = item.prefillFrom === void 0 ? void 0 : prefill[item.prefillFrom];
|
|
379
|
+
draft[item.key] = raw === "" && typeof fallback === "string" && fallback !== "" ? fallback : raw;
|
|
362
380
|
}
|
|
363
381
|
return draft;
|
|
364
382
|
}
|
|
@@ -375,7 +393,73 @@ window.__ModuleLoader__.load({
|
|
|
375
393
|
}
|
|
376
394
|
return values;
|
|
377
395
|
}
|
|
378
|
-
/**
|
|
396
|
+
/** 形状守卫:拿到的那个东西像不像目录选择命名空间(本包只认 `pick` 是函数)。 */
|
|
397
|
+
function isDirectoryPicker(raw) {
|
|
398
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
399
|
+
return typeof raw.pick === "function";
|
|
400
|
+
}
|
|
401
|
+
/** 现取目录选择命名空间:**软依赖**,拿不到回 null(页面据此不出入口)。
|
|
402
|
+
*
|
|
403
|
+
* 为什么不用 `inject` 声明:见 cookbook §13——写进声明=硬依赖,提供方缺席时整包被停靠,
|
|
404
|
+
* 设置页会跟着装不上。这里按平台给的「可选查找」办:缺席只是没有入口。
|
|
405
|
+
* 守卫拒绝或提供方中途卸载会抛,一律当「没有」,绝不把设置页带下来。 */
|
|
406
|
+
function resolveDirectoryPicker(getService) {
|
|
407
|
+
if (typeof getService !== "function") return null;
|
|
408
|
+
try {
|
|
409
|
+
const raw = getService(REMOTE_DIRECTORY_PICKER);
|
|
410
|
+
return isDirectoryPicker(raw) ? raw : null;
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** 平台回执 → 三种结果(**永不抛**)。回执是信封 `{ok, value|error}`(见 dsh-ctx.ts 的
|
|
416
|
+
* `DirectoryPickerAnswer`):成功回的是 `value` 不是路径本身,被拒回的是 `ok:false` 不是抛——
|
|
417
|
+
* 照裸值解会把两种情况都误判成「用户取消」(#743 真机现象:点「选择文件夹」什么都没发生)。
|
|
418
|
+
* 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉。 */
|
|
419
|
+
function readPickAnswer(raw) {
|
|
420
|
+
if (typeof raw === "string") return raw.trim() === "" ? { kind: "cancelled" } : {
|
|
421
|
+
kind: "picked",
|
|
422
|
+
path: raw
|
|
423
|
+
};
|
|
424
|
+
const answer = typeof raw === "object" && raw !== null ? raw : {};
|
|
425
|
+
if (answer.ok === true) {
|
|
426
|
+
const value = answer.value;
|
|
427
|
+
return typeof value === "string" && value.trim() !== "" ? {
|
|
428
|
+
kind: "picked",
|
|
429
|
+
path: value
|
|
430
|
+
} : { kind: "cancelled" };
|
|
431
|
+
}
|
|
432
|
+
const detail = answer.error?.message?.trim() ?? "";
|
|
433
|
+
return {
|
|
434
|
+
kind: "unavailable",
|
|
435
|
+
message: "打不开系统文件夹对话框" + (detail === "" ? "" : "(" + detail + ")") + ":请直接在框里填绝对路径。"
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** 唤起一次系统文件夹选择器并归一结果(**永不抛**)。 */
|
|
439
|
+
async function pickDirectory(picker) {
|
|
440
|
+
try {
|
|
441
|
+
return readPickAnswer(await picker.pick());
|
|
442
|
+
} catch (e) {
|
|
443
|
+
return {
|
|
444
|
+
kind: "unavailable",
|
|
445
|
+
message: "打不开系统文件夹对话框(" + (e instanceof Error ? e.message : String(e)) + "):请直接在框里填绝对路径。"
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** 「选择文件夹」按钮的真装配函数(目录行渲染出来的按钮,onClick 就是它)。
|
|
450
|
+
*
|
|
451
|
+
* 回 Promise 是为了本地可判(用例直接 await);接进 React 时调用方 `void` 掉。 */
|
|
452
|
+
function createBrowseHandler(deps) {
|
|
453
|
+
return async (key) => {
|
|
454
|
+
const outcome = await pickDirectory(deps.picker);
|
|
455
|
+
if (outcome.kind === "picked") deps.onChange(key, outcome.path);
|
|
456
|
+
else if (outcome.kind === "unavailable") deps.onUnavailable(outcome.message);
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/** 一行输入(四种控件对齐受限 YAML 子集:文本/数字/布尔/目录)。
|
|
460
|
+
*
|
|
461
|
+
* 目录档=文本框 + 一枚唤起系统文件夹选择器的按钮;`onBrowse` 缺席(命名空间拿不到、
|
|
462
|
+
* 或这条路已被拒)时不画按钮,文本框照旧——那是该缝自己的契约(供不了就收起入口,不是失败)。 */
|
|
379
463
|
function Row(props) {
|
|
380
464
|
const { item } = props;
|
|
381
465
|
const onChange = (e) => props.onChange(item.key, e.target.value);
|
|
@@ -392,13 +476,21 @@ window.__ModuleLoader__.load({
|
|
|
392
476
|
spellCheck: false,
|
|
393
477
|
onChange
|
|
394
478
|
});
|
|
395
|
-
|
|
479
|
+
const browse = item.control === "directory" && props.onBrowse !== void 0 ? react.createElement("button", {
|
|
480
|
+
style: S.btnPick,
|
|
481
|
+
type: "button",
|
|
482
|
+
disabled: props.disabled,
|
|
483
|
+
onClick: () => props.onBrowse?.(item.key)
|
|
484
|
+
}, "选择文件夹…") : null;
|
|
485
|
+
return react.createElement("div", { style: S.row }, react.createElement("div", { style: S.label }, item.title), react.createElement("div", { style: S.hint }, item.hint), browse === null ? control : react.createElement("div", { style: S.pickRow }, control, browse));
|
|
396
486
|
}
|
|
397
487
|
/** 技能设置页:承载饼干记账自己的全部可配置项(只配置,不干活)。
|
|
398
488
|
*
|
|
399
489
|
* 三态:loading(一次 RPC 内)/ready(真表单)/failed(人话报错 + 指引,不返空、不转圈)。 */
|
|
400
490
|
function BillConfig(props) {
|
|
401
491
|
const [state, setState] = react.useState({ kind: "loading" });
|
|
492
|
+
const [pickerGone, setPickerGone] = react.useState(false);
|
|
493
|
+
const [picking, setPicking] = react.useState(false);
|
|
402
494
|
const [draft, setDraft] = react.useState({});
|
|
403
495
|
const [busy, setBusy] = react.useState(false);
|
|
404
496
|
const [notice, setNotice] = react.useState(null);
|
|
@@ -408,7 +500,7 @@ window.__ModuleLoader__.load({
|
|
|
408
500
|
kind: "ready",
|
|
409
501
|
surface
|
|
410
502
|
});
|
|
411
|
-
setDraft(toDraft(surface.values));
|
|
503
|
+
setDraft(toDraft(surface.values, surface));
|
|
412
504
|
}, []);
|
|
413
505
|
const load = react.useCallback(async () => {
|
|
414
506
|
setNotice(null);
|
|
@@ -443,8 +535,26 @@ window.__ModuleLoader__.load({
|
|
|
443
535
|
setNotice(null);
|
|
444
536
|
setError(null);
|
|
445
537
|
}, []);
|
|
538
|
+
/** 目录选择:拿不到命名空间就没有入口;被拒一次即收起入口(不留死按钮)。 */
|
|
539
|
+
const picker = pickerGone ? null : props.getPicker();
|
|
540
|
+
const onBrowse = react.useMemo(() => {
|
|
541
|
+
if (picker === null) return void 0;
|
|
542
|
+
const browse = createBrowseHandler({
|
|
543
|
+
picker,
|
|
544
|
+
onChange,
|
|
545
|
+
onUnavailable: (message) => {
|
|
546
|
+
setError(message);
|
|
547
|
+
setPickerGone(true);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return (key) => {
|
|
551
|
+
setPicking(true);
|
|
552
|
+
setError(null);
|
|
553
|
+
browse(key).finally(() => setPicking(false));
|
|
554
|
+
};
|
|
555
|
+
}, [picker, onChange]);
|
|
446
556
|
const surface = state.kind === "ready" ? state.surface : null;
|
|
447
|
-
const dirty = surface !== null && CONFIG_ITEMS.some((i) => (draft[i.key] ?? "") !== (toDraft(surface.values)[i.key] ?? ""));
|
|
557
|
+
const dirty = surface !== null && CONFIG_ITEMS.some((i) => (draft[i.key] ?? "") !== (toDraft(surface.values, surface)[i.key] ?? ""));
|
|
448
558
|
const onSave = react.useCallback(async () => {
|
|
449
559
|
setBusy(true);
|
|
450
560
|
setNotice(null);
|
|
@@ -471,7 +581,7 @@ window.__ModuleLoader__.load({
|
|
|
471
581
|
setNotice("已重置为默认(原配置已另存一份 .bak)");
|
|
472
582
|
} else setError(r.message);
|
|
473
583
|
}, [apply, props.getCall]);
|
|
474
|
-
const head = react.createElement("div", null, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), surface !== null ? react.createElement("div", null, react.createElement("div", { style: S.info }, `配置文件 ${surface.path}`), react.createElement("div", { style: S.info }, `数据目录 ${surface.dataDir}`), surface.created ? react.createElement("div", { style: S.muted }, "
|
|
584
|
+
const head = react.createElement("div", null, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), surface !== null ? react.createElement("div", null, react.createElement("div", { style: S.info }, `配置文件 ${surface.path}`), react.createElement("div", { style: S.info }, `数据目录 ${surface.dataDir}`), surface.created ? react.createElement("div", { style: S.muted }, "(配置文件刚按默认值生成)") : null) : null);
|
|
475
585
|
if (state.kind === "loading") return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.muted }, "配置读取中…"));
|
|
476
586
|
if (state.kind === "failed") return react.createElement("div", { style: S.card }, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), react.createElement("div", { style: S.error }, state.message), react.createElement("div", { style: S.bar }, react.createElement("button", {
|
|
477
587
|
style: S.btn,
|
|
@@ -481,7 +591,7 @@ window.__ModuleLoader__.load({
|
|
|
481
591
|
style: S.btn,
|
|
482
592
|
type: "button",
|
|
483
593
|
onClick: () => void onReset()
|
|
484
|
-
}, "重置为默认")), react.createElement("div", { style: S.version }, `${PLUGIN}
|
|
594
|
+
}, "重置为默认")), react.createElement("div", { style: S.version }, `${PLUGIN} 本页只配置;记账在对话里说。`));
|
|
485
595
|
const common = CONFIG_ITEMS.slice(0, COMMON_ITEM_COUNT);
|
|
486
596
|
const advanced = CONFIG_ITEMS.slice(COMMON_ITEM_COUNT);
|
|
487
597
|
const renderRow = (item) => react.createElement(Row, {
|
|
@@ -489,7 +599,8 @@ window.__ModuleLoader__.load({
|
|
|
489
599
|
item,
|
|
490
600
|
value: draft[item.key] ?? "",
|
|
491
601
|
disabled: busy,
|
|
492
|
-
onChange
|
|
602
|
+
onChange,
|
|
603
|
+
onBrowse
|
|
493
604
|
});
|
|
494
605
|
return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), react.createElement("details", { style: S.adv }, react.createElement("summary", { style: S.advSummary }, ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.muted }, ADVANCED_GROUP_NOTE), advanced.map(renderRow)), react.createElement("div", { style: S.bar }, react.createElement("button", {
|
|
495
606
|
style: dirty ? S.btnPrimary : S.btn,
|
|
@@ -506,25 +617,35 @@ window.__ModuleLoader__.load({
|
|
|
506
617
|
type: "button",
|
|
507
618
|
disabled: busy,
|
|
508
619
|
onClick: () => void load()
|
|
509
|
-
}, "重新读取")), notice !== null ? react.createElement("div", { style: S.okText }, notice) : null, error !== null ? react.createElement("div", { style: S.error }, error) : null, react.createElement("div", { style: S.version }, `${PLUGIN}
|
|
620
|
+
}, "重新读取")), picking ? react.createElement("div", { style: S.muted }, "已唤起系统文件夹对话框:选中后自动填上,取消则不动。") : null, notice !== null ? react.createElement("div", { style: S.okText }, notice) : null, error !== null ? react.createElement("div", { style: S.error }, error) : null, react.createElement("div", { style: S.version }, `${PLUGIN} 本页只配置;记账在对话里说。`));
|
|
510
621
|
}
|
|
511
622
|
function apply(ctx) {
|
|
512
623
|
const getCall = () => ctx.connection?.rpc?.call ?? null;
|
|
624
|
+
const getPicker = () => typeof ctx.get === "function" ? resolveDirectoryPicker((name) => ctx.get?.(name)) : null;
|
|
513
625
|
ctx.slots.inject("ilife.config-tab", () => ctx.slots.register({
|
|
514
626
|
name: "ilife.config-tab",
|
|
515
627
|
id: PLUGIN,
|
|
516
628
|
order: 95,
|
|
517
629
|
label: () => SLOT_TITLE,
|
|
518
630
|
channel: RPC_CHANNEL
|
|
519
|
-
}, () => react.createElement(BillConfig, {
|
|
631
|
+
}, () => react.createElement(BillConfig, {
|
|
632
|
+
getCall,
|
|
633
|
+
getPicker
|
|
634
|
+
})));
|
|
520
635
|
}
|
|
521
636
|
//#endregion
|
|
637
|
+
exports.Row = Row;
|
|
522
638
|
exports.apply = apply;
|
|
639
|
+
exports.createBrowseHandler = createBrowseHandler;
|
|
523
640
|
exports.fetchConfigSurface = fetchConfigSurface;
|
|
524
641
|
exports.fromDraft = fromDraft;
|
|
525
642
|
exports.humanizeConfigFailure = humanizeConfigFailure;
|
|
526
643
|
exports.inject = inject;
|
|
644
|
+
exports.isDirectoryPicker = isDirectoryPicker;
|
|
645
|
+
exports.pickDirectory = pickDirectory;
|
|
646
|
+
exports.readPickAnswer = readPickAnswer;
|
|
527
647
|
exports.resetConfigSurface = resetConfigSurface;
|
|
648
|
+
exports.resolveDirectoryPicker = resolveDirectoryPicker;
|
|
528
649
|
exports.saveConfigSurface = saveConfigSurface;
|
|
529
650
|
exports.toDraft = toDraft;
|
|
530
651
|
return module.exports;
|