dsh-life-pack 0.3.2 → 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 +722 -28
- 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/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) {
|
|
@@ -1225,8 +1225,12 @@ window.__ModuleLoader__.load({
|
|
|
1225
1225
|
};
|
|
1226
1226
|
}
|
|
1227
1227
|
}
|
|
1228
|
-
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。
|
|
1229
|
-
|
|
1228
|
+
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。
|
|
1229
|
+
*
|
|
1230
|
+
* `onRow`(票 #741):某一家一落定就回调一次——面板据此**按家增量**画,不再等齐全部。
|
|
1231
|
+
* 六家的电话是并发打出去的,但宿主里干活的只有一个线程(各单品宿主半 `spawnSync`),
|
|
1232
|
+
* 所以真实到达顺序就是它们被串行干完的顺序;先回来的那家先画出来,用户才看得见进度。 */
|
|
1233
|
+
async function loadHealthReports(call, tabs, onRow) {
|
|
1230
1234
|
if (typeof call !== "function") return {
|
|
1231
1235
|
rows: [],
|
|
1232
1236
|
error: "宿主连接缺席:connection.rpc.call 不可用"
|
|
@@ -1234,11 +1238,15 @@ window.__ModuleLoader__.load({
|
|
|
1234
1238
|
return {
|
|
1235
1239
|
rows: await Promise.all(tabs.map(async (tab) => {
|
|
1236
1240
|
const one = await fetchOne(call, tab);
|
|
1237
|
-
|
|
1241
|
+
const row = {
|
|
1238
1242
|
id: tab.id,
|
|
1239
1243
|
report: one.report,
|
|
1240
1244
|
error: one.error
|
|
1241
1245
|
};
|
|
1246
|
+
try {
|
|
1247
|
+
onRow?.(row);
|
|
1248
|
+
} catch {}
|
|
1249
|
+
return row;
|
|
1242
1250
|
})),
|
|
1243
1251
|
error: null
|
|
1244
1252
|
};
|
|
@@ -1257,7 +1265,10 @@ window.__ModuleLoader__.load({
|
|
|
1257
1265
|
report: null,
|
|
1258
1266
|
error: null
|
|
1259
1267
|
};
|
|
1260
|
-
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。
|
|
1268
|
+
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。
|
|
1269
|
+
*
|
|
1270
|
+
* **按家增量**(票 #741):某一家一落定就画那一家——不再等全部齐了才一次性 setRows。
|
|
1271
|
+
* 取数口仍然只有一处(`loadHealthReports`),它把「一家好了」的回调交给这里 patch 状态。 */
|
|
1261
1272
|
function useHealthPanel(getCall, tabs) {
|
|
1262
1273
|
const [rows, setRows] = react.useState({});
|
|
1263
1274
|
const [error, setError] = react.useState(null);
|
|
@@ -1277,18 +1288,26 @@ window.__ModuleLoader__.load({
|
|
|
1277
1288
|
phase: "running"
|
|
1278
1289
|
}])));
|
|
1279
1290
|
(async () => {
|
|
1280
|
-
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);
|
|
1281
1307
|
try {
|
|
1282
1308
|
const diag = globalThis.__T706_RUN__;
|
|
1283
1309
|
if (Array.isArray(diag)) diag.push("rows=" + String(loaded.rows.length) + " err=" + String(loaded.error) + " first=" + String(loaded.rows[0]?.error ?? "null"));
|
|
1284
1310
|
} catch {}
|
|
1285
|
-
const next = {};
|
|
1286
|
-
for (const row of loaded.rows) next[row.id] = {
|
|
1287
|
-
phase: row.report ? "ready" : "failed",
|
|
1288
|
-
report: row.report,
|
|
1289
|
-
error: row.error
|
|
1290
|
-
};
|
|
1291
|
-
setRows(next);
|
|
1292
1311
|
setError(loaded.error);
|
|
1293
1312
|
setRunning(false);
|
|
1294
1313
|
})();
|
|
@@ -1716,6 +1735,672 @@ window.__ModuleLoader__.load({
|
|
|
1716
1735
|
}, "· " + item.title))) : null) : null);
|
|
1717
1736
|
}
|
|
1718
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
|
|
1719
2404
|
//#region src/client.ts
|
|
1720
2405
|
/** dsh-life-pack client 适配器(六边形:browser 侧;爱生活单卡方案)。
|
|
1721
2406
|
*
|
|
@@ -2101,8 +2786,17 @@ window.__ModuleLoader__.load({
|
|
|
2101
2786
|
}
|
|
2102
2787
|
//#endregion
|
|
2103
2788
|
exports.CONFIG_TAB_SLOT = CONFIG_TAB_SLOT;
|
|
2789
|
+
exports.DirectoryBrowserFromRow = DirectoryBrowserFromRow;
|
|
2104
2790
|
exports.apply = apply;
|
|
2791
|
+
exports.canGoUp = canGoUp;
|
|
2792
|
+
exports.createBrowseController = createBrowseController;
|
|
2793
|
+
exports.createDirectoryRowBrowser = createDirectoryRowBrowser;
|
|
2794
|
+
exports.entryPath = entryPath;
|
|
2105
2795
|
exports.inject = inject;
|
|
2796
|
+
exports.pickerModeOf = pickerModeOf;
|
|
2797
|
+
exports.readPickAnswer = readPickAnswer;
|
|
2798
|
+
exports.rowsOf = rowsOf;
|
|
2799
|
+
exports.targetOf = targetOf;
|
|
2106
2800
|
return module.exports;
|
|
2107
2801
|
}
|
|
2108
2802
|
});
|