vitepress-plugin-filelist 0.6.0 → 0.6.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
@@ -8,7 +8,7 @@
8
8
  - ✅ 勾选文件;选中文件夹会自动加载并选中其下所有层级的文件
9
9
  - 📦 批量打包 ZIP(基于 `client-zip`),带进度、可取消;Chrome / Edge 边下载边写入磁盘,不受大小限制;保留目录结构,不同目录的同名文件不会互相覆盖
10
10
  - 📄 大目录每个文件夹先显示 200 条,可“显示更多 / 全部显示”
11
- - ⬇️ 单文件下载(`fs/get` 的 `raw_url`,回退 `/d/`)
11
+ - ⬇️ 单文件下载(`fs/get` 的 `raw_url`,回退 `/d/`):交给浏览器下载;OpenList 自己的 `/p/`、`/d/` 链接在当前页直接下载、不打开新标签页,存储直链在新标签页打开
12
12
  - 👁️ 弹窗预览(不跳转):图片、音视频、文本、PDF;Office 等其它类型显示文件信息与下载
13
13
  - 🔐 目录密码输入,可选只读 token
14
14
  - 🔎 当前树过滤,按名称 / 大小 / 日期排序
@@ -135,6 +135,7 @@ interface FileListSource {
135
135
  - Chrome / Edge(含较新的 Android Chrome):点“下载 ZIP”先弹出系统“另存为”窗口,之后边下载边写入,内存占用很小,不受大小限制
136
136
  - Firefox、Safari、iPhone:浏览器只能先把文件放进内存再打包。按已选总大小判断:超过 `zipWarnSize` 先确认,超过 `zipMaxSize` 不打包并建议分批
137
137
  - 打包期间操作条显示进度(大小与文件数),可随时取消;勾选暂时锁定。任一文件失败会整体取消并提示文件名
138
+ - 同时取 4 个文件(顺序不变),减少每个文件的网络等待;进度每 100 ms 更新一次
138
139
  - ZIP 只存储不压缩(网盘文件多为已压缩格式),支持超过 4 GB(Zip64)
139
140
 
140
141
  ## 大目录
@@ -92,6 +92,8 @@ interface FileListLabels {
92
92
  unlockFolder: string;
93
93
  /** @deprecated Network failures now append `corsHint`. */
94
94
  networkHint: string;
95
+ /** Single file handed to the browser's download manager. `{name}` */
96
+ downloadStarted: string;
95
97
  /** Batch progress detail. `{done}` `{total}` sizes, `{files}` `{count}` file counts */
96
98
  batchProgress: string;
97
99
  /** Batch finished. `{name}` `{count}` `{size}` */
@@ -329,6 +329,7 @@ function createOpenListApi(base, token) {
329
329
  return url;
330
330
  },
331
331
  directUrl,
332
+ isAttachmentUrl: (url) => !!root && (url.startsWith(`${root}/p/`) || url.startsWith(`${root}/d/`)),
332
333
  clearCache(path) {
333
334
  if (!path) urls.clear();
334
335
  else for (const key of [...urls.keys()]) if (isWithin(key, path)) urls.delete(key);
@@ -517,6 +518,7 @@ function createDemoApi(delay = 220) {
517
518
  }),
518
519
  fileUrl: async () => "",
519
520
  directUrl: () => "",
521
+ isAttachmentUrl: () => false,
520
522
  clearCache: () => {}
521
523
  };
522
524
  }
@@ -594,12 +596,21 @@ const _sfc_main$3 = /*@__PURE__*/ defineComponent({
594
596
  }
595
597
  });
596
598
  // src/client/download.ts
597
- /** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
598
- function openUrl(url) {
599
+ /**
600
+ * Hand a file URL to the browser's own download manager.
601
+ *
602
+ * `sameTab` is for links known to answer as attachments (OpenList `/p/`, `/d/`):
603
+ * the browser downloads and the page stays put, with no tab flashing open.
604
+ * Other links (a storage provider's direct URL may display inline) open in a
605
+ * new tab so the document page is never replaced. `download` also keeps a
606
+ * same-origin link out of the VitePress router and names the file.
607
+ */
608
+ function openUrl(url, options) {
599
609
  const anchor = document.createElement("a");
600
610
  anchor.href = url;
601
- anchor.target = "_blank";
611
+ anchor.download = options.name;
602
612
  anchor.rel = "noopener";
613
+ if (!options.sameTab) anchor.target = "_blank";
603
614
  document.body.appendChild(anchor);
604
615
  anchor.click();
605
616
  anchor.remove();
@@ -641,10 +652,14 @@ async function pickZipFile(name) {
641
652
  }
642
653
  }
643
654
  /**
644
- * The ZIP as a stream: files are fetched one after another and passed through
655
+ * The ZIP as a stream: files are fetched a few at a time and passed through
645
656
  * as they arrive, stored without compression (most shared files are already
646
657
  * compressed). Clashing entry names get ` (2)`, ` (3)`...
647
658
  */
659
+ /** Files fetched at once: hides per-file latency, stays under the browser's 6 per host. */
660
+ const CONCURRENCY = 4;
661
+ /** Progress updates at most this often (ms), so the bar does not re-render per chunk. */
662
+ const PROGRESS_INTERVAL = 100;
648
663
  async function zipStream(files, options) {
649
664
  const { makeZip } = await import("client-zip");
650
665
  const progress = {
@@ -653,12 +668,28 @@ async function zipStream(files, options) {
653
668
  files: 0,
654
669
  totalFiles: files.length
655
670
  };
656
- const report = () => options.onProgress({ ...progress });
657
- report();
671
+ let reportedAt = 0;
672
+ const report = (force = false) => {
673
+ const now = Date.now();
674
+ if (!force && now - reportedAt < PROGRESS_INTERVAL) return;
675
+ reportedAt = now;
676
+ options.onProgress({ ...progress });
677
+ };
678
+ report(true);
658
679
  async function* entries() {
659
680
  const used = /* @__PURE__ */ new Set();
681
+ const ahead = [];
682
+ let started = 0;
683
+ const startNext = () => {
684
+ const file = files[started++];
685
+ const response = options.urlOf(file).then((url) => fetch(url, { signal: options.signal }));
686
+ response.catch(() => {});
687
+ ahead.push(response);
688
+ };
689
+ while (started < files.length && ahead.length < CONCURRENCY) startNext();
660
690
  for (const file of files) {
661
- const response = await fetch(await options.urlOf(file), { signal: options.signal });
691
+ const response = await ahead.shift();
692
+ if (started < files.length) startNext();
662
693
  if (!response.ok || !response.body) throw new Error(`${file.name}:HTTP ${response.status}`);
663
694
  const counted = response.body.pipeThrough(new TransformStream({
664
695
  transform(chunk, controller) {
@@ -668,7 +699,7 @@ async function zipStream(files, options) {
668
699
  },
669
700
  flush() {
670
701
  progress.files += 1;
671
- report();
702
+ report(progress.files === progress.totalFiles);
672
703
  }
673
704
  }));
674
705
  const modified = new Date(file.modified);
@@ -868,6 +899,7 @@ const fileListLocales = {
868
899
  forgetPassword: "忘记",
869
900
  unlockFolder: "输入 {name} 的密码",
870
901
  networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
902
+ downloadStarted: "已交给浏览器下载:{name}",
871
903
  batchProgress: "{done} / {total} · {files} / {count} 个文件",
872
904
  batchDone: "已打包 {name}({count} 个文件,{size})",
873
905
  batchCanceled: "已取消下载",
@@ -945,6 +977,7 @@ const fileListLocales = {
945
977
  forgetPassword: "Forget",
946
978
  unlockFolder: "Enter the password for {name}",
947
979
  networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
980
+ downloadStarted: "Downloading {name} in your browser",
948
981
  batchProgress: "{done} / {total} · {files} / {count} files",
949
982
  batchDone: "Saved {name} ({count} files, {size})",
950
983
  batchCanceled: "Download cancelled",
@@ -2244,7 +2277,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2244
2277
  return;
2245
2278
  }
2246
2279
  try {
2247
- openUrl(await fileUrlOf(node));
2280
+ const url = await fileUrlOf(node);
2281
+ openUrl(url, {
2282
+ sameTab: api.value.isAttachmentUrl(url),
2283
+ name: node.name
2284
+ });
2285
+ showToast(t("downloadStarted", { name: node.name }));
2248
2286
  } catch (error) {
2249
2287
  showToast(t("downloadFailed", { message: errorText(error) }));
2250
2288
  }
@@ -92,6 +92,8 @@ interface FileListLabels {
92
92
  unlockFolder: string;
93
93
  /** @deprecated Network failures now append `corsHint`. */
94
94
  networkHint: string;
95
+ /** Single file handed to the browser's download manager. `{name}` */
96
+ downloadStarted: string;
95
97
  /** Batch progress detail. `{done}` `{total}` sizes, `{files}` `{count}` file counts */
96
98
  batchProgress: string;
97
99
  /** Batch finished. `{name}` `{count}` `{size}` */
@@ -329,6 +329,7 @@ function createOpenListApi(base, token) {
329
329
  return url;
330
330
  },
331
331
  directUrl,
332
+ isAttachmentUrl: (url) => !!root && (url.startsWith(`${root}/p/`) || url.startsWith(`${root}/d/`)),
332
333
  clearCache(path) {
333
334
  if (!path) urls.clear();
334
335
  else for (const key of [...urls.keys()]) if (isWithin(key, path)) urls.delete(key);
@@ -517,6 +518,7 @@ function createDemoApi(delay = 220) {
517
518
  }),
518
519
  fileUrl: async () => "",
519
520
  directUrl: () => "",
521
+ isAttachmentUrl: () => false,
520
522
  clearCache: () => {}
521
523
  };
522
524
  }
@@ -575,12 +577,21 @@ _sfc_main$3.setup = (props, ctx) => {
575
577
  return _sfc_setup$3 ? _sfc_setup$3(props, ctx) : void 0;
576
578
  };
577
579
  // src/client/download.ts
578
- /** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
579
- function openUrl(url) {
580
+ /**
581
+ * Hand a file URL to the browser's own download manager.
582
+ *
583
+ * `sameTab` is for links known to answer as attachments (OpenList `/p/`, `/d/`):
584
+ * the browser downloads and the page stays put, with no tab flashing open.
585
+ * Other links (a storage provider's direct URL may display inline) open in a
586
+ * new tab so the document page is never replaced. `download` also keeps a
587
+ * same-origin link out of the VitePress router and names the file.
588
+ */
589
+ function openUrl(url, options) {
580
590
  const anchor = document.createElement("a");
581
591
  anchor.href = url;
582
- anchor.target = "_blank";
592
+ anchor.download = options.name;
583
593
  anchor.rel = "noopener";
594
+ if (!options.sameTab) anchor.target = "_blank";
584
595
  document.body.appendChild(anchor);
585
596
  anchor.click();
586
597
  anchor.remove();
@@ -600,10 +611,14 @@ function zipFileName(title) {
600
611
  return `${title.replace(/[\\/:*?"<>|]/g, "_")}.zip`;
601
612
  }
602
613
  /**
603
- * The ZIP as a stream: files are fetched one after another and passed through
614
+ * The ZIP as a stream: files are fetched a few at a time and passed through
604
615
  * as they arrive, stored without compression (most shared files are already
605
616
  * compressed). Clashing entry names get ` (2)`, ` (3)`...
606
617
  */
618
+ /** Files fetched at once: hides per-file latency, stays under the browser's 6 per host. */
619
+ const CONCURRENCY = 4;
620
+ /** Progress updates at most this often (ms), so the bar does not re-render per chunk. */
621
+ const PROGRESS_INTERVAL = 100;
607
622
  async function zipStream(files, options) {
608
623
  const { makeZip } = await import("client-zip");
609
624
  const progress = {
@@ -612,12 +627,28 @@ async function zipStream(files, options) {
612
627
  files: 0,
613
628
  totalFiles: files.length
614
629
  };
615
- const report = () => options.onProgress({ ...progress });
616
- report();
630
+ let reportedAt = 0;
631
+ const report = (force = false) => {
632
+ const now = Date.now();
633
+ if (!force && now - reportedAt < PROGRESS_INTERVAL) return;
634
+ reportedAt = now;
635
+ options.onProgress({ ...progress });
636
+ };
637
+ report(true);
617
638
  async function* entries() {
618
639
  const used = /* @__PURE__ */ new Set();
640
+ const ahead = [];
641
+ let started = 0;
642
+ const startNext = () => {
643
+ const file = files[started++];
644
+ const response = options.urlOf(file).then((url) => fetch(url, { signal: options.signal }));
645
+ response.catch(() => {});
646
+ ahead.push(response);
647
+ };
648
+ while (started < files.length && ahead.length < CONCURRENCY) startNext();
619
649
  for (const file of files) {
620
- const response = await fetch(await options.urlOf(file), { signal: options.signal });
650
+ const response = await ahead.shift();
651
+ if (started < files.length) startNext();
621
652
  if (!response.ok || !response.body) throw new Error(`${file.name}:HTTP ${response.status}`);
622
653
  const counted = response.body.pipeThrough(new TransformStream({
623
654
  transform(chunk, controller) {
@@ -627,7 +658,7 @@ async function zipStream(files, options) {
627
658
  },
628
659
  flush() {
629
660
  progress.files += 1;
630
- report();
661
+ report(progress.files === progress.totalFiles);
631
662
  }
632
663
  }));
633
664
  const modified = new Date(file.modified);
@@ -821,6 +852,7 @@ const fileListLocales = {
821
852
  forgetPassword: "忘记",
822
853
  unlockFolder: "输入 {name} 的密码",
823
854
  networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
855
+ downloadStarted: "已交给浏览器下载:{name}",
824
856
  batchProgress: "{done} / {total} · {files} / {count} 个文件",
825
857
  batchDone: "已打包 {name}({count} 个文件,{size})",
826
858
  batchCanceled: "已取消下载",
@@ -898,6 +930,7 @@ const fileListLocales = {
898
930
  forgetPassword: "Forget",
899
931
  unlockFolder: "Enter the password for {name}",
900
932
  networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
933
+ downloadStarted: "Downloading {name} in your browser",
901
934
  batchProgress: "{done} / {total} · {files} / {count} files",
902
935
  batchDone: "Saved {name} ({count} files, {size})",
903
936
  batchCanceled: "Download cancelled",
@@ -1707,7 +1740,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1707
1740
  return;
1708
1741
  }
1709
1742
  try {
1710
- openUrl(await fileUrlOf(node));
1743
+ const url = await fileUrlOf(node);
1744
+ openUrl(url, {
1745
+ sameTab: api.value.isAttachmentUrl(url),
1746
+ name: node.name
1747
+ });
1748
+ showToast(t("downloadStarted", { name: node.name }));
1711
1749
  } catch (error) {
1712
1750
  showToast(t("downloadFailed", { message: errorText(error) }));
1713
1751
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitepress-plugin-filelist",
3
- "version": "0.6.0",
3
+ "version": "0.6.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.",