vitepress-plugin-filelist 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,8 @@ export default {
73
73
  | --- | --- | --- | --- |
74
74
  | `base` | `string` | `''` | OpenList 站点地址;为空时进入演示模式 |
75
75
  | `path` | `string` | `'/'` | 初始目录 |
76
+ | `sources` | `FileListSource[]` | `[]` | 多根挂载;设置后忽略 `path` |
77
+ | `expanded` | `boolean` | `false` | 目录根是否默认展开 |
76
78
  | `token` | `string` | `''` | 可选只读 token;不传则以游客身份请求 |
77
79
  | `password` | `string` | `''` | 目录密码,也可以在运行时输入 |
78
80
  | `title` | `string` | `''` | 工具栏标题;为空时回退到 OpenList 的 `site_title` |
@@ -81,6 +83,35 @@ export default {
81
83
 
82
84
  同时注册了 `<FileList>` 和 `<VPFileList>` 两个组件名。
83
85
 
86
+ ## 多根挂载
87
+
88
+ ```md
89
+ <FileList
90
+ base="https://pan.example.com"
91
+ title="课程资料"
92
+ :sources="[
93
+ { path: '/课程资料/语文' },
94
+ { path: '/课程资料/数学', expanded: true },
95
+ { path: '/课程资料/说明.pdf' }
96
+ ]"
97
+ />
98
+ ```
99
+
100
+ - 目录路径挂载为根文件夹,默认折叠;`expanded` 可为全局或单个 source 打开
101
+ - 文件路径挂载为一条文件记录,直接预览/下载
102
+ - 根目录复选框与勾选、全选效果一致:选中根目录会选中其已加载的子项
103
+ - 每个 source 可单独设置 `title`、`password`
104
+ - 根节点顺序按 `sources` 顺序,子项仍按名称/大小/日期排序
105
+
106
+ ```ts
107
+ interface FileListSource {
108
+ path: string
109
+ title?: string
110
+ password?: string
111
+ expanded?: boolean
112
+ }
113
+ ```
114
+
84
115
  ## 国际化
85
116
 
86
117
  ```md
@@ -34,11 +34,25 @@ interface FileListLabels {
34
34
  batchFailed: string;
35
35
  defaultZip: string;
36
36
  }
37
+ interface FileListSource {
38
+ /** OpenList path, directory or single file. */
39
+ path: string;
40
+ /** Root label. Defaults to the last path segment. */
41
+ title?: string;
42
+ /** Per-source folder password. */
43
+ password?: string;
44
+ /** Initial expanded state for a directory source. Overrides the global `expanded` prop. */
45
+ expanded?: boolean;
46
+ }
37
47
  interface FileListProps {
38
48
  /** OpenList base URL, e.g. `https://pan.example.com`. Empty string enables demo mode. */
39
49
  base?: string;
40
50
  /** Initial directory, defaults to `/`. */
41
51
  path?: string;
52
+ /** Mount multiple paths as root nodes. When set, `path` is ignored. */
53
+ sources?: FileListSource[];
54
+ /** Expand directory roots initially. Defaults to `false`. */
55
+ expanded?: boolean;
42
56
  /** Optional read-only token. Guest access is used when omitted. */
43
57
  token?: string;
44
58
  /** Folder password. The component also asks at runtime when OpenList reports one. */
@@ -64,6 +78,8 @@ interface OpenListEntry {
64
78
  declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
65
79
  base: string;
66
80
  path: string;
81
+ sources: FileListSource[];
82
+ expanded: boolean;
67
83
  token: string;
68
84
  password: string;
69
85
  title: string;
@@ -176,4 +192,4 @@ export declare function compareNodes(a: SortableNode, b: SortableNode, key: Sort
176
192
  * ```
177
193
  */
178
194
  export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
179
- export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type OpenListEntry };
195
+ export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource, type OpenListEntry };
@@ -173,32 +173,33 @@ const _hoisted_23 = [
173
173
  ".indeterminate",
174
174
  "onChange"
175
175
  ];
176
- const _hoisted_24 = ["onClick"];
177
- const _hoisted_25 = {
176
+ const _hoisted_24 = ["title"];
177
+ const _hoisted_25 = ["onClick"];
178
+ const _hoisted_26 = {
178
179
  key: 1,
179
180
  class: "fl-arrow-placeholder"
180
181
  };
181
- const _hoisted_26 = ["onClick"];
182
- const _hoisted_27 = {
182
+ const _hoisted_27 = ["onClick"];
183
+ const _hoisted_28 = {
183
184
  key: 0,
184
185
  class: "fl-folder-icon"
185
186
  };
186
- const _hoisted_28 = {
187
+ const _hoisted_29 = {
187
188
  key: 1,
188
189
  class: "fl-ext-badge"
189
190
  };
190
- const _hoisted_29 = {
191
+ const _hoisted_30 = {
191
192
  key: 2,
192
193
  class: "fl-loading"
193
194
  };
194
- const _hoisted_30 = { class: "fl-cell fl-cell-size" };
195
- const _hoisted_31 = { class: "fl-cell fl-cell-date" };
196
- const _hoisted_32 = { class: "fl-cell fl-cell-op" };
197
- const _hoisted_33 = ["title", "onClick"];
195
+ const _hoisted_31 = { class: "fl-cell fl-cell-size" };
196
+ const _hoisted_32 = { class: "fl-cell fl-cell-date" };
197
+ const _hoisted_33 = { class: "fl-cell fl-cell-op" };
198
198
  const _hoisted_34 = ["title", "onClick"];
199
- const _hoisted_35 = { class: "fl-foot" };
200
- const _hoisted_36 = { key: 0 };
201
- const _hoisted_37 = {
199
+ const _hoisted_35 = ["title", "onClick"];
200
+ const _hoisted_36 = { class: "fl-foot" };
201
+ const _hoisted_37 = { key: 0 };
202
+ const _hoisted_38 = {
202
203
  key: 4,
203
204
  class: "fl-toast"
204
205
  };
@@ -207,6 +208,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
207
208
  props: {
208
209
  base: { default: "" },
209
210
  path: { default: "/" },
211
+ sources: { default: () => [] },
212
+ expanded: {
213
+ type: Boolean,
214
+ default: false
215
+ },
210
216
  token: { default: "" },
211
217
  password: { default: "" },
212
218
  title: { default: "" },
@@ -409,29 +415,40 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
409
415
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
410
416
  return payload.data;
411
417
  }
412
- async function listDir(path) {
418
+ async function listDir(path, password = activePassword.value) {
413
419
  if (demo.value) {
414
420
  await new Promise((resolve) => setTimeout(resolve, 220));
415
421
  return DEMO_TREE[path] ?? [];
416
422
  }
417
423
  return (await request("/api/fs/list", {
418
424
  path,
419
- password: activePassword.value,
425
+ password,
420
426
  page: 1,
421
427
  per_page: 0,
422
428
  refresh: false
423
429
  }))?.content ?? [];
424
430
  }
431
+ function absoluteUrl(raw) {
432
+ if (!raw) return "";
433
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
434
+ const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
435
+ try {
436
+ return new URL(raw, `${root}/`).href;
437
+ } catch {
438
+ return raw;
439
+ }
440
+ }
425
441
  async function resolveUrl(node) {
426
442
  if (demo.value) return "";
443
+ if (node.raw_url) return absoluteUrl(node.raw_url);
427
444
  const cached = downloadCache.get(node.path);
428
445
  if (cached) return cached;
429
446
  let url = "";
430
447
  try {
431
- url = (await request("/api/fs/get", {
448
+ url = absoluteUrl((await request("/api/fs/get", {
432
449
  path: node.path,
433
- password: activePassword.value
434
- }))?.raw_url ?? "";
450
+ password: node.password || activePassword.value
451
+ }))?.raw_url ?? "");
435
452
  } catch {
436
453
  url = "";
437
454
  }
@@ -455,7 +472,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
455
472
  nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
456
473
  for (const node of nodes) if (node.children.length) sortTree(node.children);
457
474
  }
458
- function toNodes(items, parent, depth) {
475
+ function toNodes(items, parent, depth, password = "") {
459
476
  return items.map((item) => ({
460
477
  ...item,
461
478
  path: joinPath(parent, item.name),
@@ -463,7 +480,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
463
480
  expanded: false,
464
481
  loaded: false,
465
482
  loading: false,
466
- children: []
483
+ children: [],
484
+ password
467
485
  }));
468
486
  }
469
487
  function setSort(key) {
@@ -472,15 +490,82 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
472
490
  sortKey.value = key;
473
491
  sortAsc.value = true;
474
492
  }
475
- sortTree(roots.value);
493
+ if (props.sources.length) {
494
+ for (const root of roots.value) if (root.children.length) sortTree(root.children);
495
+ } else sortTree(roots.value);
496
+ }
497
+ function sourceName(path) {
498
+ const parts = path.split("/").filter(Boolean);
499
+ return parts[parts.length - 1] || path;
500
+ }
501
+ async function loadChildren(node) {
502
+ if (node.loaded) return true;
503
+ node.loading = true;
504
+ try {
505
+ node.children = toNodes(await listDir(node.path, node.password), node.path, node.depth + 1, node.password);
506
+ sortTree(node.children);
507
+ node.loaded = true;
508
+ return true;
509
+ } catch (error) {
510
+ showToast(t("folderFailed", { message: errorText(error) }));
511
+ return false;
512
+ } finally {
513
+ node.loading = false;
514
+ }
515
+ }
516
+ async function loadSourceRoot(source) {
517
+ const path = source.path;
518
+ const password = source.password ?? "";
519
+ let item = {
520
+ name: sourceName(path),
521
+ size: 0,
522
+ is_dir: true,
523
+ modified: ""
524
+ };
525
+ if (demo.value) {
526
+ if (/\/[^/]+\.[^/]+$/.test(path)) item = {
527
+ ...item,
528
+ is_dir: false,
529
+ modified: "2026-09-01T08:00:00+08:00"
530
+ };
531
+ } else try {
532
+ const data = await request("/api/fs/get", {
533
+ path,
534
+ password
535
+ });
536
+ item = {
537
+ ...item,
538
+ ...data
539
+ };
540
+ } catch {}
541
+ return {
542
+ ...item,
543
+ name: source.title || item.name || sourceName(path),
544
+ path,
545
+ depth: 0,
546
+ expanded: source.expanded ?? props.expanded,
547
+ loaded: false,
548
+ loading: false,
549
+ children: [],
550
+ password,
551
+ isRoot: true
552
+ };
476
553
  }
477
554
  async function loadRoot() {
478
555
  loadingRoot.value = true;
479
556
  errorMsg.value = "";
480
557
  needsPassword.value = false;
481
558
  try {
559
+ if (props.sources.length) {
560
+ const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
561
+ roots.value = nodes;
562
+ for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
563
+ selected.value = /* @__PURE__ */ new Set();
564
+ downloadCache.clear();
565
+ return;
566
+ }
482
567
  const items = await listDir(props.path);
483
- roots.value = toNodes(items, props.path, 0);
568
+ roots.value = toNodes(items, props.path, 0, activePassword.value);
484
569
  sortTree(roots.value);
485
570
  selected.value = /* @__PURE__ */ new Set();
486
571
  downloadCache.clear();
@@ -498,19 +583,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
498
583
  node.expanded = false;
499
584
  return;
500
585
  }
501
- if (!node.loaded) {
502
- node.loading = true;
503
- try {
504
- node.children = toNodes(await listDir(node.path), node.path, node.depth + 1);
505
- sortTree(node.children);
506
- node.loaded = true;
507
- } catch (error) {
508
- showToast(t("folderFailed", { message: errorText(error) }));
509
- return;
510
- } finally {
511
- node.loading = false;
512
- }
513
- }
586
+ if (!await loadChildren(node)) return;
514
587
  node.expanded = true;
515
588
  }
516
589
  const visible = computed(() => {
@@ -735,6 +808,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
735
808
  key: node.path,
736
809
  class: normalizeClass(["fl-row", {
737
810
  "fl-row-dir": node.is_dir,
811
+ "fl-row-root": node.isRoot,
738
812
  "fl-row-selected": selected.value.has(node.path)
739
813
  }])
740
814
  }, [
@@ -746,23 +820,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
746
820
  }, null, 40, _hoisted_23)]),
747
821
  createElementVNode("span", {
748
822
  class: "fl-cell fl-cell-name",
823
+ title: node.isRoot ? node.path : void 0,
749
824
  style: normalizeStyle({ paddingLeft: `${node.depth * 22}px` })
750
825
  }, [node.is_dir ? (openBlock(), createElementBlock("button", {
751
826
  key: 0,
752
827
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
753
828
  type: "button",
754
829
  onClick: ($event) => toggleExpand(node)
755
- }, "▸", 10, _hoisted_24)) : (openBlock(), createElementBlock("span", _hoisted_25)), createElementVNode("span", {
830
+ }, "▸", 10, _hoisted_25)) : (openBlock(), createElementBlock("span", _hoisted_26)), createElementVNode("span", {
756
831
  class: normalizeClass(["fl-name", { "fl-name-link": node.is_dir }]),
757
832
  onClick: ($event) => node.is_dir ? toggleExpand(node) : download(node)
758
833
  }, [
759
- node.is_dir ? (openBlock(), createElementBlock("span", _hoisted_27, "📁")) : (openBlock(), createElementBlock("span", _hoisted_28, toDisplayString(unref(extOf)(node.name) || "FILE"), 1)),
834
+ node.is_dir ? (openBlock(), createElementBlock("span", _hoisted_28, "📁")) : (openBlock(), createElementBlock("span", _hoisted_29, toDisplayString(unref(extOf)(node.name) || "FILE"), 1)),
760
835
  createTextVNode(" " + toDisplayString(node.name) + " ", 1),
761
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_29, "…")) : createCommentVNode("v-if", true)
762
- ], 10, _hoisted_26)], 4),
763
- createElementVNode("span", _hoisted_30, toDisplayString(node.is_dir ? "-" : unref(formatSize)(node.size)), 1),
764
- createElementVNode("span", _hoisted_31, toDisplayString(unref(formatDate)(node.modified)), 1),
765
- createElementVNode("span", _hoisted_32, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
836
+ node.loading ? (openBlock(), createElementBlock("span", _hoisted_30, "…")) : createCommentVNode("v-if", true)
837
+ ], 10, _hoisted_27)], 12, _hoisted_24),
838
+ createElementVNode("span", _hoisted_31, toDisplayString(node.is_dir ? "-" : unref(formatSize)(node.size)), 1),
839
+ createElementVNode("span", _hoisted_32, toDisplayString(unref(formatDate)(node.modified)), 1),
840
+ createElementVNode("span", _hoisted_33, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
766
841
  key: 0,
767
842
  class: "fl-download",
768
843
  type: "button",
@@ -782,7 +857,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
782
857
  cx: "12",
783
858
  cy: "12",
784
859
  r: "3"
785
- })], -1)])], 8, _hoisted_33)) : createCommentVNode("v-if", true), createElementVNode("button", {
860
+ })], -1)])], 8, _hoisted_34)) : createCommentVNode("v-if", true), createElementVNode("button", {
786
861
  class: "fl-download",
787
862
  type: "button",
788
863
  title: t("download"),
@@ -801,11 +876,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
801
876
  createElementVNode("path", { d: "M12 3v12" }),
802
877
  createElementVNode("path", { d: "m7 10 5 5 5-5" }),
803
878
  createElementVNode("path", { d: "M5 21h14" })
804
- ], -1)])], 8, _hoisted_34)], 64)) : createCommentVNode("v-if", true)])
879
+ ], -1)])], 8, _hoisted_35)], 64)) : createCommentVNode("v-if", true)])
805
880
  ], 2);
806
881
  }), 128)),
807
- createElementVNode("div", _hoisted_35, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("selected", { count: selected.value.size })), 1), unloadedFolders.value ? (openBlock(), createElementBlock("span", _hoisted_36, " · " + toDisplayString(t("unloaded", { count: unloadedFolders.value })), 1)) : createCommentVNode("v-if", true)]),
808
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_37, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
882
+ createElementVNode("div", _hoisted_36, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("selected", { count: selected.value.size })), 1), unloadedFolders.value ? (openBlock(), createElementBlock("span", _hoisted_37, " · " + toDisplayString(t("unloaded", { count: unloadedFolders.value })), 1)) : createCommentVNode("v-if", true)]),
883
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_38, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
809
884
  ]);
810
885
  };
811
886
  }
@@ -34,11 +34,25 @@ interface FileListLabels {
34
34
  batchFailed: string;
35
35
  defaultZip: string;
36
36
  }
37
+ interface FileListSource {
38
+ /** OpenList path, directory or single file. */
39
+ path: string;
40
+ /** Root label. Defaults to the last path segment. */
41
+ title?: string;
42
+ /** Per-source folder password. */
43
+ password?: string;
44
+ /** Initial expanded state for a directory source. Overrides the global `expanded` prop. */
45
+ expanded?: boolean;
46
+ }
37
47
  interface FileListProps {
38
48
  /** OpenList base URL, e.g. `https://pan.example.com`. Empty string enables demo mode. */
39
49
  base?: string;
40
50
  /** Initial directory, defaults to `/`. */
41
51
  path?: string;
52
+ /** Mount multiple paths as root nodes. When set, `path` is ignored. */
53
+ sources?: FileListSource[];
54
+ /** Expand directory roots initially. Defaults to `false`. */
55
+ expanded?: boolean;
42
56
  /** Optional read-only token. Guest access is used when omitted. */
43
57
  token?: string;
44
58
  /** Folder password. The component also asks at runtime when OpenList reports one. */
@@ -64,6 +78,8 @@ interface OpenListEntry {
64
78
  declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
65
79
  base: string;
66
80
  path: string;
81
+ sources: FileListSource[];
82
+ expanded: boolean;
67
83
  token: string;
68
84
  password: string;
69
85
  title: string;
@@ -176,4 +192,4 @@ export declare function compareNodes(a: SortableNode, b: SortableNode, key: Sort
176
192
  * ```
177
193
  */
178
194
  export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
179
- export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type OpenListEntry };
195
+ export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource, type OpenListEntry };
@@ -131,6 +131,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
131
131
  props: {
132
132
  base: { default: "" },
133
133
  path: { default: "/" },
134
+ sources: { default: () => [] },
135
+ expanded: {
136
+ type: Boolean,
137
+ default: false
138
+ },
134
139
  token: { default: "" },
135
140
  password: { default: "" },
136
141
  title: { default: "" },
@@ -167,6 +172,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
167
172
  const batchBusy = ref(false);
168
173
  const batchProgress = ref(0);
169
174
  const downloadCache = /* @__PURE__ */ new Map();
175
+ let toastTimer;
176
+ function showToast(msg) {
177
+ toast.value = msg;
178
+ clearTimeout(toastTimer);
179
+ toastTimer = setTimeout(() => toast.value = "", 3200);
180
+ }
170
181
  function errorText(error) {
171
182
  return error instanceof Error ? error.message : String(error);
172
183
  }
@@ -327,14 +338,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
327
338
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
328
339
  return payload.data;
329
340
  }
330
- async function listDir(path) {
341
+ async function listDir(path, password = activePassword.value) {
331
342
  if (demo.value) {
332
343
  await new Promise((resolve) => setTimeout(resolve, 220));
333
344
  return DEMO_TREE[path] ?? [];
334
345
  }
335
346
  return (await request("/api/fs/list", {
336
347
  path,
337
- password: activePassword.value,
348
+ password,
338
349
  page: 1,
339
350
  per_page: 0,
340
351
  refresh: false
@@ -353,7 +364,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
353
364
  nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
354
365
  for (const node of nodes) if (node.children.length) sortTree(node.children);
355
366
  }
356
- function toNodes(items, parent, depth) {
367
+ function toNodes(items, parent, depth, password = "") {
357
368
  return items.map((item) => ({
358
369
  ...item,
359
370
  path: joinPath(parent, item.name),
@@ -361,16 +372,82 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
361
372
  expanded: false,
362
373
  loaded: false,
363
374
  loading: false,
364
- children: []
375
+ children: [],
376
+ password
365
377
  }));
366
378
  }
379
+ function sourceName(path) {
380
+ const parts = path.split("/").filter(Boolean);
381
+ return parts[parts.length - 1] || path;
382
+ }
383
+ async function loadChildren(node) {
384
+ if (node.loaded) return true;
385
+ node.loading = true;
386
+ try {
387
+ node.children = toNodes(await listDir(node.path, node.password), node.path, node.depth + 1, node.password);
388
+ sortTree(node.children);
389
+ node.loaded = true;
390
+ return true;
391
+ } catch (error) {
392
+ showToast(t("folderFailed", { message: errorText(error) }));
393
+ return false;
394
+ } finally {
395
+ node.loading = false;
396
+ }
397
+ }
398
+ async function loadSourceRoot(source) {
399
+ const path = source.path;
400
+ const password = source.password ?? "";
401
+ let item = {
402
+ name: sourceName(path),
403
+ size: 0,
404
+ is_dir: true,
405
+ modified: ""
406
+ };
407
+ if (demo.value) {
408
+ if (/\/[^/]+\.[^/]+$/.test(path)) item = {
409
+ ...item,
410
+ is_dir: false,
411
+ modified: "2026-09-01T08:00:00+08:00"
412
+ };
413
+ } else try {
414
+ const data = await request("/api/fs/get", {
415
+ path,
416
+ password
417
+ });
418
+ item = {
419
+ ...item,
420
+ ...data
421
+ };
422
+ } catch {}
423
+ return {
424
+ ...item,
425
+ name: source.title || item.name || sourceName(path),
426
+ path,
427
+ depth: 0,
428
+ expanded: source.expanded ?? props.expanded,
429
+ loaded: false,
430
+ loading: false,
431
+ children: [],
432
+ password,
433
+ isRoot: true
434
+ };
435
+ }
367
436
  async function loadRoot() {
368
437
  loadingRoot.value = true;
369
438
  errorMsg.value = "";
370
439
  needsPassword.value = false;
371
440
  try {
441
+ if (props.sources.length) {
442
+ const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
443
+ roots.value = nodes;
444
+ for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
445
+ selected.value = /* @__PURE__ */ new Set();
446
+ downloadCache.clear();
447
+ return;
448
+ }
372
449
  const items = await listDir(props.path);
373
- roots.value = toNodes(items, props.path, 0);
450
+ roots.value = toNodes(items, props.path, 0, activePassword.value);
374
451
  sortTree(roots.value);
375
452
  selected.value = /* @__PURE__ */ new Set();
376
453
  downloadCache.clear();
@@ -454,8 +531,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
454
531
  ssrRenderList(visible.value, (node) => {
455
532
  _push(`<div class="${ssrRenderClass([{
456
533
  "fl-row-dir": node.is_dir,
534
+ "fl-row-root": node.isRoot,
457
535
  "fl-row-selected": selected.value.has(node.path)
458
- }, "fl-row"])}"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></span><span class="fl-cell fl-cell-name" style="${ssrRenderStyle({ paddingLeft: `${node.depth * 22}px` })}">`);
536
+ }, "fl-row"])}"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></span><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)} style="${ssrRenderStyle({ paddingLeft: `${node.depth * 22}px` })}">`);
459
537
  if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}">▸</button>`);
460
538
  else _push(`<span class="fl-arrow-placeholder"></span>`);
461
539
  _push(`<span class="${ssrRenderClass([{ "fl-name-link": node.is_dir }, "fl-name"])}">`);
@@ -125,6 +125,10 @@
125
125
  background: var(--vp-c-bg-soft);
126
126
  }
127
127
 
128
+ .fl-row-root .fl-name {
129
+ font-weight: 600;
130
+ }
131
+
128
132
  .fl-row-selected {
129
133
  background: var(--vp-c-brand-soft);
130
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitepress-plugin-filelist",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "packageManager": "pnpm@11.25.0",
5
5
  "type": "module",
6
6
  "description": "OpenList file browser for VitePress: tree view, batch ZIP download, i18n, password and token support.",