dsh-life-pack 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.
Files changed (43) hide show
  1. package/README.md +38 -0
  2. package/dist/client.js +166 -22
  3. package/dist/client.js.map +1 -1
  4. package/dist/directory-browser-api.d.ts +10 -7
  5. package/dist/directory-browser-api.d.ts.map +1 -1
  6. package/dist/directory-browser-api.js +8 -6
  7. package/dist/directory-browser-api.js.map +1 -1
  8. package/dist/directory-browser-contract.d.ts +51 -3
  9. package/dist/directory-browser-contract.d.ts.map +1 -1
  10. package/dist/directory-browser-contract.js +56 -5
  11. package/dist/directory-browser-contract.js.map +1 -1
  12. package/dist/directory-browser-parts.d.ts +68 -0
  13. package/dist/directory-browser-parts.d.ts.map +1 -0
  14. package/dist/directory-browser-parts.js +195 -0
  15. package/dist/directory-browser-parts.js.map +1 -0
  16. package/dist/directory-browser-roots.d.ts +21 -0
  17. package/dist/directory-browser-roots.d.ts.map +1 -0
  18. package/dist/directory-browser-roots.js +40 -0
  19. package/dist/directory-browser-roots.js.map +1 -0
  20. package/dist/directory-browser-state.d.ts +38 -5
  21. package/dist/directory-browser-state.d.ts.map +1 -1
  22. package/dist/directory-browser-state.js +69 -2
  23. package/dist/directory-browser-state.js.map +1 -1
  24. package/dist/directory-browser-ui.d.ts +8 -23
  25. package/dist/directory-browser-ui.d.ts.map +1 -1
  26. package/dist/directory-browser-ui.js +10 -148
  27. package/dist/directory-browser-ui.js.map +1 -1
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/roots.d.ts +34 -0
  33. package/dist/roots.d.ts.map +1 -0
  34. package/dist/roots.js +95 -0
  35. package/dist/roots.js.map +1 -0
  36. package/dist/update-contract.d.ts +7 -0
  37. package/dist/update-contract.d.ts.map +1 -1
  38. package/dist/update-contract.js +7 -0
  39. package/dist/update-contract.js.map +1 -1
  40. package/dist/update-host.d.ts.map +1 -1
  41. package/dist/update-host.js +5 -1
  42. package/dist/update-host.js.map +1 -1
  43. package/package.json +6 -1
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # 爱生活(dsh-life-pack)
2
+
3
+ DSH 设置弹窗里的「爱生活」卡:把六个技能(备忘录、卡路里、私家大厨、居家管家、作息管家、饼干记账)的**设置页**收进一排页签,另带**配置体检**与**更新区**。
4
+
5
+ One settings card for the iLife home-life suite: memo, calorie, chef, home, schedule and bill in a tab bar, with a read-only config health check and an update panel.
6
+
7
+ ## 装
8
+
9
+ ```sh
10
+ dsh plugin add dsh-life-pack # 只要这张卡
11
+ dsh plugin add dsh-life-pack dsh-calorie # 卡 + 某一个技能(一条命令两个包)
12
+ ```
13
+
14
+ 某个技能没装时,那一个页签的位置会变成**补装提示**,带上该条可直接复制的命令——不需要先装齐六个。
15
+
16
+ ## 卡里有什么
17
+
18
+ | 区域 | 做什么 |
19
+ | --- | --- |
20
+ | 页签条 | 六个技能的设置页各自注册进「爱生活页签槽」;点谁切谁 |
21
+ | 配置体检 | 点一次,按家取数:顶部一行总账,每家一张表(每条:灯/标题/档/一句话/去哪修)。**只读**——不建目录、不改配置、不自动重置 |
22
+ | 更新区 | 七个包(它自己 + 六家)检查更新、逐个安装;装完按事实给下一步(重启 DSH 生效/重试/重新检查) |
23
+ | 面板入口 | 右上角:检查更新、去仓库点 Star、反馈问题;底部一句「作者其他插件」 |
24
+
25
+ ## 它不做什么
26
+
27
+ 不 import 任何一个单品包,也不依赖它们——依赖方向是反的(单品硬依赖 `dsh-life-pack`)。总管只做导航与聚合:六家的页面各自注册进槽位,缺席就渲染补装提示,不轮询、不代管。
28
+
29
+ ## 配置与数据
30
+
31
+ - 配置:`~/.ilife/<技能>.yaml`(六个技能各一份,在各自的设置页里改)
32
+ - 数据目录:默认 `~/.ilife/data`(可在各技能设置页里改;留空=用默认)
33
+
34
+ ## 链接
35
+
36
+ - 仓库与整套插件(六个单品同仓):<https://github.com/FeatherHunter/ilife>
37
+ - 问题与建议:<https://github.com/FeatherHunter/ilife/issues>
38
+ - 许可:MIT
package/dist/client.js CHANGED
@@ -179,7 +179,14 @@ window.__ModuleLoader__.load({
179
179
  * 宿主读**自己这份已安装包**的 `package.json`(`manager-version.ts`),面板只渲染读到的值。
180
180
  * 为什么走电话:面板是浏览器产物,禁 node 内建(`test/client-bundle-48.test.mjs` 看门),
181
181
  * 读盘只许在宿主半;与卡路里 #130 同一条路(host 读 → RPC → client 纯渲染)。 */
182
- version: "ilife-manager.version"
182
+ version: "ilife-manager.version",
183
+ /** 本机「根」清单(票 #744):入参 `{}`,回包 `{roots: [{path, kind}]}`。
184
+ *
185
+ * 为什么这条能力住在总管:浏览器里没有卷清单接口,而宿主那条目录选择接缝只有
186
+ * `list`/`createDirectory` 两格——「这台机器上有哪些盘」只能由宿主去问操作系统。
187
+ * 六家的目录浏览器都取这一通电话,故名字与实现都只有这一处(清单本身见 `roots.ts`)。
188
+ * 回包**恒成功**:列不出来是空清单,不是错误码(界面据此只是不画那一行)。 */
189
+ roots: "ilife-manager.roots"
183
190
  };
184
191
  /** 版本读不到时两侧共用的降级字面量(面板照原样显示,不假装知道版本)。
185
192
  *
@@ -1747,10 +1754,15 @@ window.__ModuleLoader__.load({
1747
1754
  if (typeof raw !== "object" || raw === null) return false;
1748
1755
  return typeof raw.pick === "function";
1749
1756
  }
1750
- /** 三态判定:先 native(有 pick),再 browse(有两格原语),都没有=`none`。 */
1757
+ /** 三态判定:**先看两格浏览原语**(都在=应用内浏览器这条路在),再看单独的 `pick`,都没有=`none`。
1758
+ *
1759
+ * 为什么浏览优先:客户端命名空间上三条动词一定都在(由 `TYPERT_REMOTE.descriptors` 生成,
1760
+ * 与组合里服务哪种能力无关),所以两格原语在=这台宿主**可能**给浏览;真调一次才知道给不给,
1761
+ * 不给({@link CAPABILITY_REFUSED})再由调用方换系统对话框。反过来先认 `pick` 就会在
1762
+ * Desktop 这种「只服务 browse」的宿主上每次都去唤一次系统对话框、被拒一次(#744 实测)。 */
1751
1763
  function pickerModeOf(raw) {
1752
- if (hasPickFn(raw)) return "native";
1753
- return isBrowseFace(raw) ? "browse" : "none";
1764
+ if (isBrowseFace(raw)) return "browse";
1765
+ return hasPickFn(raw) ? "native" : "none";
1754
1766
  }
1755
1767
  /** 平台回执 → 三态(**永不抛**):成功回的是信封里的 `value` 不是路径;`ok:false` 是「供不了」不是「取消」。
1756
1768
  *
@@ -1771,8 +1783,10 @@ window.__ModuleLoader__.load({
1771
1783
  } : { kind: "cancelled" };
1772
1784
  }
1773
1785
  const detail = answer.error?.message?.trim() ?? "";
1786
+ const code = answer.error?.code;
1774
1787
  return {
1775
1788
  kind: "unavailable",
1789
+ ...typeof code === "string" && code !== "" ? { code } : {},
1776
1790
  message: "打不开系统文件夹对话框" + (detail === "" ? "" : "(" + detail + ")") + ":请直接在框里填绝对路径。"
1777
1791
  };
1778
1792
  }
@@ -1877,7 +1891,8 @@ window.__ModuleLoader__.load({
1877
1891
  draft: deps.initialPath,
1878
1892
  filter: "",
1879
1893
  creating: null,
1880
- notice: null
1894
+ notice: null,
1895
+ roots: []
1881
1896
  };
1882
1897
  /** 每次列举带一个序号:晚回来的旧回执直接丢掉,不许覆盖新一层。 */
1883
1898
  let generation = 0;
@@ -1888,6 +1903,18 @@ window.__ModuleLoader__.load({
1888
1903
  };
1889
1904
  for (const listener of [...listeners]) listener(state);
1890
1905
  };
1906
+ /** 根清单只问一次:取不到就是空(不报错、不重试——它只是图上的一行入口)。
1907
+ *
1908
+ * 「问一次」钉在这里而不是图里:视图那半每次重画都会重新求值,问在这里才谈得上一次。 */
1909
+ let rootsAsked = false;
1910
+ const loadRoots = async () => {
1911
+ if (rootsAsked || deps.rootsSource === void 0) return;
1912
+ rootsAsked = true;
1913
+ try {
1914
+ const roots = await deps.rootsSource();
1915
+ if (roots.length > 0) emit({ roots });
1916
+ } catch {}
1917
+ };
1891
1918
  const humanize = (cause) => {
1892
1919
  const raw = typeof cause === "object" && cause !== null ? cause : {};
1893
1920
  const message = typeof raw.message === "string" && raw.message.trim() !== "" ? raw.message.trim() : String(cause);
@@ -1944,7 +1971,16 @@ window.__ModuleLoader__.load({
1944
1971
  listeners.add(listener);
1945
1972
  return () => listeners.delete(listener);
1946
1973
  },
1947
- open: () => load(null),
1974
+ async open() {
1975
+ loadRoots();
1976
+ const wanted = deps.initialPath.trim();
1977
+ if (wanted !== "") {
1978
+ await load(wanted);
1979
+ if (state.phase === "ready") return state;
1980
+ }
1981
+ await load(null);
1982
+ return state;
1983
+ },
1948
1984
  enter: (path) => load(path),
1949
1985
  async up() {
1950
1986
  const listing = state.listing;
@@ -2049,10 +2085,12 @@ window.__ModuleLoader__.load({
2049
2085
  initialPath: deps.initialPath,
2050
2086
  onPicked: deps.onPicked,
2051
2087
  onClose: () => deps.onClosed?.(),
2052
- ...deps.failureLabel === void 0 ? {} : { failureLabel: deps.failureLabel }
2088
+ ...deps.failureLabel === void 0 ? {} : { failureLabel: deps.failureLabel },
2089
+ ...deps.rootsSource === void 0 ? {} : { rootsSource: deps.rootsSource }
2053
2090
  });
2054
2091
  return {
2055
2092
  open: () => controller.open(),
2093
+ subscribe: (listener) => controller.subscribe(listener),
2056
2094
  state: () => controller.getState(),
2057
2095
  summary: () => describe(controller.getState()),
2058
2096
  actions: {
@@ -2070,18 +2108,22 @@ window.__ModuleLoader__.load({
2070
2108
  };
2071
2109
  }
2072
2110
  //#endregion
2073
- //#region src/directory-browser-ui.ts
2074
- /** 文件浏览器的视图半:把 `BrowseState` 画成一张对话框。**不认识宿主**,也不自己做 IO。
2111
+ //#region src/directory-browser-parts.ts
2112
+ /** 文件浏览器视图半的**零件**:样式表与四行控件(面包屑/根/一行子目录/新建行)。
2075
2113
  *
2076
- * 手写 `React.createElement`(本包 client 束禁 JSX,见 `tsdown.config.ts` 的既成口径)。
2077
- * 只读状态、只回调;进哪一层、选到什么由操作半(`directory-browser-state.ts`)算。
2114
+ * 为什么拆出来:整张对话框的组版与这些零件的变化频率不同——零件跟着「图上多一行/少一行」动,
2115
+ * 组版跟着「对话框长什么样」动。拆开之后两边都在 350 行告警线内,而对外面不变
2116
+ * (`directory-browser-ui.ts` 照旧转出组件与两个接线函数)。
2117
+ *
2118
+ * 本件与视图半同样**不认识宿主**:只认状态与文案。
2078
2119
  */
2079
- const EMPTY_LISTING = {
2080
- path: "",
2081
- home: "",
2082
- crumbs: [],
2083
- entries: [],
2084
- truncated: false
2120
+ /** 根的类型说明(悬停可见):**本件自己的词汇**,与取数方无关。 */
2121
+ const ROOT_KIND_TEXT = {
2122
+ fixed: "固定磁盘",
2123
+ network: "网络驱动器",
2124
+ removable: "可移动磁盘",
2125
+ optical: "光驱",
2126
+ other: "磁盘"
2085
2127
  };
2086
2128
  const S$1 = {
2087
2129
  scrim: {
@@ -2147,6 +2189,24 @@ window.__ModuleLoader__.load({
2147
2189
  opacity: .45,
2148
2190
  fontSize: 12
2149
2191
  },
2192
+ roots: {
2193
+ display: "flex",
2194
+ flexWrap: "wrap",
2195
+ alignItems: "center",
2196
+ gap: 6,
2197
+ padding: "0 12px 8px",
2198
+ fontSize: 12
2199
+ },
2200
+ rootsLabel: {
2201
+ opacity: .72,
2202
+ marginRight: 2
2203
+ },
2204
+ rootButton: {
2205
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
2206
+ borderRadius: 6,
2207
+ padding: "3px 8px",
2208
+ opacity: 1
2209
+ },
2150
2210
  pathRow: {
2151
2211
  display: "flex",
2152
2212
  gap: 6,
@@ -2225,7 +2285,8 @@ window.__ModuleLoader__.load({
2225
2285
  },
2226
2286
  error: { color: "var(--dsw-alias-label-error, #e66)" }
2227
2287
  };
2228
- function crumbRow(state, labels, onEnter) {
2288
+ /** 面包屑:从文件系统根到当前层,每一格都是跳转目标(`listing.crumbs` 由取数方给)。 */
2289
+ function crumbRow(state, onEnter) {
2229
2290
  const listing = state.listing;
2230
2291
  if (listing === null || listing.crumbs.length === 0) return null;
2231
2292
  const parts = [];
@@ -2244,10 +2305,34 @@ window.__ModuleLoader__.load({
2244
2305
  },
2245
2306
  title: crumb.path,
2246
2307
  onClick: () => onEnter(crumb.path)
2247
- }, index === 0 ? labels.up : crumb.name));
2308
+ }, crumb.name));
2248
2309
  });
2249
2310
  return react.createElement("div", { style: S$1.crumbs }, parts);
2250
2311
  }
2312
+ /** 可跳转的根那一行(Windows 上的盘符等)。**少于两个根就不画**:一个根没有「跳到别处」可言。 */
2313
+ function rootsRow(state, labels, onEnter) {
2314
+ if (state.roots.length < 2) return null;
2315
+ const parts = [react.createElement("span", {
2316
+ key: "roots-label",
2317
+ style: S$1.rootsLabel
2318
+ }, labels.roots)];
2319
+ for (const root of state.roots) parts.push(react.createElement("button", {
2320
+ key: root.path,
2321
+ type: "button",
2322
+ style: {
2323
+ ...S$1.chromeButton,
2324
+ ...S$1.rootButton
2325
+ },
2326
+ title: ROOT_KIND_TEXT[root.kind] ?? ROOT_KIND_TEXT.other,
2327
+ onClick: () => onEnter(root.path)
2328
+ }, root.path));
2329
+ return react.createElement("div", {
2330
+ style: S$1.roots,
2331
+ role: "group",
2332
+ "aria-label": labels.roots
2333
+ }, parts);
2334
+ }
2335
+ /** 一行子目录:点名字进它,点「选」把它当目标(「选」再点一次取消)。 */
2251
2336
  function entryRow(entry, selected, labels, onToggleSelect, onEnter) {
2252
2337
  return react.createElement("div", {
2253
2338
  key: entry.path,
@@ -2276,6 +2361,7 @@ window.__ModuleLoader__.load({
2276
2361
  onClick: () => onToggleSelect(entry.path)
2277
2362
  }, selected ? "✓" : labels.select));
2278
2363
  }
2364
+ /** 「新建文件夹」那一行(`creatingName === null` 时收起)。 */
2279
2365
  function createRow(labels, onName, onCancel, onCreate, creatingName) {
2280
2366
  if (creatingName === null) return null;
2281
2367
  return react.createElement("div", { style: {
@@ -2301,6 +2387,21 @@ window.__ModuleLoader__.load({
2301
2387
  onClick: onCancel
2302
2388
  }, labels.createCancel));
2303
2389
  }
2390
+ //#endregion
2391
+ //#region src/directory-browser-ui.ts
2392
+ /** 文件浏览器的视图半:把 `BrowseState` 画成一张对话框。**不认识宿主**,也不自己做 IO。
2393
+ *
2394
+ * 手写 `React.createElement`(本包 client 束禁 JSX,见 `tsdown.config.ts` 的既成口径)。
2395
+ * 只读状态、只回调;进哪一层、选到什么由操作半(`directory-browser-state.ts`)算。
2396
+ * 样式与四行控件在 `directory-browser-parts.ts`(那里跟着「多一行/少一行」动,本件只管组版)。
2397
+ */
2398
+ const EMPTY_LISTING = {
2399
+ path: "",
2400
+ home: "",
2401
+ crumbs: [],
2402
+ entries: [],
2403
+ truncated: false
2404
+ };
2304
2405
  /** 画一张文件浏览器对话框。`open=false` 时返回 null(本件不挂 portal,由调用方决定挂在哪)。 */
2305
2406
  function DirectoryBrowser(props) {
2306
2407
  const { open, state, labels } = props;
@@ -2310,7 +2411,8 @@ window.__ModuleLoader__.load({
2310
2411
  const listing = state.listing ?? EMPTY_LISTING;
2311
2412
  const hidden = hiddenCount(listing);
2312
2413
  const body = [];
2313
- body.push(crumbRow(state, labels, props.onEnter));
2414
+ body.push(crumbRow(state, props.onEnter));
2415
+ body.push(rootsRow(state, labels, props.onEnter));
2314
2416
  body.push(react.createElement("div", { style: S$1.pathRow }, react.createElement("input", {
2315
2417
  style: S$1.input,
2316
2418
  value: state.draft,
@@ -2391,16 +2493,56 @@ window.__ModuleLoader__.load({
2391
2493
  /** 目录行接线的 React 半边:把操作半那组动作直接喂给组件。
2392
2494
  *
2393
2495
  * 操作半(`directory-browser-state.ts` 的 `createDirectoryRowBrowser`)不碰 React,
2394
- * 这里只把它的 `actions` 摊进 props —— 于是「进哪一层」那套在 Node 里可测,这里只做接线。 */
2496
+ * 这里只把它的 `actions` 摊进 props —— 于是「进哪一层」那套在 Node 里可测,这里只做接线。
2497
+ *
2498
+ * `useSyncExternalStore` 是**必需**的:列举是异步落定的,不订阅的话这一帧画完就再没有重画,
2499
+ * 图上会永远停在「正在读取…」(#744 实测)。 */
2395
2500
  function DirectoryBrowserFromRow(props) {
2501
+ const state = react.useSyncExternalStore(props.row.subscribe, props.row.state);
2396
2502
  return DirectoryBrowser({
2397
2503
  open: props.open,
2398
- state: props.row.state(),
2504
+ state,
2399
2505
  labels: props.labels,
2400
2506
  ...props.row.actions
2401
2507
  });
2402
2508
  }
2403
2509
  //#endregion
2510
+ //#region src/directory-browser-roots.ts
2511
+ /** 认一认某个值像不像一个根(认不出的行丢掉:图的干净比多画一行重要)。 */
2512
+ function isRootRow(raw) {
2513
+ if (typeof raw !== "object" || raw === null) return false;
2514
+ const row = raw;
2515
+ return typeof row.path === "string" && row.path !== "" && typeof row.kind === "string";
2516
+ }
2517
+ /** 信封 → 根清单(纯函数,**永不抛**):`{ok:true, value:{roots:[…]}}` 之外的形状一律当空。 */
2518
+ function readRootsAnswer(raw) {
2519
+ const answer = typeof raw === "object" && raw !== null ? raw : {};
2520
+ if (answer.ok !== true) return [];
2521
+ const value = typeof answer.value === "object" && answer.value !== null ? answer.value : {};
2522
+ if (!Array.isArray(value.roots)) return [];
2523
+ return value.roots.filter(isRootRow).map((row) => ({
2524
+ path: row.path,
2525
+ kind: row.kind
2526
+ }));
2527
+ }
2528
+ /** 建一个「根清单取数器」:给调用口与三个名字,回一个 `() => Promise<RootRow[]>`。
2529
+ *
2530
+ * 界面按需调它(开图一次),不在渲染里调,也不缓存到这里——缓存是宿主半的事。 */
2531
+ function createRootsSource(input) {
2532
+ return async () => {
2533
+ const call = input.getCall();
2534
+ if (typeof call !== "function") return [];
2535
+ try {
2536
+ return readRootsAnswer(await call(input.base, input.endpoint, {
2537
+ method: input.method,
2538
+ payload: {}
2539
+ }));
2540
+ } catch {
2541
+ return [];
2542
+ }
2543
+ };
2544
+ }
2545
+ //#endregion
2404
2546
  //#region src/client.ts
2405
2547
  /** dsh-life-pack client 适配器(六边形:browser 侧;爱生活单卡方案)。
2406
2548
  *
@@ -2791,10 +2933,12 @@ window.__ModuleLoader__.load({
2791
2933
  exports.canGoUp = canGoUp;
2792
2934
  exports.createBrowseController = createBrowseController;
2793
2935
  exports.createDirectoryRowBrowser = createDirectoryRowBrowser;
2936
+ exports.createRootsSource = createRootsSource;
2794
2937
  exports.entryPath = entryPath;
2795
2938
  exports.inject = inject;
2796
2939
  exports.pickerModeOf = pickerModeOf;
2797
2940
  exports.readPickAnswer = readPickAnswer;
2941
+ exports.readRootsAnswer = readRootsAnswer;
2798
2942
  exports.rowsOf = rowsOf;
2799
2943
  exports.targetOf = targetOf;
2800
2944
  return module.exports;