dsh-calorie 0.3.3 → 0.3.5
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 +788 -64
- package/dist/client.js.map +1 -1
- package/package.json +7 -2
package/dist/client.js
CHANGED
|
@@ -186,6 +186,678 @@ window.__ModuleLoader__.load({
|
|
|
186
186
|
/** 宿主目录选择命名空间的服务名(#736;出处与禁令见 cookbook §13)。 */
|
|
187
187
|
const REMOTE_DIRECTORY_PICKER = "remote.directoryPicker";
|
|
188
188
|
//#endregion
|
|
189
|
+
//#region ../plugin-manager/dist/directory-browser-contract.js
|
|
190
|
+
/** 目录浏览器的纯逻辑半(**不认识宿主**:只认两格原语)。
|
|
191
|
+
*
|
|
192
|
+
* 为什么在这里:六个单品插件的设置页各有一批「值是一个目录」的行,它们要的界面是同一个东西。
|
|
193
|
+
* 界面的**取数**来自宿主给的两个原语(`list`/`createDirectory`),而这两个原语的形状是**调用方喂进来的**
|
|
194
|
+
* ——本件不 import 宿主的任何实现,也不认识 `remote.directoryPicker` 这个名字(那两件住在各家的
|
|
195
|
+
* `dsh-ctx.ts` 镜像里)。于是本件在 Windows 桌面、远程浏览器、SSH 三种部署下是同一份代码。
|
|
196
|
+
*
|
|
197
|
+
* 装什么:本次要浏览的那一层怎么走(进子目录/回上一级/跳面包屑)、路径怎么切出「目录部分」与
|
|
198
|
+
* 「筛选词」、回执怎么认。**不装**任何 IO、不装任何持久化——`list`/`createDirectory` 由调用方执行。
|
|
199
|
+
*
|
|
200
|
+
* 与平台自带浏览器件的关系:`@deepseek-ai/dsh-client-ui-directory-picker-browse` 的 `DirectoryBrowser`
|
|
201
|
+
* 是包内私有件(只经 `ui-workspace` 的流程空位暴露),插件侧取不到,所以这里是一份独立实现,
|
|
202
|
+
* 只对齐它的**交互结果**(进/选/新建),不对齐它的 DOM。
|
|
203
|
+
*/
|
|
204
|
+
/** 认一认某个值是不是可用的目录取数面(软依赖守卫用:认不出就当没有,绝不把页面带下来)。 */
|
|
205
|
+
function isBrowseFace(raw) {
|
|
206
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
207
|
+
const face = raw;
|
|
208
|
+
return typeof face.list === "function" && typeof face.createDirectory === "function";
|
|
209
|
+
}
|
|
210
|
+
/** 认一认某个值有没有 `pick`(对应宿主 composition 里的 `native` 能力)。 */
|
|
211
|
+
function hasPickFn(raw) {
|
|
212
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
213
|
+
return typeof raw.pick === "function";
|
|
214
|
+
}
|
|
215
|
+
/** 三态判定:先 native(有 pick),再 browse(有两格原语),都没有=`none`。 */
|
|
216
|
+
function pickerModeOf$1(raw) {
|
|
217
|
+
if (hasPickFn(raw)) return "native";
|
|
218
|
+
return isBrowseFace(raw) ? "browse" : "none";
|
|
219
|
+
}
|
|
220
|
+
/** 平台回执 → 三态(**永不抛**):成功回的是信封里的 `value` 不是路径;`ok:false` 是「供不了」不是「取消」。
|
|
221
|
+
*
|
|
222
|
+
* 出处:`@deepseek-ai/dsh-api-gateway/lib/client.js` 的 `invoke()`(失败不抛、成功也不回裸值),
|
|
223
|
+
* 第一方消费方 `@deepseek-ai/dsh-client-ui-workspace/lib/client.js:99-103` 就照这个信封拆。
|
|
224
|
+
* 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉(#743 的教训)。 */
|
|
225
|
+
function readPickAnswer$1(raw) {
|
|
226
|
+
if (typeof raw === "string") return raw.trim() === "" ? { kind: "cancelled" } : {
|
|
227
|
+
kind: "picked",
|
|
228
|
+
path: raw
|
|
229
|
+
};
|
|
230
|
+
const answer = typeof raw === "object" && raw !== null ? raw : {};
|
|
231
|
+
if (answer.ok === true) {
|
|
232
|
+
const value = answer.value;
|
|
233
|
+
return typeof value === "string" && value.trim() !== "" ? {
|
|
234
|
+
kind: "picked",
|
|
235
|
+
path: value
|
|
236
|
+
} : { kind: "cancelled" };
|
|
237
|
+
}
|
|
238
|
+
const detail = answer.error?.message?.trim() ?? "";
|
|
239
|
+
return {
|
|
240
|
+
kind: "unavailable",
|
|
241
|
+
message: "打不开系统文件夹对话框" + (detail === "" ? "" : "(" + detail + ")") + ":请直接在框里填绝对路径。"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** 一段路径里最后那个分隔符的位置(Windows 上 `\` 与 `/` 都算;POSIX 上 `\` 是合法文件名字符,不算)。 */
|
|
245
|
+
function lastSeparator(path) {
|
|
246
|
+
const back = path.lastIndexOf("\\");
|
|
247
|
+
const slash = path.lastIndexOf("/");
|
|
248
|
+
return back > slash ? back : slash;
|
|
249
|
+
}
|
|
250
|
+
/** 上一层:`'C:\\a\\b\\'` → `'C:\\a'`;已经是根则回自身;相对名(没有任何分隔符)也回自身。
|
|
251
|
+
*
|
|
252
|
+
* 尾分隔符先吃掉再切——否则「回上一级」会在原地打转。**盘根单独认**:`C:\` 掐掉尾分隔符会剩成
|
|
253
|
+
* `C:`,而 `C:` 是「当前盘」不是「上一层」,回它就会多跑一趟、还可能跨到别的盘上去。
|
|
254
|
+
* UNC(`\\server\share`)不特殊处理:切到头就是共享名本身,浏览停在共享根。 */
|
|
255
|
+
function parentOf(path) {
|
|
256
|
+
const trimmed = path.replace(/[\\/]+$/, "");
|
|
257
|
+
if (trimmed === "") return path;
|
|
258
|
+
const sep = path.includes("\\") ? "\\" : "/";
|
|
259
|
+
if (/^[A-Za-z]:$/.test(trimmed)) return trimmed + sep;
|
|
260
|
+
const cut = lastSeparator(trimmed);
|
|
261
|
+
if (cut === -1) return trimmed;
|
|
262
|
+
if (cut === 0) return path.slice(0, 1);
|
|
263
|
+
if (cut === 2 && /^[A-Za-z]:/.test(trimmed)) return trimmed.slice(0, 2) + sep;
|
|
264
|
+
return trimmed.slice(0, cut);
|
|
265
|
+
}
|
|
266
|
+
/** 一层列举里,哪些行可进:隐藏目录按开关过滤,其余照宿主给的顺序。 */
|
|
267
|
+
function visibleEntries(listing, showHidden) {
|
|
268
|
+
return showHidden ? listing.entries : listing.entries.filter((entry) => !entry.hidden);
|
|
269
|
+
}
|
|
270
|
+
/** 这一层有多少个被藏起来的目录(用来决定「显示隐藏目录」那个开关要不要出现在图上)。 */
|
|
271
|
+
function hiddenCount(listing) {
|
|
272
|
+
return listing.entries.filter((entry) => entry.hidden).length;
|
|
273
|
+
}
|
|
274
|
+
/** 拿一段文本按「最后一个分隔符」切成「目录部分 + 筛选词」。
|
|
275
|
+
*
|
|
276
|
+
* 没打过任何分隔符时目录部分=null(那段文字还不指任何目录,只当筛选词用)。
|
|
277
|
+
* Windows 上 `/` 也当分隔符(宿主那边两种都收),POSIX 上不。 */
|
|
278
|
+
function splitDraft(draft) {
|
|
279
|
+
const cut = lastSeparator(draft);
|
|
280
|
+
if (cut === -1) return {
|
|
281
|
+
directory: null,
|
|
282
|
+
filter: draft
|
|
283
|
+
};
|
|
284
|
+
return {
|
|
285
|
+
directory: draft.slice(0, cut + 1),
|
|
286
|
+
filter: draft.slice(cut + 1)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** 按筛选词过一层目录(大小写不敏感;空筛选词=全留)。 */
|
|
290
|
+
function filterEntries(entries, filter) {
|
|
291
|
+
const needle = filter.trim().toLowerCase();
|
|
292
|
+
if (needle === "") return entries;
|
|
293
|
+
return entries.filter((entry) => entry.name.toLowerCase().includes(needle));
|
|
294
|
+
}
|
|
295
|
+
/** 给界面用的一句话位置说明:优先显示相对 home 的说法,省得整条绝对路径占满一行。 */
|
|
296
|
+
function locationLabel(listing) {
|
|
297
|
+
const home = listing.home.replace(/[\\/]+$/, "");
|
|
298
|
+
if (home !== "" && listing.path !== home && listing.path.startsWith(home)) {
|
|
299
|
+
const tail = listing.path.slice(home.length).replace(/^[\\/]+/, "");
|
|
300
|
+
if (tail !== "") return "~" + (listing.path.includes("\\") ? "\\" : "/") + tail;
|
|
301
|
+
}
|
|
302
|
+
return listing.path;
|
|
303
|
+
}
|
|
304
|
+
/** 新建文件夹的名字合不合法(单个路径段:不许分隔符、不许空、不许 `.`/`..`)。
|
|
305
|
+
*
|
|
306
|
+
* 与宿主 browse 后端的校验同口径——这里先拦一道,是为了在图上给得出人话,不是替代宿主校验。 */
|
|
307
|
+
function validateFolderName(name) {
|
|
308
|
+
const trimmed = name.trim();
|
|
309
|
+
if (trimmed === "") return {
|
|
310
|
+
ok: false,
|
|
311
|
+
reason: "文件夹名不能为空。"
|
|
312
|
+
};
|
|
313
|
+
if (/[\\/]/.test(trimmed)) return {
|
|
314
|
+
ok: false,
|
|
315
|
+
reason: "文件夹名里不能带路径分隔符。"
|
|
316
|
+
};
|
|
317
|
+
if (trimmed === "." || trimmed === "..") return {
|
|
318
|
+
ok: false,
|
|
319
|
+
reason: "「.」与「..」不是文件夹名。"
|
|
320
|
+
};
|
|
321
|
+
return { ok: true };
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region ../plugin-manager/dist/directory-browser-state.js
|
|
325
|
+
/** 文件浏览器的操作半:把「进哪一层、这一层显示什么、选到了什么」算成状态,**IO 全部由调用方注入**。
|
|
326
|
+
*
|
|
327
|
+
* 分两层的原因:本件(操作)在 Node 里就能直测——喂一个假的取数面,就能把「进目录/回上一级/
|
|
328
|
+
* 跳面包屑/筛选/新建文件夹/失败出人话」全跑一遍;视图那半(`directory-browser-ui.ts`)
|
|
329
|
+
* 只负责把这份状态画成 DOM。两边都不认识宿主。
|
|
330
|
+
*
|
|
331
|
+
* 数据来源只有两格:`list(path?)` 与 `createDirectory(path, name)`(调用方注入的 `DirectoryBrowseFace`)。
|
|
332
|
+
*/
|
|
333
|
+
/** 建一个浏览器控制器。取数、回调、初值全部由调用方给。 */
|
|
334
|
+
function createBrowseController(deps) {
|
|
335
|
+
const label = deps.failureLabel ?? "浏览失败";
|
|
336
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
337
|
+
let state = {
|
|
338
|
+
phase: "idle",
|
|
339
|
+
listing: null,
|
|
340
|
+
failure: null,
|
|
341
|
+
selected: null,
|
|
342
|
+
showHidden: false,
|
|
343
|
+
draft: deps.initialPath,
|
|
344
|
+
filter: "",
|
|
345
|
+
creating: null,
|
|
346
|
+
notice: null
|
|
347
|
+
};
|
|
348
|
+
/** 每次列举带一个序号:晚回来的旧回执直接丢掉,不许覆盖新一层。 */
|
|
349
|
+
let generation = 0;
|
|
350
|
+
const emit = (next) => {
|
|
351
|
+
state = {
|
|
352
|
+
...state,
|
|
353
|
+
...next
|
|
354
|
+
};
|
|
355
|
+
for (const listener of [...listeners]) listener(state);
|
|
356
|
+
};
|
|
357
|
+
const humanize = (cause) => {
|
|
358
|
+
const raw = typeof cause === "object" && cause !== null ? cause : {};
|
|
359
|
+
const message = typeof raw.message === "string" && raw.message.trim() !== "" ? raw.message.trim() : String(cause);
|
|
360
|
+
return {
|
|
361
|
+
code: typeof raw.code === "string" && raw.code !== "" ? raw.code : "browse-failed",
|
|
362
|
+
message: `${label}:${message}`
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const load = async (path) => {
|
|
366
|
+
const mine = ++generation;
|
|
367
|
+
emit({
|
|
368
|
+
phase: "loading",
|
|
369
|
+
failure: null,
|
|
370
|
+
notice: null
|
|
371
|
+
});
|
|
372
|
+
try {
|
|
373
|
+
const listing = path === null ? await deps.face.list() : await deps.face.list(path);
|
|
374
|
+
if (mine !== generation) return;
|
|
375
|
+
emit({
|
|
376
|
+
phase: "ready",
|
|
377
|
+
listing,
|
|
378
|
+
failure: null,
|
|
379
|
+
selected: null,
|
|
380
|
+
draft: listing.path,
|
|
381
|
+
filter: ""
|
|
382
|
+
});
|
|
383
|
+
} catch (cause) {
|
|
384
|
+
if (mine !== generation) return;
|
|
385
|
+
emit({
|
|
386
|
+
phase: "failed",
|
|
387
|
+
listing: null,
|
|
388
|
+
failure: humanize(cause),
|
|
389
|
+
selected: null
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/** 草稿指到的那一层已经列举过时,按末段筛当前这一层的行;否则不过滤。
|
|
394
|
+
*
|
|
395
|
+
* 两边都先掐掉尾分隔符再比:草稿那边切开后天然带尾分隔符(`C:\a\` + `b`),
|
|
396
|
+
* 而这一层的 `path` 由宿主给、不带尾分隔符。 */
|
|
397
|
+
const applyDraft = (draft) => {
|
|
398
|
+
const { directory, filter } = splitDraft(draft);
|
|
399
|
+
const listing = state.listing;
|
|
400
|
+
const sameLevel = listing !== null && directory !== null && normalize(directory) === normalize(listing.path);
|
|
401
|
+
emit({
|
|
402
|
+
draft,
|
|
403
|
+
filter: sameLevel ? filter : ""
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
const normalize = (path) => path.replace(/[\\/]+$/, "");
|
|
407
|
+
return {
|
|
408
|
+
getState: () => state,
|
|
409
|
+
subscribe(listener) {
|
|
410
|
+
listeners.add(listener);
|
|
411
|
+
return () => listeners.delete(listener);
|
|
412
|
+
},
|
|
413
|
+
open: () => load(null),
|
|
414
|
+
enter: (path) => load(path),
|
|
415
|
+
async up() {
|
|
416
|
+
const listing = state.listing;
|
|
417
|
+
if (listing === null) return;
|
|
418
|
+
const parent = parentOf(listing.path);
|
|
419
|
+
if (parent === listing.path) return;
|
|
420
|
+
await load(parent);
|
|
421
|
+
},
|
|
422
|
+
pick() {
|
|
423
|
+
const target = state.selected ?? state.listing?.path ?? null;
|
|
424
|
+
if (target === null) return;
|
|
425
|
+
deps.onPicked(target);
|
|
426
|
+
},
|
|
427
|
+
cancel() {
|
|
428
|
+
deps.onClose();
|
|
429
|
+
},
|
|
430
|
+
close() {
|
|
431
|
+
deps.onClose();
|
|
432
|
+
},
|
|
433
|
+
select(path) {
|
|
434
|
+
emit({
|
|
435
|
+
selected: state.selected === path ? null : path,
|
|
436
|
+
notice: null
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
toggleHidden() {
|
|
440
|
+
emit({ showHidden: !state.showHidden });
|
|
441
|
+
},
|
|
442
|
+
setDraft(draft) {
|
|
443
|
+
applyDraft(draft);
|
|
444
|
+
},
|
|
445
|
+
async commitDraft() {
|
|
446
|
+
const trimmed = state.draft.trim();
|
|
447
|
+
if (trimmed === "") return;
|
|
448
|
+
await load(trimmed);
|
|
449
|
+
},
|
|
450
|
+
setCreating(name) {
|
|
451
|
+
emit({
|
|
452
|
+
creating: name,
|
|
453
|
+
notice: null
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
async createFolder(name) {
|
|
457
|
+
const checked = validateFolderName(name);
|
|
458
|
+
if (!checked.ok) {
|
|
459
|
+
emit({ notice: checked.reason });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const base = state.selected ?? state.listing?.path ?? null;
|
|
463
|
+
if (base === null) {
|
|
464
|
+
emit({ notice: "还没有打开任何目录,无法新建文件夹。" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const created = await deps.face.createDirectory(base, name.trim());
|
|
469
|
+
emit({
|
|
470
|
+
creating: null,
|
|
471
|
+
notice: null
|
|
472
|
+
});
|
|
473
|
+
await load(created);
|
|
474
|
+
emit({ selected: created });
|
|
475
|
+
} catch (cause) {
|
|
476
|
+
const failure = humanize(cause);
|
|
477
|
+
emit({
|
|
478
|
+
notice: failure.message,
|
|
479
|
+
creating: null
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** 图上这一层要画的行(按隐藏开关与草稿末段过滤之后的)。 */
|
|
486
|
+
function rowsOf(state) {
|
|
487
|
+
if (state.listing === null) return [];
|
|
488
|
+
return filterEntries(visibleEntries(state.listing, state.showHidden), state.filter);
|
|
489
|
+
}
|
|
490
|
+
/** 「打开」这颗按钮的目标:选中行优先,没选中就是当前层。 */
|
|
491
|
+
function targetOf(state) {
|
|
492
|
+
return state.selected ?? state.listing?.path ?? null;
|
|
493
|
+
}
|
|
494
|
+
/** 一段路径拼成可读的一句话(给用例与调试用;图上不直接显示)。 */
|
|
495
|
+
function describe(state) {
|
|
496
|
+
if (state.phase === "failed") return `${state.failure?.code ?? "browse-failed"} @ ${state.draft}`;
|
|
497
|
+
if (state.listing === null) return `${state.phase}`;
|
|
498
|
+
return `${state.listing.path}(${rowsOf(state).length} 行)`;
|
|
499
|
+
}
|
|
500
|
+
/** 建一份「目录行接线」:给一个取数面与初值,回一组动作。
|
|
501
|
+
*
|
|
502
|
+
* 只做接线:把控制器算出来的状态与回调打包好,不取数、不画图、不认宿主。 */
|
|
503
|
+
function createDirectoryRowBrowser(deps) {
|
|
504
|
+
const controller = createBrowseController({
|
|
505
|
+
face: deps.face,
|
|
506
|
+
initialPath: deps.initialPath,
|
|
507
|
+
onPicked: deps.onPicked,
|
|
508
|
+
onClose: () => deps.onClosed?.(),
|
|
509
|
+
...deps.failureLabel === void 0 ? {} : { failureLabel: deps.failureLabel }
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
open: () => controller.open(),
|
|
513
|
+
state: () => controller.getState(),
|
|
514
|
+
summary: () => describe(controller.getState()),
|
|
515
|
+
actions: {
|
|
516
|
+
onPick: () => controller.pick(),
|
|
517
|
+
onClose: () => controller.cancel(),
|
|
518
|
+
onEnter: (path) => void controller.enter(path),
|
|
519
|
+
onUp: () => void controller.up(),
|
|
520
|
+
onSelect: (path) => controller.select(path),
|
|
521
|
+
onToggleHidden: () => controller.toggleHidden(),
|
|
522
|
+
onDraft: (draft) => controller.setDraft(draft),
|
|
523
|
+
onCommitDraft: () => void controller.commitDraft(),
|
|
524
|
+
onCreate: (name) => void controller.createFolder(name),
|
|
525
|
+
onCreatingChange: (name) => controller.setCreating(name)
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region ../plugin-manager/dist/directory-browser-ui.js
|
|
531
|
+
/** 文件浏览器的视图半:把 `BrowseState` 画成一张对话框。**不认识宿主**,也不自己做 IO。
|
|
532
|
+
*
|
|
533
|
+
* 手写 `React.createElement`(本包 client 束禁 JSX,见 `tsdown.config.ts` 的既成口径)。
|
|
534
|
+
* 只读状态、只回调;进哪一层、选到什么由操作半(`directory-browser-state.ts`)算。
|
|
535
|
+
*/
|
|
536
|
+
const EMPTY_LISTING = {
|
|
537
|
+
path: "",
|
|
538
|
+
home: "",
|
|
539
|
+
crumbs: [],
|
|
540
|
+
entries: [],
|
|
541
|
+
truncated: false
|
|
542
|
+
};
|
|
543
|
+
const S$1 = {
|
|
544
|
+
scrim: {
|
|
545
|
+
position: "fixed",
|
|
546
|
+
inset: 0,
|
|
547
|
+
background: "rgba(0,0,0,0.35)",
|
|
548
|
+
display: "flex",
|
|
549
|
+
alignItems: "center",
|
|
550
|
+
justifyContent: "center",
|
|
551
|
+
zIndex: 9999
|
|
552
|
+
},
|
|
553
|
+
dialog: {
|
|
554
|
+
width: 680,
|
|
555
|
+
maxWidth: "92vw",
|
|
556
|
+
height: 500,
|
|
557
|
+
maxHeight: "86vh",
|
|
558
|
+
display: "flex",
|
|
559
|
+
flexDirection: "column",
|
|
560
|
+
background: "var(--dsw-alias-bg-layer-1, #1f1f23)",
|
|
561
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
562
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
563
|
+
borderRadius: 10,
|
|
564
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.35)",
|
|
565
|
+
overflow: "hidden"
|
|
566
|
+
},
|
|
567
|
+
head: {
|
|
568
|
+
display: "flex",
|
|
569
|
+
alignItems: "center",
|
|
570
|
+
gap: 8,
|
|
571
|
+
padding: "10px 12px",
|
|
572
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
573
|
+
},
|
|
574
|
+
title: {
|
|
575
|
+
fontSize: 14,
|
|
576
|
+
fontWeight: 700
|
|
577
|
+
},
|
|
578
|
+
headTail: {
|
|
579
|
+
marginLeft: "auto",
|
|
580
|
+
fontSize: 12,
|
|
581
|
+
opacity: .72,
|
|
582
|
+
overflow: "hidden",
|
|
583
|
+
textOverflow: "ellipsis",
|
|
584
|
+
whiteSpace: "nowrap"
|
|
585
|
+
},
|
|
586
|
+
crumbs: {
|
|
587
|
+
display: "flex",
|
|
588
|
+
flexWrap: "wrap",
|
|
589
|
+
alignItems: "center",
|
|
590
|
+
gap: 2,
|
|
591
|
+
padding: "8px 12px 0",
|
|
592
|
+
fontSize: 12
|
|
593
|
+
},
|
|
594
|
+
chromeButton: {
|
|
595
|
+
background: "transparent",
|
|
596
|
+
border: "none",
|
|
597
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
598
|
+
cursor: "pointer",
|
|
599
|
+
padding: "2px 4px",
|
|
600
|
+
fontSize: 12,
|
|
601
|
+
opacity: .85
|
|
602
|
+
},
|
|
603
|
+
crumbSep: {
|
|
604
|
+
opacity: .45,
|
|
605
|
+
fontSize: 12
|
|
606
|
+
},
|
|
607
|
+
pathRow: {
|
|
608
|
+
display: "flex",
|
|
609
|
+
gap: 6,
|
|
610
|
+
padding: "8px 12px"
|
|
611
|
+
},
|
|
612
|
+
input: {
|
|
613
|
+
flex: 1,
|
|
614
|
+
minWidth: 0,
|
|
615
|
+
fontSize: 12,
|
|
616
|
+
padding: "5px 8px",
|
|
617
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.12))",
|
|
618
|
+
color: "inherit",
|
|
619
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
620
|
+
borderRadius: 6
|
|
621
|
+
},
|
|
622
|
+
button: {
|
|
623
|
+
fontSize: 12,
|
|
624
|
+
padding: "5px 10px",
|
|
625
|
+
cursor: "pointer",
|
|
626
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.12))",
|
|
627
|
+
color: "inherit",
|
|
628
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
629
|
+
borderRadius: 6
|
|
630
|
+
},
|
|
631
|
+
buttonOff: {
|
|
632
|
+
opacity: .45,
|
|
633
|
+
cursor: "default"
|
|
634
|
+
},
|
|
635
|
+
list: {
|
|
636
|
+
flex: 1,
|
|
637
|
+
minHeight: 0,
|
|
638
|
+
overflowY: "auto",
|
|
639
|
+
margin: "0 12px",
|
|
640
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
|
|
641
|
+
borderRadius: 6
|
|
642
|
+
},
|
|
643
|
+
row: {
|
|
644
|
+
display: "flex",
|
|
645
|
+
alignItems: "center",
|
|
646
|
+
gap: 8,
|
|
647
|
+
width: "100%",
|
|
648
|
+
padding: "6px 10px",
|
|
649
|
+
textAlign: "left",
|
|
650
|
+
background: "transparent",
|
|
651
|
+
color: "inherit",
|
|
652
|
+
border: "none",
|
|
653
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
|
|
654
|
+
cursor: "pointer",
|
|
655
|
+
fontSize: 13
|
|
656
|
+
},
|
|
657
|
+
rowSelected: { background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.18))" },
|
|
658
|
+
empty: {
|
|
659
|
+
padding: "14px 12px",
|
|
660
|
+
fontSize: 12,
|
|
661
|
+
opacity: .7
|
|
662
|
+
},
|
|
663
|
+
footNote: {
|
|
664
|
+
padding: "6px 12px 0",
|
|
665
|
+
fontSize: 12,
|
|
666
|
+
minHeight: 20
|
|
667
|
+
},
|
|
668
|
+
foot: {
|
|
669
|
+
display: "flex",
|
|
670
|
+
alignItems: "center",
|
|
671
|
+
gap: 8,
|
|
672
|
+
padding: "10px 12px",
|
|
673
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
674
|
+
},
|
|
675
|
+
target: {
|
|
676
|
+
marginRight: "auto",
|
|
677
|
+
fontSize: 12,
|
|
678
|
+
opacity: .8,
|
|
679
|
+
overflow: "hidden",
|
|
680
|
+
textOverflow: "ellipsis",
|
|
681
|
+
whiteSpace: "nowrap"
|
|
682
|
+
},
|
|
683
|
+
error: { color: "var(--dsw-alias-label-error, #e66)" }
|
|
684
|
+
};
|
|
685
|
+
function crumbRow(state, labels, onEnter) {
|
|
686
|
+
const listing = state.listing;
|
|
687
|
+
if (listing === null || listing.crumbs.length === 0) return null;
|
|
688
|
+
const parts = [];
|
|
689
|
+
listing.crumbs.forEach((crumb, index) => {
|
|
690
|
+
if (index > 0) parts.push(react.createElement("span", {
|
|
691
|
+
key: "sep-" + crumb.path,
|
|
692
|
+
style: S$1.crumbSep
|
|
693
|
+
}, "›"));
|
|
694
|
+
const isCurrent = crumb.path === listing.path;
|
|
695
|
+
parts.push(react.createElement("button", {
|
|
696
|
+
key: crumb.path,
|
|
697
|
+
type: "button",
|
|
698
|
+
style: {
|
|
699
|
+
...S$1.chromeButton,
|
|
700
|
+
fontWeight: isCurrent ? 700 : 400
|
|
701
|
+
},
|
|
702
|
+
title: crumb.path,
|
|
703
|
+
onClick: () => onEnter(crumb.path)
|
|
704
|
+
}, index === 0 ? labels.up : crumb.name));
|
|
705
|
+
});
|
|
706
|
+
return react.createElement("div", { style: S$1.crumbs }, parts);
|
|
707
|
+
}
|
|
708
|
+
function entryRow(entry, selected, labels, onToggleSelect, onEnter) {
|
|
709
|
+
return react.createElement("div", {
|
|
710
|
+
key: entry.path,
|
|
711
|
+
style: {
|
|
712
|
+
...S$1.row,
|
|
713
|
+
padding: 0,
|
|
714
|
+
...selected ? S$1.rowSelected : {}
|
|
715
|
+
}
|
|
716
|
+
}, react.createElement("button", {
|
|
717
|
+
type: "button",
|
|
718
|
+
style: {
|
|
719
|
+
...S$1.row,
|
|
720
|
+
flex: 1,
|
|
721
|
+
border: "none"
|
|
722
|
+
},
|
|
723
|
+
onClick: () => onEnter(entry.path)
|
|
724
|
+
}, "📁 " + entry.name), react.createElement("button", {
|
|
725
|
+
type: "button",
|
|
726
|
+
style: {
|
|
727
|
+
...S$1.button,
|
|
728
|
+
marginRight: 8,
|
|
729
|
+
padding: "2px 8px"
|
|
730
|
+
},
|
|
731
|
+
"aria-pressed": selected,
|
|
732
|
+
title: selected ? labels.selected : labels.select,
|
|
733
|
+
onClick: () => onToggleSelect(entry.path)
|
|
734
|
+
}, selected ? "✓" : labels.select));
|
|
735
|
+
}
|
|
736
|
+
function createRow(labels, onName, onCancel, onCreate, creatingName) {
|
|
737
|
+
if (creatingName === null) return null;
|
|
738
|
+
return react.createElement("div", { style: {
|
|
739
|
+
...S$1.pathRow,
|
|
740
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
741
|
+
} }, react.createElement("input", {
|
|
742
|
+
style: S$1.input,
|
|
743
|
+
value: creatingName,
|
|
744
|
+
placeholder: labels.newFolder,
|
|
745
|
+
"aria-label": labels.newFolder,
|
|
746
|
+
onChange: (event) => onName(event.currentTarget.value),
|
|
747
|
+
onKeyDown: (event) => {
|
|
748
|
+
if (event.key === "Enter") onCreate(creatingName);
|
|
749
|
+
if (event.key === "Escape") onCancel();
|
|
750
|
+
}
|
|
751
|
+
}), react.createElement("button", {
|
|
752
|
+
type: "button",
|
|
753
|
+
style: S$1.button,
|
|
754
|
+
onClick: () => onCreate(creatingName)
|
|
755
|
+
}, labels.createConfirm), react.createElement("button", {
|
|
756
|
+
type: "button",
|
|
757
|
+
style: S$1.button,
|
|
758
|
+
onClick: onCancel
|
|
759
|
+
}, labels.createCancel));
|
|
760
|
+
}
|
|
761
|
+
/** 画一张文件浏览器对话框。`open=false` 时返回 null(本件不挂 portal,由调用方决定挂在哪)。 */
|
|
762
|
+
function DirectoryBrowser(props) {
|
|
763
|
+
const { open, state, labels } = props;
|
|
764
|
+
if (!open) return null;
|
|
765
|
+
const rows = rowsOf(state);
|
|
766
|
+
const target = targetOf(state);
|
|
767
|
+
const listing = state.listing ?? EMPTY_LISTING;
|
|
768
|
+
const hidden = hiddenCount(listing);
|
|
769
|
+
const body = [];
|
|
770
|
+
body.push(crumbRow(state, labels, props.onEnter));
|
|
771
|
+
body.push(react.createElement("div", { style: S$1.pathRow }, react.createElement("input", {
|
|
772
|
+
style: S$1.input,
|
|
773
|
+
value: state.draft,
|
|
774
|
+
placeholder: labels.pathPlaceholder,
|
|
775
|
+
"aria-label": labels.pathPlaceholder,
|
|
776
|
+
onChange: (event) => props.onDraft(event.currentTarget.value),
|
|
777
|
+
onKeyDown: (event) => {
|
|
778
|
+
if (event.key === "Enter") props.onCommitDraft();
|
|
779
|
+
}
|
|
780
|
+
}), react.createElement("button", {
|
|
781
|
+
type: "button",
|
|
782
|
+
style: S$1.button,
|
|
783
|
+
onClick: props.onUp
|
|
784
|
+
}, labels.up), react.createElement("button", {
|
|
785
|
+
type: "button",
|
|
786
|
+
style: S$1.button,
|
|
787
|
+
onClick: props.onCommitDraft
|
|
788
|
+
}, labels.go)));
|
|
789
|
+
body.push(react.createElement("div", {
|
|
790
|
+
style: S$1.list,
|
|
791
|
+
role: "listbox",
|
|
792
|
+
"aria-label": labels.title
|
|
793
|
+
}, rows.length === 0 ? react.createElement("div", { style: S$1.empty }, state.phase === "loading" ? labels.loading : labels.empty) : rows.map((entry) => entryRow(entry, state.selected === entry.path, labels, props.onSelect, props.onEnter))));
|
|
794
|
+
body.push(createRow(labels, props.onDraft, () => props.onCreatingChange(null), props.onCreate, state.creating));
|
|
795
|
+
body.push(react.createElement("div", { style: S$1.footNote }, hidden > 0 ? react.createElement("label", { style: {
|
|
796
|
+
fontSize: 12,
|
|
797
|
+
cursor: "pointer",
|
|
798
|
+
marginRight: 10
|
|
799
|
+
} }, react.createElement("input", {
|
|
800
|
+
type: "checkbox",
|
|
801
|
+
checked: state.showHidden,
|
|
802
|
+
onChange: props.onToggleHidden
|
|
803
|
+
}), " " + labels.showHidden(hidden)) : null, state.creating === null ? react.createElement("button", {
|
|
804
|
+
type: "button",
|
|
805
|
+
style: {
|
|
806
|
+
...S$1.chromeButton,
|
|
807
|
+
textDecoration: "underline"
|
|
808
|
+
},
|
|
809
|
+
onClick: () => props.onCreatingChange("")
|
|
810
|
+
}, labels.newFolder) : null, state.notice !== null ? react.createElement("div", { style: S$1.error }, state.notice) : null, state.phase === "failed" && state.failure !== null ? react.createElement("div", { style: S$1.error }, state.failure.message) : null));
|
|
811
|
+
body.push(react.createElement("div", { style: S$1.foot }, react.createElement("span", {
|
|
812
|
+
style: S$1.target,
|
|
813
|
+
title: target ?? ""
|
|
814
|
+
}, target === null ? "" : labels.willPick + target), react.createElement("button", {
|
|
815
|
+
type: "button",
|
|
816
|
+
style: {
|
|
817
|
+
...S$1.button,
|
|
818
|
+
...target === null ? S$1.buttonOff : {}
|
|
819
|
+
},
|
|
820
|
+
onClick: props.onPick
|
|
821
|
+
}, labels.open), react.createElement("button", {
|
|
822
|
+
type: "button",
|
|
823
|
+
style: S$1.button,
|
|
824
|
+
onClick: props.onClose
|
|
825
|
+
}, labels.cancel)));
|
|
826
|
+
return react.createElement("div", {
|
|
827
|
+
style: S$1.scrim,
|
|
828
|
+
role: "presentation",
|
|
829
|
+
onMouseDown: (event) => {
|
|
830
|
+
if (event.target === event.currentTarget) props.onClose();
|
|
831
|
+
}
|
|
832
|
+
}, react.createElement("div", {
|
|
833
|
+
style: S$1.dialog,
|
|
834
|
+
role: "dialog",
|
|
835
|
+
"aria-modal": true,
|
|
836
|
+
"aria-label": labels.title
|
|
837
|
+
}, react.createElement("div", { style: S$1.head }, react.createElement("span", { style: S$1.title }, labels.title), react.createElement("span", {
|
|
838
|
+
style: S$1.headTail,
|
|
839
|
+
title: listing.path
|
|
840
|
+
}, state.listing === null ? "" : locationLabel(listing)), react.createElement("button", {
|
|
841
|
+
type: "button",
|
|
842
|
+
style: S$1.button,
|
|
843
|
+
"aria-label": labels.close,
|
|
844
|
+
title: labels.close,
|
|
845
|
+
onClick: props.onClose
|
|
846
|
+
}, "✕")), body));
|
|
847
|
+
}
|
|
848
|
+
/** 目录行接线的 React 半边:把操作半那组动作直接喂给组件。
|
|
849
|
+
*
|
|
850
|
+
* 操作半(`directory-browser-state.ts` 的 `createDirectoryRowBrowser`)不碰 React,
|
|
851
|
+
* 这里只把它的 `actions` 摊进 props —— 于是「进哪一层」那套在 Node 里可测,这里只做接线。 */
|
|
852
|
+
function DirectoryBrowserFromRow(props) {
|
|
853
|
+
return DirectoryBrowser({
|
|
854
|
+
open: props.open,
|
|
855
|
+
state: props.row.state(),
|
|
856
|
+
labels: props.labels,
|
|
857
|
+
...props.row.actions
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
189
861
|
//#region src/client.ts
|
|
190
862
|
/** dsh-calorie client 适配器(六边形:port=contract+dsh-ctx 镜像,adapter=本文件)。
|
|
191
863
|
*
|
|
@@ -213,7 +885,10 @@ window.__ModuleLoader__.load({
|
|
|
213
885
|
/** client 短名声明:只有这两个(#736 的目录选择走**可选查找**,不写进来——
|
|
214
886
|
* 写进来=硬依赖,提供方缺席时整包被停靠,设置页会跟着装不上;见 cookbook §13)。 */
|
|
215
887
|
const inject = ["slots", "connection"];
|
|
216
|
-
/** 面板视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退)。
|
|
888
|
+
/** 面板视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退)。
|
|
889
|
+
*
|
|
890
|
+
* **六家逐项同形**(备忘·卡路里·记账·作息·居家·大厨):同一项在这六份里逐字相同,
|
|
891
|
+
* 改任一条要六家一起改,锁见 `test/panel-copy-743.test.mjs` 第 ⑤ 条。 */
|
|
217
892
|
const S = {
|
|
218
893
|
card: {
|
|
219
894
|
padding: "12px 14px",
|
|
@@ -238,10 +913,13 @@ window.__ModuleLoader__.load({
|
|
|
238
913
|
fontSize: "0.92em"
|
|
239
914
|
},
|
|
240
915
|
error: {
|
|
241
|
-
|
|
242
|
-
|
|
916
|
+
marginTop: 8,
|
|
917
|
+
color: "var(--dsw-alias-label-error, #b3261e)",
|
|
918
|
+
fontSize: "1em",
|
|
919
|
+
whiteSpace: "pre-wrap"
|
|
243
920
|
},
|
|
244
921
|
okText: {
|
|
922
|
+
marginTop: 8,
|
|
245
923
|
color: "var(--dsw-alias-state-success-primary, #12805c)",
|
|
246
924
|
fontSize: "0.96em"
|
|
247
925
|
},
|
|
@@ -258,16 +936,16 @@ window.__ModuleLoader__.load({
|
|
|
258
936
|
fontSize: "0.92em"
|
|
259
937
|
},
|
|
260
938
|
info: {
|
|
261
|
-
fontSize: "0.92em",
|
|
262
939
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)",
|
|
940
|
+
fontSize: "0.92em",
|
|
263
941
|
overflowWrap: "anywhere"
|
|
264
942
|
},
|
|
265
|
-
row: {
|
|
943
|
+
row: { marginBottom: 10 },
|
|
266
944
|
label: { fontWeight: 600 },
|
|
267
945
|
hint: {
|
|
268
|
-
color: "var(--dsw-alias-label-
|
|
946
|
+
color: "var(--dsw-alias-label-secondary, #9a9a9a)",
|
|
269
947
|
fontSize: "0.92em",
|
|
270
|
-
|
|
948
|
+
marginBottom: 4
|
|
271
949
|
},
|
|
272
950
|
input: {
|
|
273
951
|
width: "100%",
|
|
@@ -294,34 +972,29 @@ window.__ModuleLoader__.load({
|
|
|
294
972
|
cursor: "pointer"
|
|
295
973
|
},
|
|
296
974
|
bar: {
|
|
297
|
-
marginTop: 14,
|
|
298
975
|
display: "flex",
|
|
299
976
|
gap: 8,
|
|
300
|
-
|
|
977
|
+
marginTop: 12,
|
|
301
978
|
flexWrap: "wrap"
|
|
302
979
|
},
|
|
303
980
|
btn: {
|
|
304
|
-
padding: "
|
|
305
|
-
borderRadius:
|
|
981
|
+
padding: "4px 12px",
|
|
982
|
+
borderRadius: 6,
|
|
306
983
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
|
|
307
984
|
background: "var(--dsw-alias-bg-base, transparent)",
|
|
308
985
|
color: "var(--dsw-alias-label-primary, inherit)",
|
|
309
986
|
cursor: "pointer"
|
|
310
987
|
},
|
|
311
988
|
btnPrimary: {
|
|
312
|
-
padding: "
|
|
313
|
-
borderRadius:
|
|
989
|
+
padding: "4px 12px",
|
|
990
|
+
borderRadius: 6,
|
|
314
991
|
border: "1px solid var(--dsw-alias-brand-primary, #2f6fed)",
|
|
315
992
|
background: "var(--dsw-alias-brand-primary, #2f6fed)",
|
|
316
993
|
color: "#fff",
|
|
317
994
|
cursor: "pointer"
|
|
318
995
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
paddingTop: 8,
|
|
322
|
-
borderTop: "1px solid var(--dsw-alias-border, rgba(128,128,128,.25))"
|
|
323
|
-
},
|
|
324
|
-
advSummary: {
|
|
996
|
+
advanced: { marginTop: 12 },
|
|
997
|
+
summary: {
|
|
325
998
|
cursor: "pointer",
|
|
326
999
|
fontWeight: 600
|
|
327
1000
|
}
|
|
@@ -646,29 +1319,9 @@ window.__ModuleLoader__.load({
|
|
|
646
1319
|
return null;
|
|
647
1320
|
}
|
|
648
1321
|
}
|
|
649
|
-
/**
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
* 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉。 */
|
|
653
|
-
function readPickAnswer(raw) {
|
|
654
|
-
if (typeof raw === "string") return raw.trim() === "" ? { kind: "cancelled" } : {
|
|
655
|
-
kind: "picked",
|
|
656
|
-
path: raw
|
|
657
|
-
};
|
|
658
|
-
const answer = typeof raw === "object" && raw !== null ? raw : {};
|
|
659
|
-
if (answer.ok === true) {
|
|
660
|
-
const value = answer.value;
|
|
661
|
-
return typeof value === "string" && value.trim() !== "" ? {
|
|
662
|
-
kind: "picked",
|
|
663
|
-
path: value
|
|
664
|
-
} : { kind: "cancelled" };
|
|
665
|
-
}
|
|
666
|
-
const detail = answer.error?.message?.trim() ?? "";
|
|
667
|
-
return {
|
|
668
|
-
kind: "unavailable",
|
|
669
|
-
message: "打不开系统文件夹对话框" + (detail === "" ? "" : "(" + detail + ")") + ":请直接在框里填绝对路径。"
|
|
670
|
-
};
|
|
671
|
-
}
|
|
1322
|
+
/** 平台回执/入口三态:**已经收进共用件**(票 #744),这里只同名转出。 */
|
|
1323
|
+
const readPickAnswer = readPickAnswer$1;
|
|
1324
|
+
const pickerModeOf = pickerModeOf$1;
|
|
672
1325
|
/** 唤起一次系统文件夹选择器并归一结果(**永不抛**)。 */
|
|
673
1326
|
async function pickDirectory(picker) {
|
|
674
1327
|
try {
|
|
@@ -680,6 +1333,29 @@ window.__ModuleLoader__.load({
|
|
|
680
1333
|
};
|
|
681
1334
|
}
|
|
682
1335
|
}
|
|
1336
|
+
/** 入口三态:有系统对话框(`pick`)/只有应用内浏览(`list` + `createDirectory`)/都没有。
|
|
1337
|
+
*
|
|
1338
|
+
* 判定取自共用件(一处定义)。本函数只读那个命名空间,不做 IO。 */
|
|
1339
|
+
function directoryEntryMode(picker) {
|
|
1340
|
+
return picker === null ? "none" : pickerModeOf(picker);
|
|
1341
|
+
}
|
|
1342
|
+
/** 开一行的应用内浏览器;返回 undefined=这条路供不了(调用方据此不画入口)。
|
|
1343
|
+
*
|
|
1344
|
+
* **状态由调用方持有**(`setBrowseRow`):本函数只造浏览器、开图,不碰 React 状态。 */
|
|
1345
|
+
function openRowBrowser(input) {
|
|
1346
|
+
if (input.picker === null || pickerModeOf(input.picker) !== "browse") return void 0;
|
|
1347
|
+
const row = createDirectoryRowBrowser({
|
|
1348
|
+
face: input.picker,
|
|
1349
|
+
initialPath: input.path,
|
|
1350
|
+
onPicked: (picked) => {
|
|
1351
|
+
input.onChange(input.key, picked);
|
|
1352
|
+
input.setBrowseRow(null);
|
|
1353
|
+
},
|
|
1354
|
+
onClosed: () => input.setBrowseRow(null)
|
|
1355
|
+
});
|
|
1356
|
+
input.setBrowseRow(row);
|
|
1357
|
+
return row.open();
|
|
1358
|
+
}
|
|
683
1359
|
/** 「选择文件夹」按钮的真装配函数(目录行渲染出来的按钮,onClick 就是它)。
|
|
684
1360
|
*
|
|
685
1361
|
* 回 Promise 是为了本地可判(用例直接 await);接进 React 时调用方 `void` 掉。 */
|
|
@@ -710,12 +1386,14 @@ window.__ModuleLoader__.load({
|
|
|
710
1386
|
spellCheck: false,
|
|
711
1387
|
onChange
|
|
712
1388
|
});
|
|
713
|
-
|
|
1389
|
+
/** 三态入口:供不了(`none`/缺席)就不画,不摆一个点了没反应的死按钮。 */
|
|
1390
|
+
const entry = props.browser ?? null;
|
|
1391
|
+
const browse = item.control === "directory" && entry !== null && entry.mode !== "none" ? react.createElement("button", {
|
|
714
1392
|
style: S.btnPick,
|
|
715
1393
|
type: "button",
|
|
716
1394
|
disabled: props.disabled,
|
|
717
|
-
onClick: () =>
|
|
718
|
-
}, "选择文件夹…") : null;
|
|
1395
|
+
onClick: () => entry.onOpen(item.key)
|
|
1396
|
+
}, entry.mode === "native" ? "选择文件夹…" : "浏览…") : null;
|
|
719
1397
|
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));
|
|
720
1398
|
}
|
|
721
1399
|
/** 技能设置页:承载卡路里自己的全部可配置项(只配置,不干活)。
|
|
@@ -790,24 +1468,46 @@ window.__ModuleLoader__.load({
|
|
|
790
1468
|
setNotice(null);
|
|
791
1469
|
setError(null);
|
|
792
1470
|
}, []);
|
|
793
|
-
/**
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1471
|
+
/** 目录行入口:三态(系统对话框/应用内浏览/都没有)。
|
|
1472
|
+
*
|
|
1473
|
+
* **软依赖**:拿不到命名空间就没有入口(留文本框),被拒一次即收起入口,不留死按钮。
|
|
1474
|
+
* 状态全部住在组件自己(hook 顺序与旧版同形):浏览器那一份只存「当前开着的那条」。 */
|
|
1475
|
+
const picker = pickerGone ? null : props.pickerSource();
|
|
1476
|
+
const [browseRow, setBrowseRow] = react.useState(null);
|
|
1477
|
+
/** 目录行的入口动作:三态各一条路;都供不了就不给入口(`undefined` ⇒ Row 不画按钮)。 */
|
|
1478
|
+
const onOpenRow = react.useCallback(async (key) => {
|
|
1479
|
+
if (pickerModeOf(picker) === "native") {
|
|
1480
|
+
setPicking(true);
|
|
1481
|
+
setError(null);
|
|
1482
|
+
const outcome = await pickDirectory(picker);
|
|
1483
|
+
setPicking(false);
|
|
1484
|
+
if (outcome.kind === "picked") onChange(key, outcome.path);
|
|
1485
|
+
else if (outcome.kind === "unavailable") {
|
|
1486
|
+
setError(outcome.message);
|
|
802
1487
|
setPickerGone(true);
|
|
803
1488
|
}
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const opened = openRowBrowser({
|
|
1492
|
+
picker,
|
|
1493
|
+
key,
|
|
1494
|
+
path: draft[key] ?? "",
|
|
1495
|
+
onChange,
|
|
1496
|
+
setBrowseRow
|
|
804
1497
|
});
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1498
|
+
if (opened === void 0) return;
|
|
1499
|
+
setError(null);
|
|
1500
|
+
await opened;
|
|
1501
|
+
}, [
|
|
1502
|
+
picker,
|
|
1503
|
+
onChange,
|
|
1504
|
+
draft
|
|
1505
|
+
]);
|
|
1506
|
+
/** 给 Row 的三态入口:`none` 时给 null(不画按钮,文本框照旧)。 */
|
|
1507
|
+
const rowEntry = pickerModeOf(picker) === "none" ? null : {
|
|
1508
|
+
mode: pickerModeOf(picker),
|
|
1509
|
+
onOpen: onOpenRow
|
|
1510
|
+
};
|
|
811
1511
|
const surface = state.kind === "ready" ? state.surface : null;
|
|
812
1512
|
const dirty = surface !== null && CONFIG_ITEMS.some((i) => (draft[i.key] ?? "") !== (toDraft(surface.values, surface)[i.key] ?? ""));
|
|
813
1513
|
/** 写完(保存/重置)之后重新读一份整面:写回执是 `{path, values}`、重置回执是 `{path, backupPath}`,
|
|
@@ -874,9 +1574,9 @@ window.__ModuleLoader__.load({
|
|
|
874
1574
|
value: draft[item.key] ?? "",
|
|
875
1575
|
disabled: busy,
|
|
876
1576
|
onChange,
|
|
877
|
-
|
|
1577
|
+
browser: rowEntry
|
|
878
1578
|
});
|
|
879
|
-
return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), react.createElement("details", { style: S.
|
|
1579
|
+
return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), react.createElement("details", { style: S.advanced }, react.createElement("summary", { style: S.summary }, ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.muted }, ADVANCED_GROUP_NOTE), advanced.map(renderRow)), react.createElement("div", { style: S.bar }, react.createElement("button", {
|
|
880
1580
|
style: dirty ? S.btnPrimary : S.btn,
|
|
881
1581
|
type: "button",
|
|
882
1582
|
disabled: busy || !dirty,
|
|
@@ -891,7 +1591,28 @@ window.__ModuleLoader__.load({
|
|
|
891
1591
|
type: "button",
|
|
892
1592
|
disabled: busy,
|
|
893
1593
|
onClick: () => void load()
|
|
894
|
-
}, "重新读取")), picking ? react.createElement("div", { style: S.muted }, "已唤起系统文件夹对话框:选中后自动填上,取消则不动。") : null,
|
|
1594
|
+
}, "重新读取")), picking ? react.createElement("div", { style: S.muted }, "已唤起系统文件夹对话框:选中后自动填上,取消则不动。") : null, browseRow !== null ? react.createElement(DirectoryBrowserFromRow, {
|
|
1595
|
+
open: true,
|
|
1596
|
+
row: browseRow,
|
|
1597
|
+
labels: {
|
|
1598
|
+
title: "选择文件夹",
|
|
1599
|
+
close: "关闭",
|
|
1600
|
+
up: "上一级",
|
|
1601
|
+
pathPlaceholder: "直接填绝对路径,回车即进入",
|
|
1602
|
+
go: "转到",
|
|
1603
|
+
showHidden: (n) => "显示隐藏目录(" + n + ")",
|
|
1604
|
+
empty: "这个目录里没有子目录。",
|
|
1605
|
+
loading: "正在读取…",
|
|
1606
|
+
newFolder: "新建文件夹",
|
|
1607
|
+
createConfirm: "创建",
|
|
1608
|
+
createCancel: "取消",
|
|
1609
|
+
select: "选",
|
|
1610
|
+
selected: "已选",
|
|
1611
|
+
open: "选定这个目录",
|
|
1612
|
+
cancel: "取消",
|
|
1613
|
+
willPick: "将选定:"
|
|
1614
|
+
}
|
|
1615
|
+
}) : null, notice !== null ? react.createElement("div", { style: S.okText }, notice) : null, error !== null ? react.createElement("div", { style: S.error }, error) : null, react.createElement(VersionLine, {
|
|
895
1616
|
pluginVersion: versions.plugin,
|
|
896
1617
|
skillVersion: versions.skill
|
|
897
1618
|
}));
|
|
@@ -907,7 +1628,7 @@ window.__ModuleLoader__.load({
|
|
|
907
1628
|
channel: RPC_CHANNEL
|
|
908
1629
|
}, () => react.createElement(CalorieConfig, {
|
|
909
1630
|
getCall,
|
|
910
|
-
getPicker
|
|
1631
|
+
pickerSource: getPicker
|
|
911
1632
|
})));
|
|
912
1633
|
const ensureWorkTab = () => {
|
|
913
1634
|
try {
|
|
@@ -949,6 +1670,7 @@ window.__ModuleLoader__.load({
|
|
|
949
1670
|
exports.VersionLine = VersionLine;
|
|
950
1671
|
exports.apply = apply;
|
|
951
1672
|
exports.createBrowseHandler = createBrowseHandler;
|
|
1673
|
+
exports.directoryEntryMode = directoryEntryMode;
|
|
952
1674
|
exports.fetchConfigSurface = fetchConfigSurface;
|
|
953
1675
|
exports.fetchVersions = fetchVersions;
|
|
954
1676
|
exports.formatVersionLine = formatVersionLine;
|
|
@@ -957,7 +1679,9 @@ window.__ModuleLoader__.load({
|
|
|
957
1679
|
exports.inject = inject;
|
|
958
1680
|
exports.isDirectoryPicker = isDirectoryPicker;
|
|
959
1681
|
exports.normalizeVersion = normalizeVersion;
|
|
1682
|
+
exports.openRowBrowser = openRowBrowser;
|
|
960
1683
|
exports.pickDirectory = pickDirectory;
|
|
1684
|
+
exports.pickerModeOf = pickerModeOf;
|
|
961
1685
|
exports.readPickAnswer = readPickAnswer;
|
|
962
1686
|
exports.resetConfigSurface = resetConfigSurface;
|
|
963
1687
|
exports.resolveDirectoryPicker = resolveDirectoryPicker;
|