dsh-life-pack 0.3.1 → 0.3.3
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 +752 -83
- package/dist/client.js.map +1 -1
- package/dist/directory-browser-api.d.ts +23 -0
- package/dist/directory-browser-api.d.ts.map +1 -0
- package/dist/directory-browser-api.js +12 -0
- package/dist/directory-browser-api.js.map +1 -0
- package/dist/directory-browser-contract.d.ts +111 -0
- package/dist/directory-browser-contract.d.ts.map +1 -0
- package/dist/directory-browser-contract.js +134 -0
- package/dist/directory-browser-contract.js.map +1 -0
- package/dist/directory-browser-state.d.ts +114 -0
- package/dist/directory-browser-state.d.ts.map +1 -0
- package/dist/directory-browser-state.js +203 -0
- package/dist/directory-browser-state.js.map +1 -0
- package/dist/directory-browser-ui.d.ts +59 -0
- package/dist/directory-browser-ui.d.ts.map +1 -0
- package/dist/directory-browser-ui.js +211 -0
- package/dist/directory-browser-ui.js.map +1 -0
- package/dist/health-fetch.d.ts +6 -2
- package/dist/health-fetch.d.ts.map +1 -1
- package/dist/health-fetch.js +15 -3
- package/dist/health-fetch.js.map +1 -1
- package/dist/health-panel.d.ts +4 -1
- package/dist/health-panel.d.ts.map +1 -1
- package/dist/health-panel.js +19 -8
- package/dist/health-panel.js.map +1 -1
- package/dist/nav.js +5 -5
- package/dist/nav.js.map +1 -1
- package/dist/update-client.d.ts +7 -1
- package/dist/update-client.d.ts.map +1 -1
- package/dist/update-client.js +7 -1
- package/dist/update-client.js.map +1 -1
- package/dist/update-view.d.ts +7 -9
- package/dist/update-view.d.ts.map +1 -1
- package/dist/update-view.js +8 -13
- package/dist/update-view.js.map +1 -1
- package/package.json +3 -1
package/dist/client.js
CHANGED
|
@@ -34,12 +34,12 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
/** P3 定案 6 tab(order 升序即 + 号菜单顺序;已打开条不受 order 控制)。 */
|
|
35
35
|
const MANAGER_TABS = [
|
|
36
36
|
{
|
|
37
|
-
skill: "
|
|
38
|
-
slotId: "ilife:
|
|
37
|
+
skill: "bill",
|
|
38
|
+
slotId: "ilife:cookie",
|
|
39
39
|
order: 70,
|
|
40
|
-
title: "
|
|
41
|
-
plugin: "dsh-
|
|
42
|
-
channel: "/ilife-
|
|
40
|
+
title: "饼干记账",
|
|
41
|
+
plugin: "dsh-bill-ilife",
|
|
42
|
+
channel: "/ilife-bill-ilife"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
skill: "calorie",
|
|
@@ -49,10 +49,18 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
plugin: "dsh-calorie",
|
|
50
50
|
channel: "/ilife-calorie"
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
skill: "memo",
|
|
54
|
+
slotId: "ilife:memo",
|
|
55
|
+
order: 80,
|
|
56
|
+
title: "备忘录",
|
|
57
|
+
plugin: "dsh-memo-ilife",
|
|
58
|
+
channel: "/ilife-memo"
|
|
59
|
+
},
|
|
52
60
|
{
|
|
53
61
|
skill: "schedule",
|
|
54
62
|
slotId: "ilife:schedule",
|
|
55
|
-
order:
|
|
63
|
+
order: 85,
|
|
56
64
|
title: "作息管家",
|
|
57
65
|
plugin: "dsh-schedule-ilife",
|
|
58
66
|
channel: "/ilife-schedule-ilife"
|
|
@@ -60,7 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
60
68
|
{
|
|
61
69
|
skill: "home",
|
|
62
70
|
slotId: "ilife:home",
|
|
63
|
-
order:
|
|
71
|
+
order: 90,
|
|
64
72
|
title: "居家管家",
|
|
65
73
|
plugin: "dsh-home-ilife",
|
|
66
74
|
channel: "/ilife-home-ilife"
|
|
@@ -68,18 +76,10 @@ window.__ModuleLoader__.load({
|
|
|
68
76
|
{
|
|
69
77
|
skill: "chef",
|
|
70
78
|
slotId: "ilife:chef",
|
|
71
|
-
order:
|
|
79
|
+
order: 95,
|
|
72
80
|
title: "私家大厨",
|
|
73
81
|
plugin: "dsh-chef",
|
|
74
82
|
channel: "/ilife-chef"
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
skill: "bill",
|
|
78
|
-
slotId: "ilife:cookie",
|
|
79
|
-
order: 95,
|
|
80
|
-
title: "饼干记账",
|
|
81
|
-
plugin: "dsh-bill-ilife",
|
|
82
|
-
channel: "/ilife-bill-ilife"
|
|
83
83
|
}
|
|
84
84
|
];
|
|
85
85
|
function recoFor(tab) {
|
|
@@ -310,7 +310,13 @@ window.__ModuleLoader__.load({
|
|
|
310
310
|
version
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
-
/** 装上更新:查新版 → 提交安装 → 有界轮询查状态到任务收尾;凭证过期重查一次再提交。
|
|
313
|
+
/** 装上更新:查新版 → 提交安装 → 有界轮询查状态到任务收尾;凭证过期重查一次再提交。
|
|
314
|
+
*
|
|
315
|
+
* **第一句那次 `checkTarget` 不是多余的**(票 #740 第四轮):凭证是「这一刻的安装态」的凭据,
|
|
316
|
+
* 更新包提交时拿它跟当时读到的安装态比(`service.js:309`/`:325`),不一样就拒。
|
|
317
|
+
* 而面板**从来只提交这一刻现签的凭证**——这就是「刚在本面板装过别家,这一家照样点得动
|
|
318
|
+
* 「装上更新」」的全部依据:旧快照只用来显示版本,不参与提交。(第四轮以前面板还把别家整行
|
|
319
|
+
* 换成「重新检查」——那是白挡一次本来会成功的点击,已删。) */
|
|
314
320
|
async function updateInstalled(call, target, pollMs) {
|
|
315
321
|
if (!target.phones) return failure("bad-request", { target: target.key });
|
|
316
322
|
const first = await checkTarget(call, target);
|
|
@@ -418,25 +424,14 @@ window.__ModuleLoader__.load({
|
|
|
418
424
|
if (verdict?.action) return verdict.action;
|
|
419
425
|
return phase === "failed" ? "recheck" : null;
|
|
420
426
|
}
|
|
421
|
-
/**
|
|
422
|
-
*
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
/** 把除 `keepKey` 之外的行全标成过期(票 #740):装成一家 ⇒ 七家共用的安装清单被改写,
|
|
431
|
-
* 其余各家的旧快照再拿去装必然被守卫拦下。纯函数:状态源与单测共用同一份判据,
|
|
432
|
-
* 不关心行里还有什么(泛型只要求有 `stale` 这一格)。 */
|
|
433
|
-
function markStaleOthers(rows, keepKey) {
|
|
434
|
-
const next = {};
|
|
435
|
-
for (const [key, row] of Object.entries(rows)) next[key] = key === keepKey ? row : {
|
|
436
|
-
...row,
|
|
437
|
-
stale: true
|
|
438
|
-
};
|
|
439
|
-
return next;
|
|
427
|
+
/** 这一行要不要摊出那条手工命令(票 #740 第三轮定的形状,第四轮补的这一格)。
|
|
428
|
+
*
|
|
429
|
+
* 三种情形摊:① 上一次装失败(`failed`);② 这句话自己指着「下面这条命令」(`manualHint`)。
|
|
430
|
+
* **待重启**与 `incompatible-node` 不摊:它们那时该做的是**重启**(或升级 Node),不是重装一次。
|
|
431
|
+
* (第四轮维护者真机截图逮到:待重启那张卡下面挂着一条「dsh plugin … add」——点它等于把刚装好的
|
|
432
|
+
* 那一版再装一遍,纯噪声,还会让人以为状态不对。) */
|
|
433
|
+
function showManualOf(verdict, phase) {
|
|
434
|
+
return phase === "failed" || verdict?.manualHint === true;
|
|
440
435
|
}
|
|
441
436
|
/** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 待重启 / 装不了(附人话原因与下一步)。 */
|
|
442
437
|
function verdictOf(target, snapshot) {
|
|
@@ -823,10 +818,6 @@ window.__ModuleLoader__.load({
|
|
|
823
818
|
[key]: next
|
|
824
819
|
}));
|
|
825
820
|
}, []);
|
|
826
|
-
/** 装成一家 ⇒ 其余各家的旧快照一律作废(票 #740):判据见 `markStaleOthers`。 */
|
|
827
|
-
const staleOthers = react.useCallback((keepKey) => {
|
|
828
|
-
setRows((previous) => markStaleOthers(previous, keepKey));
|
|
829
|
-
}, []);
|
|
830
821
|
return {
|
|
831
822
|
targets,
|
|
832
823
|
rows,
|
|
@@ -842,21 +833,18 @@ window.__ModuleLoader__.load({
|
|
|
842
833
|
for (const target of list) patch(target.key, {
|
|
843
834
|
phase: "checking",
|
|
844
835
|
outcome: rows[target.key]?.outcome ?? null,
|
|
845
|
-
failure: null
|
|
846
|
-
stale: false
|
|
836
|
+
failure: null
|
|
847
837
|
});
|
|
848
838
|
Promise.all(list.map(async (target) => {
|
|
849
839
|
const result = await checkTarget(getCall(), target);
|
|
850
840
|
patch(target.key, result.ok ? {
|
|
851
841
|
phase: "ready",
|
|
852
842
|
outcome: result.value,
|
|
853
|
-
failure: null
|
|
854
|
-
stale: false
|
|
843
|
+
failure: null
|
|
855
844
|
} : {
|
|
856
845
|
phase: "failed",
|
|
857
846
|
outcome: null,
|
|
858
|
-
failure: result
|
|
859
|
-
stale: false
|
|
847
|
+
failure: result
|
|
860
848
|
});
|
|
861
849
|
})).finally(() => setChecking(false));
|
|
862
850
|
}, [
|
|
@@ -871,21 +859,18 @@ window.__ModuleLoader__.load({
|
|
|
871
859
|
patch(target.key, {
|
|
872
860
|
phase: "checking",
|
|
873
861
|
outcome: current.outcome,
|
|
874
|
-
failure: null
|
|
875
|
-
stale: false
|
|
862
|
+
failure: null
|
|
876
863
|
});
|
|
877
864
|
(async () => {
|
|
878
865
|
const checked = await checkTarget(getCall(), target);
|
|
879
866
|
patch(target.key, checked.ok ? {
|
|
880
867
|
phase: "ready",
|
|
881
868
|
outcome: checked.value,
|
|
882
|
-
failure: null
|
|
883
|
-
stale: false
|
|
869
|
+
failure: null
|
|
884
870
|
} : {
|
|
885
871
|
phase: "failed",
|
|
886
872
|
outcome: null,
|
|
887
|
-
failure: checked
|
|
888
|
-
stale: false
|
|
873
|
+
failure: checked
|
|
889
874
|
});
|
|
890
875
|
})();
|
|
891
876
|
return;
|
|
@@ -893,8 +878,7 @@ window.__ModuleLoader__.load({
|
|
|
893
878
|
patch(target.key, {
|
|
894
879
|
...current,
|
|
895
880
|
phase: "installing",
|
|
896
|
-
failure: null
|
|
897
|
-
stale: false
|
|
881
|
+
failure: null
|
|
898
882
|
});
|
|
899
883
|
(async () => {
|
|
900
884
|
const absent = isAbsent(target);
|
|
@@ -905,36 +889,29 @@ window.__ModuleLoader__.load({
|
|
|
905
889
|
patch(target.key, {
|
|
906
890
|
phase: "failed",
|
|
907
891
|
outcome: null,
|
|
908
|
-
failure: checked
|
|
909
|
-
stale: false
|
|
892
|
+
failure: checked
|
|
910
893
|
});
|
|
911
894
|
return;
|
|
912
895
|
}
|
|
913
896
|
patch(target.key, {
|
|
914
897
|
phase: "installing",
|
|
915
898
|
outcome: checked.value,
|
|
916
|
-
failure: null
|
|
917
|
-
stale: false
|
|
899
|
+
failure: null
|
|
918
900
|
});
|
|
919
901
|
version = checked.value.snapshot.latestVersion;
|
|
920
902
|
}
|
|
921
903
|
const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
|
|
922
904
|
if (result.ok) {
|
|
923
|
-
const before = current.outcome?.snapshot.installedVersion ?? null;
|
|
924
905
|
const refreshed = await checkTarget(getCall(), target, target.phones?.status);
|
|
925
|
-
const after = refreshed.ok ? refreshed.value.snapshot.installedVersion ?? null : null;
|
|
926
906
|
patch(target.key, refreshed.ok ? {
|
|
927
907
|
phase: "ready",
|
|
928
908
|
outcome: refreshed.value,
|
|
929
|
-
failure: null
|
|
930
|
-
stale: false
|
|
909
|
+
failure: null
|
|
931
910
|
} : {
|
|
932
911
|
phase: "failed",
|
|
933
912
|
outcome: null,
|
|
934
|
-
failure: refreshed
|
|
935
|
-
stale: false
|
|
913
|
+
failure: refreshed
|
|
936
914
|
});
|
|
937
|
-
if (after !== null && after !== before) staleOthers(target.key);
|
|
938
915
|
await reload();
|
|
939
916
|
return;
|
|
940
917
|
}
|
|
@@ -944,8 +921,7 @@ window.__ModuleLoader__.load({
|
|
|
944
921
|
failure: {
|
|
945
922
|
...result,
|
|
946
923
|
manual: result.manual ?? current.outcome?.manual ?? null
|
|
947
|
-
}
|
|
948
|
-
stale: false
|
|
924
|
+
}
|
|
949
925
|
});
|
|
950
926
|
})();
|
|
951
927
|
}, [
|
|
@@ -953,8 +929,7 @@ window.__ModuleLoader__.load({
|
|
|
953
929
|
patch,
|
|
954
930
|
pollMs,
|
|
955
931
|
reload,
|
|
956
|
-
rows
|
|
957
|
-
staleOthers
|
|
932
|
+
rows
|
|
958
933
|
])
|
|
959
934
|
};
|
|
960
935
|
}
|
|
@@ -1007,10 +982,10 @@ window.__ModuleLoader__.load({
|
|
|
1007
982
|
const body = shown.map((target) => {
|
|
1008
983
|
const row = rows[target.key];
|
|
1009
984
|
const snapshot = row.outcome?.snapshot ?? null;
|
|
1010
|
-
const verdict =
|
|
985
|
+
const verdict = snapshot ? verdictOf(target, snapshot) : null;
|
|
1011
986
|
const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
|
|
1012
987
|
const busy = row.phase === "installing";
|
|
1013
|
-
const showManual = manual !== null && (row.phase
|
|
988
|
+
const showManual = manual !== null && showManualOf(verdict, row.phase);
|
|
1014
989
|
const buttonAction = cardActionOf(verdict, row.phase);
|
|
1015
990
|
const tone = rowToneOf(row, verdict);
|
|
1016
991
|
const paint = ROW_TONE[tone];
|
|
@@ -1250,8 +1225,12 @@ window.__ModuleLoader__.load({
|
|
|
1250
1225
|
};
|
|
1251
1226
|
}
|
|
1252
1227
|
}
|
|
1253
|
-
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。
|
|
1254
|
-
|
|
1228
|
+
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。
|
|
1229
|
+
*
|
|
1230
|
+
* `onRow`(票 #741):某一家一落定就回调一次——面板据此**按家增量**画,不再等齐全部。
|
|
1231
|
+
* 六家的电话是并发打出去的,但宿主里干活的只有一个线程(各单品宿主半 `spawnSync`),
|
|
1232
|
+
* 所以真实到达顺序就是它们被串行干完的顺序;先回来的那家先画出来,用户才看得见进度。 */
|
|
1233
|
+
async function loadHealthReports(call, tabs, onRow) {
|
|
1255
1234
|
if (typeof call !== "function") return {
|
|
1256
1235
|
rows: [],
|
|
1257
1236
|
error: "宿主连接缺席:connection.rpc.call 不可用"
|
|
@@ -1259,11 +1238,15 @@ window.__ModuleLoader__.load({
|
|
|
1259
1238
|
return {
|
|
1260
1239
|
rows: await Promise.all(tabs.map(async (tab) => {
|
|
1261
1240
|
const one = await fetchOne(call, tab);
|
|
1262
|
-
|
|
1241
|
+
const row = {
|
|
1263
1242
|
id: tab.id,
|
|
1264
1243
|
report: one.report,
|
|
1265
1244
|
error: one.error
|
|
1266
1245
|
};
|
|
1246
|
+
try {
|
|
1247
|
+
onRow?.(row);
|
|
1248
|
+
} catch {}
|
|
1249
|
+
return row;
|
|
1267
1250
|
})),
|
|
1268
1251
|
error: null
|
|
1269
1252
|
};
|
|
@@ -1282,7 +1265,10 @@ window.__ModuleLoader__.load({
|
|
|
1282
1265
|
report: null,
|
|
1283
1266
|
error: null
|
|
1284
1267
|
};
|
|
1285
|
-
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。
|
|
1268
|
+
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。
|
|
1269
|
+
*
|
|
1270
|
+
* **按家增量**(票 #741):某一家一落定就画那一家——不再等全部齐了才一次性 setRows。
|
|
1271
|
+
* 取数口仍然只有一处(`loadHealthReports`),它把「一家好了」的回调交给这里 patch 状态。 */
|
|
1286
1272
|
function useHealthPanel(getCall, tabs) {
|
|
1287
1273
|
const [rows, setRows] = react.useState({});
|
|
1288
1274
|
const [error, setError] = react.useState(null);
|
|
@@ -1302,18 +1288,26 @@ window.__ModuleLoader__.load({
|
|
|
1302
1288
|
phase: "running"
|
|
1303
1289
|
}])));
|
|
1304
1290
|
(async () => {
|
|
1305
|
-
const
|
|
1291
|
+
const patch = (row) => {
|
|
1292
|
+
const next = {
|
|
1293
|
+
phase: row.report ? "ready" : "failed",
|
|
1294
|
+
report: row.report,
|
|
1295
|
+
error: row.error
|
|
1296
|
+
};
|
|
1297
|
+
setRows((previous) => ({
|
|
1298
|
+
...previous,
|
|
1299
|
+
[row.id]: next
|
|
1300
|
+
}));
|
|
1301
|
+
try {
|
|
1302
|
+
const diag = globalThis.__T706_RUN__;
|
|
1303
|
+
if (Array.isArray(diag)) diag.push("row=" + row.id + " ok=" + String(row.report !== null) + " err=" + String(row.error));
|
|
1304
|
+
} catch {}
|
|
1305
|
+
};
|
|
1306
|
+
const loaded = await loadHealthReports(getCall(), targets, patch);
|
|
1306
1307
|
try {
|
|
1307
1308
|
const diag = globalThis.__T706_RUN__;
|
|
1308
1309
|
if (Array.isArray(diag)) diag.push("rows=" + String(loaded.rows.length) + " err=" + String(loaded.error) + " first=" + String(loaded.rows[0]?.error ?? "null"));
|
|
1309
1310
|
} catch {}
|
|
1310
|
-
const next = {};
|
|
1311
|
-
for (const row of loaded.rows) next[row.id] = {
|
|
1312
|
-
phase: row.report ? "ready" : "failed",
|
|
1313
|
-
report: row.report,
|
|
1314
|
-
error: row.error
|
|
1315
|
-
};
|
|
1316
|
-
setRows(next);
|
|
1317
1311
|
setError(loaded.error);
|
|
1318
1312
|
setRunning(false);
|
|
1319
1313
|
})();
|
|
@@ -1741,6 +1735,672 @@ window.__ModuleLoader__.load({
|
|
|
1741
1735
|
}, "· " + item.title))) : null) : null);
|
|
1742
1736
|
}
|
|
1743
1737
|
//#endregion
|
|
1738
|
+
//#region src/directory-browser-contract.ts
|
|
1739
|
+
/** 认一认某个值是不是可用的目录取数面(软依赖守卫用:认不出就当没有,绝不把页面带下来)。 */
|
|
1740
|
+
function isBrowseFace(raw) {
|
|
1741
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
1742
|
+
const face = raw;
|
|
1743
|
+
return typeof face.list === "function" && typeof face.createDirectory === "function";
|
|
1744
|
+
}
|
|
1745
|
+
/** 认一认某个值有没有 `pick`(对应宿主 composition 里的 `native` 能力)。 */
|
|
1746
|
+
function hasPickFn(raw) {
|
|
1747
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
1748
|
+
return typeof raw.pick === "function";
|
|
1749
|
+
}
|
|
1750
|
+
/** 三态判定:先 native(有 pick),再 browse(有两格原语),都没有=`none`。 */
|
|
1751
|
+
function pickerModeOf(raw) {
|
|
1752
|
+
if (hasPickFn(raw)) return "native";
|
|
1753
|
+
return isBrowseFace(raw) ? "browse" : "none";
|
|
1754
|
+
}
|
|
1755
|
+
/** 平台回执 → 三态(**永不抛**):成功回的是信封里的 `value` 不是路径;`ok:false` 是「供不了」不是「取消」。
|
|
1756
|
+
*
|
|
1757
|
+
* 出处:`@deepseek-ai/dsh-api-gateway/lib/client.js` 的 `invoke()`(失败不抛、成功也不回裸值),
|
|
1758
|
+
* 第一方消费方 `@deepseek-ai/dsh-client-ui-workspace/lib/client.js:99-103` 就照这个信封拆。
|
|
1759
|
+
* 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉(#743 的教训)。 */
|
|
1760
|
+
function readPickAnswer(raw) {
|
|
1761
|
+
if (typeof raw === "string") return raw.trim() === "" ? { kind: "cancelled" } : {
|
|
1762
|
+
kind: "picked",
|
|
1763
|
+
path: raw
|
|
1764
|
+
};
|
|
1765
|
+
const answer = typeof raw === "object" && raw !== null ? raw : {};
|
|
1766
|
+
if (answer.ok === true) {
|
|
1767
|
+
const value = answer.value;
|
|
1768
|
+
return typeof value === "string" && value.trim() !== "" ? {
|
|
1769
|
+
kind: "picked",
|
|
1770
|
+
path: value
|
|
1771
|
+
} : { kind: "cancelled" };
|
|
1772
|
+
}
|
|
1773
|
+
const detail = answer.error?.message?.trim() ?? "";
|
|
1774
|
+
return {
|
|
1775
|
+
kind: "unavailable",
|
|
1776
|
+
message: "打不开系统文件夹对话框" + (detail === "" ? "" : "(" + detail + ")") + ":请直接在框里填绝对路径。"
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
/** 一段路径里最后那个分隔符的位置(Windows 上 `\` 与 `/` 都算;POSIX 上 `\` 是合法文件名字符,不算)。 */
|
|
1780
|
+
function lastSeparator(path) {
|
|
1781
|
+
const back = path.lastIndexOf("\\");
|
|
1782
|
+
const slash = path.lastIndexOf("/");
|
|
1783
|
+
return back > slash ? back : slash;
|
|
1784
|
+
}
|
|
1785
|
+
/** 上一层:`'C:\\a\\b\\'` → `'C:\\a'`;已经是根则回自身;相对名(没有任何分隔符)也回自身。
|
|
1786
|
+
*
|
|
1787
|
+
* 尾分隔符先吃掉再切——否则「回上一级」会在原地打转。**盘根单独认**:`C:\` 掐掉尾分隔符会剩成
|
|
1788
|
+
* `C:`,而 `C:` 是「当前盘」不是「上一层」,回它就会多跑一趟、还可能跨到别的盘上去。
|
|
1789
|
+
* UNC(`\\server\share`)不特殊处理:切到头就是共享名本身,浏览停在共享根。 */
|
|
1790
|
+
function parentOf(path) {
|
|
1791
|
+
const trimmed = path.replace(/[\\/]+$/, "");
|
|
1792
|
+
if (trimmed === "") return path;
|
|
1793
|
+
const sep = path.includes("\\") ? "\\" : "/";
|
|
1794
|
+
if (/^[A-Za-z]:$/.test(trimmed)) return trimmed + sep;
|
|
1795
|
+
const cut = lastSeparator(trimmed);
|
|
1796
|
+
if (cut === -1) return trimmed;
|
|
1797
|
+
if (cut === 0) return path.slice(0, 1);
|
|
1798
|
+
if (cut === 2 && /^[A-Za-z]:/.test(trimmed)) return trimmed.slice(0, 2) + sep;
|
|
1799
|
+
return trimmed.slice(0, cut);
|
|
1800
|
+
}
|
|
1801
|
+
/** 把「当前路径 + 一段输入」拼成一个目标路径。 */
|
|
1802
|
+
function joinPath(directory, name) {
|
|
1803
|
+
const sep = directory.includes("\\") ? "\\" : "/";
|
|
1804
|
+
const base = directory.replace(/[\\/]+$/, "");
|
|
1805
|
+
if (base === "") return sep + name;
|
|
1806
|
+
return base + sep + name;
|
|
1807
|
+
}
|
|
1808
|
+
/** 一层列举里,哪些行可进:隐藏目录按开关过滤,其余照宿主给的顺序。 */
|
|
1809
|
+
function visibleEntries(listing, showHidden) {
|
|
1810
|
+
return showHidden ? listing.entries : listing.entries.filter((entry) => !entry.hidden);
|
|
1811
|
+
}
|
|
1812
|
+
/** 这一层有多少个被藏起来的目录(用来决定「显示隐藏目录」那个开关要不要出现在图上)。 */
|
|
1813
|
+
function hiddenCount(listing) {
|
|
1814
|
+
return listing.entries.filter((entry) => entry.hidden).length;
|
|
1815
|
+
}
|
|
1816
|
+
/** 拿一段文本按「最后一个分隔符」切成「目录部分 + 筛选词」。
|
|
1817
|
+
*
|
|
1818
|
+
* 没打过任何分隔符时目录部分=null(那段文字还不指任何目录,只当筛选词用)。
|
|
1819
|
+
* Windows 上 `/` 也当分隔符(宿主那边两种都收),POSIX 上不。 */
|
|
1820
|
+
function splitDraft(draft) {
|
|
1821
|
+
const cut = lastSeparator(draft);
|
|
1822
|
+
if (cut === -1) return {
|
|
1823
|
+
directory: null,
|
|
1824
|
+
filter: draft
|
|
1825
|
+
};
|
|
1826
|
+
return {
|
|
1827
|
+
directory: draft.slice(0, cut + 1),
|
|
1828
|
+
filter: draft.slice(cut + 1)
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
/** 按筛选词过一层目录(大小写不敏感;空筛选词=全留)。 */
|
|
1832
|
+
function filterEntries(entries, filter) {
|
|
1833
|
+
const needle = filter.trim().toLowerCase();
|
|
1834
|
+
if (needle === "") return entries;
|
|
1835
|
+
return entries.filter((entry) => entry.name.toLowerCase().includes(needle));
|
|
1836
|
+
}
|
|
1837
|
+
/** 给界面用的一句话位置说明:优先显示相对 home 的说法,省得整条绝对路径占满一行。 */
|
|
1838
|
+
function locationLabel(listing) {
|
|
1839
|
+
const home = listing.home.replace(/[\\/]+$/, "");
|
|
1840
|
+
if (home !== "" && listing.path !== home && listing.path.startsWith(home)) {
|
|
1841
|
+
const tail = listing.path.slice(home.length).replace(/^[\\/]+/, "");
|
|
1842
|
+
if (tail !== "") return "~" + (listing.path.includes("\\") ? "\\" : "/") + tail;
|
|
1843
|
+
}
|
|
1844
|
+
return listing.path;
|
|
1845
|
+
}
|
|
1846
|
+
/** 新建文件夹的名字合不合法(单个路径段:不许分隔符、不许空、不许 `.`/`..`)。
|
|
1847
|
+
*
|
|
1848
|
+
* 与宿主 browse 后端的校验同口径——这里先拦一道,是为了在图上给得出人话,不是替代宿主校验。 */
|
|
1849
|
+
function validateFolderName(name) {
|
|
1850
|
+
const trimmed = name.trim();
|
|
1851
|
+
if (trimmed === "") return {
|
|
1852
|
+
ok: false,
|
|
1853
|
+
reason: "文件夹名不能为空。"
|
|
1854
|
+
};
|
|
1855
|
+
if (/[\\/]/.test(trimmed)) return {
|
|
1856
|
+
ok: false,
|
|
1857
|
+
reason: "文件夹名里不能带路径分隔符。"
|
|
1858
|
+
};
|
|
1859
|
+
if (trimmed === "." || trimmed === "..") return {
|
|
1860
|
+
ok: false,
|
|
1861
|
+
reason: "「.」与「..」不是文件夹名。"
|
|
1862
|
+
};
|
|
1863
|
+
return { ok: true };
|
|
1864
|
+
}
|
|
1865
|
+
//#endregion
|
|
1866
|
+
//#region src/directory-browser-state.ts
|
|
1867
|
+
/** 建一个浏览器控制器。取数、回调、初值全部由调用方给。 */
|
|
1868
|
+
function createBrowseController(deps) {
|
|
1869
|
+
const label = deps.failureLabel ?? "浏览失败";
|
|
1870
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1871
|
+
let state = {
|
|
1872
|
+
phase: "idle",
|
|
1873
|
+
listing: null,
|
|
1874
|
+
failure: null,
|
|
1875
|
+
selected: null,
|
|
1876
|
+
showHidden: false,
|
|
1877
|
+
draft: deps.initialPath,
|
|
1878
|
+
filter: "",
|
|
1879
|
+
creating: null,
|
|
1880
|
+
notice: null
|
|
1881
|
+
};
|
|
1882
|
+
/** 每次列举带一个序号:晚回来的旧回执直接丢掉,不许覆盖新一层。 */
|
|
1883
|
+
let generation = 0;
|
|
1884
|
+
const emit = (next) => {
|
|
1885
|
+
state = {
|
|
1886
|
+
...state,
|
|
1887
|
+
...next
|
|
1888
|
+
};
|
|
1889
|
+
for (const listener of [...listeners]) listener(state);
|
|
1890
|
+
};
|
|
1891
|
+
const humanize = (cause) => {
|
|
1892
|
+
const raw = typeof cause === "object" && cause !== null ? cause : {};
|
|
1893
|
+
const message = typeof raw.message === "string" && raw.message.trim() !== "" ? raw.message.trim() : String(cause);
|
|
1894
|
+
return {
|
|
1895
|
+
code: typeof raw.code === "string" && raw.code !== "" ? raw.code : "browse-failed",
|
|
1896
|
+
message: `${label}:${message}`
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
const load = async (path) => {
|
|
1900
|
+
const mine = ++generation;
|
|
1901
|
+
emit({
|
|
1902
|
+
phase: "loading",
|
|
1903
|
+
failure: null,
|
|
1904
|
+
notice: null
|
|
1905
|
+
});
|
|
1906
|
+
try {
|
|
1907
|
+
const listing = path === null ? await deps.face.list() : await deps.face.list(path);
|
|
1908
|
+
if (mine !== generation) return;
|
|
1909
|
+
emit({
|
|
1910
|
+
phase: "ready",
|
|
1911
|
+
listing,
|
|
1912
|
+
failure: null,
|
|
1913
|
+
selected: null,
|
|
1914
|
+
draft: listing.path,
|
|
1915
|
+
filter: ""
|
|
1916
|
+
});
|
|
1917
|
+
} catch (cause) {
|
|
1918
|
+
if (mine !== generation) return;
|
|
1919
|
+
emit({
|
|
1920
|
+
phase: "failed",
|
|
1921
|
+
listing: null,
|
|
1922
|
+
failure: humanize(cause),
|
|
1923
|
+
selected: null
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
/** 草稿指到的那一层已经列举过时,按末段筛当前这一层的行;否则不过滤。
|
|
1928
|
+
*
|
|
1929
|
+
* 两边都先掐掉尾分隔符再比:草稿那边切开后天然带尾分隔符(`C:\a\` + `b`),
|
|
1930
|
+
* 而这一层的 `path` 由宿主给、不带尾分隔符。 */
|
|
1931
|
+
const applyDraft = (draft) => {
|
|
1932
|
+
const { directory, filter } = splitDraft(draft);
|
|
1933
|
+
const listing = state.listing;
|
|
1934
|
+
const sameLevel = listing !== null && directory !== null && normalize(directory) === normalize(listing.path);
|
|
1935
|
+
emit({
|
|
1936
|
+
draft,
|
|
1937
|
+
filter: sameLevel ? filter : ""
|
|
1938
|
+
});
|
|
1939
|
+
};
|
|
1940
|
+
const normalize = (path) => path.replace(/[\\/]+$/, "");
|
|
1941
|
+
return {
|
|
1942
|
+
getState: () => state,
|
|
1943
|
+
subscribe(listener) {
|
|
1944
|
+
listeners.add(listener);
|
|
1945
|
+
return () => listeners.delete(listener);
|
|
1946
|
+
},
|
|
1947
|
+
open: () => load(null),
|
|
1948
|
+
enter: (path) => load(path),
|
|
1949
|
+
async up() {
|
|
1950
|
+
const listing = state.listing;
|
|
1951
|
+
if (listing === null) return;
|
|
1952
|
+
const parent = parentOf(listing.path);
|
|
1953
|
+
if (parent === listing.path) return;
|
|
1954
|
+
await load(parent);
|
|
1955
|
+
},
|
|
1956
|
+
pick() {
|
|
1957
|
+
const target = state.selected ?? state.listing?.path ?? null;
|
|
1958
|
+
if (target === null) return;
|
|
1959
|
+
deps.onPicked(target);
|
|
1960
|
+
},
|
|
1961
|
+
cancel() {
|
|
1962
|
+
deps.onClose();
|
|
1963
|
+
},
|
|
1964
|
+
close() {
|
|
1965
|
+
deps.onClose();
|
|
1966
|
+
},
|
|
1967
|
+
select(path) {
|
|
1968
|
+
emit({
|
|
1969
|
+
selected: state.selected === path ? null : path,
|
|
1970
|
+
notice: null
|
|
1971
|
+
});
|
|
1972
|
+
},
|
|
1973
|
+
toggleHidden() {
|
|
1974
|
+
emit({ showHidden: !state.showHidden });
|
|
1975
|
+
},
|
|
1976
|
+
setDraft(draft) {
|
|
1977
|
+
applyDraft(draft);
|
|
1978
|
+
},
|
|
1979
|
+
async commitDraft() {
|
|
1980
|
+
const trimmed = state.draft.trim();
|
|
1981
|
+
if (trimmed === "") return;
|
|
1982
|
+
await load(trimmed);
|
|
1983
|
+
},
|
|
1984
|
+
setCreating(name) {
|
|
1985
|
+
emit({
|
|
1986
|
+
creating: name,
|
|
1987
|
+
notice: null
|
|
1988
|
+
});
|
|
1989
|
+
},
|
|
1990
|
+
async createFolder(name) {
|
|
1991
|
+
const checked = validateFolderName(name);
|
|
1992
|
+
if (!checked.ok) {
|
|
1993
|
+
emit({ notice: checked.reason });
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const base = state.selected ?? state.listing?.path ?? null;
|
|
1997
|
+
if (base === null) {
|
|
1998
|
+
emit({ notice: "还没有打开任何目录,无法新建文件夹。" });
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
try {
|
|
2002
|
+
const created = await deps.face.createDirectory(base, name.trim());
|
|
2003
|
+
emit({
|
|
2004
|
+
creating: null,
|
|
2005
|
+
notice: null
|
|
2006
|
+
});
|
|
2007
|
+
await load(created);
|
|
2008
|
+
emit({ selected: created });
|
|
2009
|
+
} catch (cause) {
|
|
2010
|
+
const failure = humanize(cause);
|
|
2011
|
+
emit({
|
|
2012
|
+
notice: failure.message,
|
|
2013
|
+
creating: null
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
/** 图上这一层要画的行(按隐藏开关与草稿末段过滤之后的)。 */
|
|
2020
|
+
function rowsOf(state) {
|
|
2021
|
+
if (state.listing === null) return [];
|
|
2022
|
+
return filterEntries(visibleEntries(state.listing, state.showHidden), state.filter);
|
|
2023
|
+
}
|
|
2024
|
+
/** 图上这一层能不能回上一级(根目录不能)。 */
|
|
2025
|
+
function canGoUp(state) {
|
|
2026
|
+
if (state.listing === null) return false;
|
|
2027
|
+
return parentOf(state.listing.path) !== state.listing.path;
|
|
2028
|
+
}
|
|
2029
|
+
/** 「打开」这颗按钮的目标:选中行优先,没选中就是当前层。 */
|
|
2030
|
+
function targetOf(state) {
|
|
2031
|
+
return state.selected ?? state.listing?.path ?? null;
|
|
2032
|
+
}
|
|
2033
|
+
/** 一段路径拼成可读的一句话(给用例与调试用;图上不直接显示)。 */
|
|
2034
|
+
function describe(state) {
|
|
2035
|
+
if (state.phase === "failed") return `${state.failure?.code ?? "browse-failed"} @ ${state.draft}`;
|
|
2036
|
+
if (state.listing === null) return `${state.phase}`;
|
|
2037
|
+
return `${state.listing.path}(${rowsOf(state).length} 行)`;
|
|
2038
|
+
}
|
|
2039
|
+
/** 一行子目录的完整路径(图上点行时用)。 */
|
|
2040
|
+
function entryPath(directory, entry) {
|
|
2041
|
+
return entry.path === "" ? joinPath(directory, entry.name) : entry.path;
|
|
2042
|
+
}
|
|
2043
|
+
/** 建一份「目录行接线」:给一个取数面与初值,回一组动作。
|
|
2044
|
+
*
|
|
2045
|
+
* 只做接线:把控制器算出来的状态与回调打包好,不取数、不画图、不认宿主。 */
|
|
2046
|
+
function createDirectoryRowBrowser(deps) {
|
|
2047
|
+
const controller = createBrowseController({
|
|
2048
|
+
face: deps.face,
|
|
2049
|
+
initialPath: deps.initialPath,
|
|
2050
|
+
onPicked: deps.onPicked,
|
|
2051
|
+
onClose: () => deps.onClosed?.(),
|
|
2052
|
+
...deps.failureLabel === void 0 ? {} : { failureLabel: deps.failureLabel }
|
|
2053
|
+
});
|
|
2054
|
+
return {
|
|
2055
|
+
open: () => controller.open(),
|
|
2056
|
+
state: () => controller.getState(),
|
|
2057
|
+
summary: () => describe(controller.getState()),
|
|
2058
|
+
actions: {
|
|
2059
|
+
onPick: () => controller.pick(),
|
|
2060
|
+
onClose: () => controller.cancel(),
|
|
2061
|
+
onEnter: (path) => void controller.enter(path),
|
|
2062
|
+
onUp: () => void controller.up(),
|
|
2063
|
+
onSelect: (path) => controller.select(path),
|
|
2064
|
+
onToggleHidden: () => controller.toggleHidden(),
|
|
2065
|
+
onDraft: (draft) => controller.setDraft(draft),
|
|
2066
|
+
onCommitDraft: () => void controller.commitDraft(),
|
|
2067
|
+
onCreate: (name) => void controller.createFolder(name),
|
|
2068
|
+
onCreatingChange: (name) => controller.setCreating(name)
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
//#endregion
|
|
2073
|
+
//#region src/directory-browser-ui.ts
|
|
2074
|
+
/** 文件浏览器的视图半:把 `BrowseState` 画成一张对话框。**不认识宿主**,也不自己做 IO。
|
|
2075
|
+
*
|
|
2076
|
+
* 手写 `React.createElement`(本包 client 束禁 JSX,见 `tsdown.config.ts` 的既成口径)。
|
|
2077
|
+
* 只读状态、只回调;进哪一层、选到什么由操作半(`directory-browser-state.ts`)算。
|
|
2078
|
+
*/
|
|
2079
|
+
const EMPTY_LISTING = {
|
|
2080
|
+
path: "",
|
|
2081
|
+
home: "",
|
|
2082
|
+
crumbs: [],
|
|
2083
|
+
entries: [],
|
|
2084
|
+
truncated: false
|
|
2085
|
+
};
|
|
2086
|
+
const S$1 = {
|
|
2087
|
+
scrim: {
|
|
2088
|
+
position: "fixed",
|
|
2089
|
+
inset: 0,
|
|
2090
|
+
background: "rgba(0,0,0,0.35)",
|
|
2091
|
+
display: "flex",
|
|
2092
|
+
alignItems: "center",
|
|
2093
|
+
justifyContent: "center",
|
|
2094
|
+
zIndex: 9999
|
|
2095
|
+
},
|
|
2096
|
+
dialog: {
|
|
2097
|
+
width: 680,
|
|
2098
|
+
maxWidth: "92vw",
|
|
2099
|
+
height: 500,
|
|
2100
|
+
maxHeight: "86vh",
|
|
2101
|
+
display: "flex",
|
|
2102
|
+
flexDirection: "column",
|
|
2103
|
+
background: "var(--dsw-alias-bg-layer-1, #1f1f23)",
|
|
2104
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2105
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
2106
|
+
borderRadius: 10,
|
|
2107
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.35)",
|
|
2108
|
+
overflow: "hidden"
|
|
2109
|
+
},
|
|
2110
|
+
head: {
|
|
2111
|
+
display: "flex",
|
|
2112
|
+
alignItems: "center",
|
|
2113
|
+
gap: 8,
|
|
2114
|
+
padding: "10px 12px",
|
|
2115
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
2116
|
+
},
|
|
2117
|
+
title: {
|
|
2118
|
+
fontSize: 14,
|
|
2119
|
+
fontWeight: 700
|
|
2120
|
+
},
|
|
2121
|
+
headTail: {
|
|
2122
|
+
marginLeft: "auto",
|
|
2123
|
+
fontSize: 12,
|
|
2124
|
+
opacity: .72,
|
|
2125
|
+
overflow: "hidden",
|
|
2126
|
+
textOverflow: "ellipsis",
|
|
2127
|
+
whiteSpace: "nowrap"
|
|
2128
|
+
},
|
|
2129
|
+
crumbs: {
|
|
2130
|
+
display: "flex",
|
|
2131
|
+
flexWrap: "wrap",
|
|
2132
|
+
alignItems: "center",
|
|
2133
|
+
gap: 2,
|
|
2134
|
+
padding: "8px 12px 0",
|
|
2135
|
+
fontSize: 12
|
|
2136
|
+
},
|
|
2137
|
+
chromeButton: {
|
|
2138
|
+
background: "transparent",
|
|
2139
|
+
border: "none",
|
|
2140
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2141
|
+
cursor: "pointer",
|
|
2142
|
+
padding: "2px 4px",
|
|
2143
|
+
fontSize: 12,
|
|
2144
|
+
opacity: .85
|
|
2145
|
+
},
|
|
2146
|
+
crumbSep: {
|
|
2147
|
+
opacity: .45,
|
|
2148
|
+
fontSize: 12
|
|
2149
|
+
},
|
|
2150
|
+
pathRow: {
|
|
2151
|
+
display: "flex",
|
|
2152
|
+
gap: 6,
|
|
2153
|
+
padding: "8px 12px"
|
|
2154
|
+
},
|
|
2155
|
+
input: {
|
|
2156
|
+
flex: 1,
|
|
2157
|
+
minWidth: 0,
|
|
2158
|
+
fontSize: 12,
|
|
2159
|
+
padding: "5px 8px",
|
|
2160
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.12))",
|
|
2161
|
+
color: "inherit",
|
|
2162
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
2163
|
+
borderRadius: 6
|
|
2164
|
+
},
|
|
2165
|
+
button: {
|
|
2166
|
+
fontSize: 12,
|
|
2167
|
+
padding: "5px 10px",
|
|
2168
|
+
cursor: "pointer",
|
|
2169
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.12))",
|
|
2170
|
+
color: "inherit",
|
|
2171
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
|
|
2172
|
+
borderRadius: 6
|
|
2173
|
+
},
|
|
2174
|
+
buttonOff: {
|
|
2175
|
+
opacity: .45,
|
|
2176
|
+
cursor: "default"
|
|
2177
|
+
},
|
|
2178
|
+
list: {
|
|
2179
|
+
flex: 1,
|
|
2180
|
+
minHeight: 0,
|
|
2181
|
+
overflowY: "auto",
|
|
2182
|
+
margin: "0 12px",
|
|
2183
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
|
|
2184
|
+
borderRadius: 6
|
|
2185
|
+
},
|
|
2186
|
+
row: {
|
|
2187
|
+
display: "flex",
|
|
2188
|
+
alignItems: "center",
|
|
2189
|
+
gap: 8,
|
|
2190
|
+
width: "100%",
|
|
2191
|
+
padding: "6px 10px",
|
|
2192
|
+
textAlign: "left",
|
|
2193
|
+
background: "transparent",
|
|
2194
|
+
color: "inherit",
|
|
2195
|
+
border: "none",
|
|
2196
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
|
|
2197
|
+
cursor: "pointer",
|
|
2198
|
+
fontSize: 13
|
|
2199
|
+
},
|
|
2200
|
+
rowSelected: { background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.18))" },
|
|
2201
|
+
empty: {
|
|
2202
|
+
padding: "14px 12px",
|
|
2203
|
+
fontSize: 12,
|
|
2204
|
+
opacity: .7
|
|
2205
|
+
},
|
|
2206
|
+
footNote: {
|
|
2207
|
+
padding: "6px 12px 0",
|
|
2208
|
+
fontSize: 12,
|
|
2209
|
+
minHeight: 20
|
|
2210
|
+
},
|
|
2211
|
+
foot: {
|
|
2212
|
+
display: "flex",
|
|
2213
|
+
alignItems: "center",
|
|
2214
|
+
gap: 8,
|
|
2215
|
+
padding: "10px 12px",
|
|
2216
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
2217
|
+
},
|
|
2218
|
+
target: {
|
|
2219
|
+
marginRight: "auto",
|
|
2220
|
+
fontSize: 12,
|
|
2221
|
+
opacity: .8,
|
|
2222
|
+
overflow: "hidden",
|
|
2223
|
+
textOverflow: "ellipsis",
|
|
2224
|
+
whiteSpace: "nowrap"
|
|
2225
|
+
},
|
|
2226
|
+
error: { color: "var(--dsw-alias-label-error, #e66)" }
|
|
2227
|
+
};
|
|
2228
|
+
function crumbRow(state, labels, onEnter) {
|
|
2229
|
+
const listing = state.listing;
|
|
2230
|
+
if (listing === null || listing.crumbs.length === 0) return null;
|
|
2231
|
+
const parts = [];
|
|
2232
|
+
listing.crumbs.forEach((crumb, index) => {
|
|
2233
|
+
if (index > 0) parts.push(react.createElement("span", {
|
|
2234
|
+
key: "sep-" + crumb.path,
|
|
2235
|
+
style: S$1.crumbSep
|
|
2236
|
+
}, "›"));
|
|
2237
|
+
const isCurrent = crumb.path === listing.path;
|
|
2238
|
+
parts.push(react.createElement("button", {
|
|
2239
|
+
key: crumb.path,
|
|
2240
|
+
type: "button",
|
|
2241
|
+
style: {
|
|
2242
|
+
...S$1.chromeButton,
|
|
2243
|
+
fontWeight: isCurrent ? 700 : 400
|
|
2244
|
+
},
|
|
2245
|
+
title: crumb.path,
|
|
2246
|
+
onClick: () => onEnter(crumb.path)
|
|
2247
|
+
}, index === 0 ? labels.up : crumb.name));
|
|
2248
|
+
});
|
|
2249
|
+
return react.createElement("div", { style: S$1.crumbs }, parts);
|
|
2250
|
+
}
|
|
2251
|
+
function entryRow(entry, selected, labels, onToggleSelect, onEnter) {
|
|
2252
|
+
return react.createElement("div", {
|
|
2253
|
+
key: entry.path,
|
|
2254
|
+
style: {
|
|
2255
|
+
...S$1.row,
|
|
2256
|
+
padding: 0,
|
|
2257
|
+
...selected ? S$1.rowSelected : {}
|
|
2258
|
+
}
|
|
2259
|
+
}, react.createElement("button", {
|
|
2260
|
+
type: "button",
|
|
2261
|
+
style: {
|
|
2262
|
+
...S$1.row,
|
|
2263
|
+
flex: 1,
|
|
2264
|
+
border: "none"
|
|
2265
|
+
},
|
|
2266
|
+
onClick: () => onEnter(entry.path)
|
|
2267
|
+
}, "📁 " + entry.name), react.createElement("button", {
|
|
2268
|
+
type: "button",
|
|
2269
|
+
style: {
|
|
2270
|
+
...S$1.button,
|
|
2271
|
+
marginRight: 8,
|
|
2272
|
+
padding: "2px 8px"
|
|
2273
|
+
},
|
|
2274
|
+
"aria-pressed": selected,
|
|
2275
|
+
title: selected ? labels.selected : labels.select,
|
|
2276
|
+
onClick: () => onToggleSelect(entry.path)
|
|
2277
|
+
}, selected ? "✓" : labels.select));
|
|
2278
|
+
}
|
|
2279
|
+
function createRow(labels, onName, onCancel, onCreate, creatingName) {
|
|
2280
|
+
if (creatingName === null) return null;
|
|
2281
|
+
return react.createElement("div", { style: {
|
|
2282
|
+
...S$1.pathRow,
|
|
2283
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
|
|
2284
|
+
} }, react.createElement("input", {
|
|
2285
|
+
style: S$1.input,
|
|
2286
|
+
value: creatingName,
|
|
2287
|
+
placeholder: labels.newFolder,
|
|
2288
|
+
"aria-label": labels.newFolder,
|
|
2289
|
+
onChange: (event) => onName(event.currentTarget.value),
|
|
2290
|
+
onKeyDown: (event) => {
|
|
2291
|
+
if (event.key === "Enter") onCreate(creatingName);
|
|
2292
|
+
if (event.key === "Escape") onCancel();
|
|
2293
|
+
}
|
|
2294
|
+
}), react.createElement("button", {
|
|
2295
|
+
type: "button",
|
|
2296
|
+
style: S$1.button,
|
|
2297
|
+
onClick: () => onCreate(creatingName)
|
|
2298
|
+
}, labels.createConfirm), react.createElement("button", {
|
|
2299
|
+
type: "button",
|
|
2300
|
+
style: S$1.button,
|
|
2301
|
+
onClick: onCancel
|
|
2302
|
+
}, labels.createCancel));
|
|
2303
|
+
}
|
|
2304
|
+
/** 画一张文件浏览器对话框。`open=false` 时返回 null(本件不挂 portal,由调用方决定挂在哪)。 */
|
|
2305
|
+
function DirectoryBrowser(props) {
|
|
2306
|
+
const { open, state, labels } = props;
|
|
2307
|
+
if (!open) return null;
|
|
2308
|
+
const rows = rowsOf(state);
|
|
2309
|
+
const target = targetOf(state);
|
|
2310
|
+
const listing = state.listing ?? EMPTY_LISTING;
|
|
2311
|
+
const hidden = hiddenCount(listing);
|
|
2312
|
+
const body = [];
|
|
2313
|
+
body.push(crumbRow(state, labels, props.onEnter));
|
|
2314
|
+
body.push(react.createElement("div", { style: S$1.pathRow }, react.createElement("input", {
|
|
2315
|
+
style: S$1.input,
|
|
2316
|
+
value: state.draft,
|
|
2317
|
+
placeholder: labels.pathPlaceholder,
|
|
2318
|
+
"aria-label": labels.pathPlaceholder,
|
|
2319
|
+
onChange: (event) => props.onDraft(event.currentTarget.value),
|
|
2320
|
+
onKeyDown: (event) => {
|
|
2321
|
+
if (event.key === "Enter") props.onCommitDraft();
|
|
2322
|
+
}
|
|
2323
|
+
}), react.createElement("button", {
|
|
2324
|
+
type: "button",
|
|
2325
|
+
style: S$1.button,
|
|
2326
|
+
onClick: props.onUp
|
|
2327
|
+
}, labels.up), react.createElement("button", {
|
|
2328
|
+
type: "button",
|
|
2329
|
+
style: S$1.button,
|
|
2330
|
+
onClick: props.onCommitDraft
|
|
2331
|
+
}, labels.go)));
|
|
2332
|
+
body.push(react.createElement("div", {
|
|
2333
|
+
style: S$1.list,
|
|
2334
|
+
role: "listbox",
|
|
2335
|
+
"aria-label": labels.title
|
|
2336
|
+
}, 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))));
|
|
2337
|
+
body.push(createRow(labels, props.onDraft, () => props.onCreatingChange(null), props.onCreate, state.creating));
|
|
2338
|
+
body.push(react.createElement("div", { style: S$1.footNote }, hidden > 0 ? react.createElement("label", { style: {
|
|
2339
|
+
fontSize: 12,
|
|
2340
|
+
cursor: "pointer",
|
|
2341
|
+
marginRight: 10
|
|
2342
|
+
} }, react.createElement("input", {
|
|
2343
|
+
type: "checkbox",
|
|
2344
|
+
checked: state.showHidden,
|
|
2345
|
+
onChange: props.onToggleHidden
|
|
2346
|
+
}), " " + labels.showHidden(hidden)) : null, state.creating === null ? react.createElement("button", {
|
|
2347
|
+
type: "button",
|
|
2348
|
+
style: {
|
|
2349
|
+
...S$1.chromeButton,
|
|
2350
|
+
textDecoration: "underline"
|
|
2351
|
+
},
|
|
2352
|
+
onClick: () => props.onCreatingChange("")
|
|
2353
|
+
}, 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));
|
|
2354
|
+
body.push(react.createElement("div", { style: S$1.foot }, react.createElement("span", {
|
|
2355
|
+
style: S$1.target,
|
|
2356
|
+
title: target ?? ""
|
|
2357
|
+
}, target === null ? "" : labels.willPick + target), react.createElement("button", {
|
|
2358
|
+
type: "button",
|
|
2359
|
+
style: {
|
|
2360
|
+
...S$1.button,
|
|
2361
|
+
...target === null ? S$1.buttonOff : {}
|
|
2362
|
+
},
|
|
2363
|
+
onClick: props.onPick
|
|
2364
|
+
}, labels.open), react.createElement("button", {
|
|
2365
|
+
type: "button",
|
|
2366
|
+
style: S$1.button,
|
|
2367
|
+
onClick: props.onClose
|
|
2368
|
+
}, labels.cancel)));
|
|
2369
|
+
return react.createElement("div", {
|
|
2370
|
+
style: S$1.scrim,
|
|
2371
|
+
role: "presentation",
|
|
2372
|
+
onMouseDown: (event) => {
|
|
2373
|
+
if (event.target === event.currentTarget) props.onClose();
|
|
2374
|
+
}
|
|
2375
|
+
}, react.createElement("div", {
|
|
2376
|
+
style: S$1.dialog,
|
|
2377
|
+
role: "dialog",
|
|
2378
|
+
"aria-modal": true,
|
|
2379
|
+
"aria-label": labels.title
|
|
2380
|
+
}, react.createElement("div", { style: S$1.head }, react.createElement("span", { style: S$1.title }, labels.title), react.createElement("span", {
|
|
2381
|
+
style: S$1.headTail,
|
|
2382
|
+
title: listing.path
|
|
2383
|
+
}, state.listing === null ? "" : locationLabel(listing)), react.createElement("button", {
|
|
2384
|
+
type: "button",
|
|
2385
|
+
style: S$1.button,
|
|
2386
|
+
"aria-label": labels.close,
|
|
2387
|
+
title: labels.close,
|
|
2388
|
+
onClick: props.onClose
|
|
2389
|
+
}, "✕")), body));
|
|
2390
|
+
}
|
|
2391
|
+
/** 目录行接线的 React 半边:把操作半那组动作直接喂给组件。
|
|
2392
|
+
*
|
|
2393
|
+
* 操作半(`directory-browser-state.ts` 的 `createDirectoryRowBrowser`)不碰 React,
|
|
2394
|
+
* 这里只把它的 `actions` 摊进 props —— 于是「进哪一层」那套在 Node 里可测,这里只做接线。 */
|
|
2395
|
+
function DirectoryBrowserFromRow(props) {
|
|
2396
|
+
return DirectoryBrowser({
|
|
2397
|
+
open: props.open,
|
|
2398
|
+
state: props.row.state(),
|
|
2399
|
+
labels: props.labels,
|
|
2400
|
+
...props.row.actions
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
//#endregion
|
|
1744
2404
|
//#region src/client.ts
|
|
1745
2405
|
/** dsh-life-pack client 适配器(六边形:browser 侧;爱生活单卡方案)。
|
|
1746
2406
|
*
|
|
@@ -2126,8 +2786,17 @@ window.__ModuleLoader__.load({
|
|
|
2126
2786
|
}
|
|
2127
2787
|
//#endregion
|
|
2128
2788
|
exports.CONFIG_TAB_SLOT = CONFIG_TAB_SLOT;
|
|
2789
|
+
exports.DirectoryBrowserFromRow = DirectoryBrowserFromRow;
|
|
2129
2790
|
exports.apply = apply;
|
|
2791
|
+
exports.canGoUp = canGoUp;
|
|
2792
|
+
exports.createBrowseController = createBrowseController;
|
|
2793
|
+
exports.createDirectoryRowBrowser = createDirectoryRowBrowser;
|
|
2794
|
+
exports.entryPath = entryPath;
|
|
2130
2795
|
exports.inject = inject;
|
|
2796
|
+
exports.pickerModeOf = pickerModeOf;
|
|
2797
|
+
exports.readPickAnswer = readPickAnswer;
|
|
2798
|
+
exports.rowsOf = rowsOf;
|
|
2799
|
+
exports.targetOf = targetOf;
|
|
2131
2800
|
return module.exports;
|
|
2132
2801
|
}
|
|
2133
2802
|
});
|