vitepress-plugin-filelist 0.6.0 → 0.6.2
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 +3 -2
- package/dist/client/browser/index.d.ts +5 -0
- package/dist/client/browser/index.js +81 -16
- package/dist/client/ssr/index.d.ts +5 -0
- package/dist/client/ssr/index.js +81 -16
- package/dist/client/style.css +6 -4
- package/package.json +1 -1
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
|
- 🔎 当前树过滤,按名称 / 大小 / 日期排序
|
|
@@ -134,7 +134,8 @@ interface FileListSource {
|
|
|
134
134
|
|
|
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,11 @@ interface FileListLabels {
|
|
|
92
92
|
unlockFolder: string;
|
|
93
93
|
/** @deprecated Network failures now append `corsHint`. */
|
|
94
94
|
networkHint: string;
|
|
95
|
+
/** Title of the dialog when a batch ZIP fails (the reason is the text). */
|
|
96
|
+
batchFailedTitle: string;
|
|
97
|
+
batchFailedHint: string;
|
|
98
|
+
/** Single file handed to the browser's download manager. `{name}` */
|
|
99
|
+
downloadStarted: string;
|
|
95
100
|
/** Batch progress detail. `{done}` `{total}` sizes, `{files}` `{count}` file counts */
|
|
96
101
|
batchProgress: string;
|
|
97
102
|
/** 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
|
-
/**
|
|
598
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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,9 @@ const fileListLocales = {
|
|
|
868
899
|
forgetPassword: "忘记",
|
|
869
900
|
unlockFolder: "输入 {name} 的密码",
|
|
870
901
|
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
902
|
+
batchFailedTitle: "打包失败",
|
|
903
|
+
batchFailedHint: "可以减少勾选的文件后重试。",
|
|
904
|
+
downloadStarted: "已交给浏览器下载:{name}",
|
|
871
905
|
batchProgress: "{done} / {total} · {files} / {count} 个文件",
|
|
872
906
|
batchDone: "已打包 {name}({count} 个文件,{size})",
|
|
873
907
|
batchCanceled: "已取消下载",
|
|
@@ -945,6 +979,9 @@ const fileListLocales = {
|
|
|
945
979
|
forgetPassword: "Forget",
|
|
946
980
|
unlockFolder: "Enter the password for {name}",
|
|
947
981
|
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
982
|
+
batchFailedTitle: "Could not create the ZIP",
|
|
983
|
+
batchFailedHint: "Try again with fewer files selected.",
|
|
984
|
+
downloadStarted: "Downloading {name} in your browser",
|
|
948
985
|
batchProgress: "{done} / {total} · {files} / {count} files",
|
|
949
986
|
batchDone: "Saved {name} ({count} files, {size})",
|
|
950
987
|
batchCanceled: "Download cancelled",
|
|
@@ -2244,7 +2281,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2244
2281
|
return;
|
|
2245
2282
|
}
|
|
2246
2283
|
try {
|
|
2247
|
-
|
|
2284
|
+
const url = await fileUrlOf(node);
|
|
2285
|
+
openUrl(url, {
|
|
2286
|
+
sameTab: api.value.isAttachmentUrl(url),
|
|
2287
|
+
name: node.name
|
|
2288
|
+
});
|
|
2289
|
+
showToast(t("downloadStarted", { name: node.name }));
|
|
2248
2290
|
} catch (error) {
|
|
2249
2291
|
showToast(t("downloadFailed", { message: errorText(error) }));
|
|
2250
2292
|
}
|
|
@@ -2260,9 +2302,28 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2260
2302
|
max: bytes(props.zipMaxSize, touch ? 1024 : 2048)
|
|
2261
2303
|
};
|
|
2262
2304
|
}
|
|
2305
|
+
/** A ZIP question or notice: over the warning size, over the limit, or failed. */
|
|
2263
2306
|
const zipNotice = ref(null);
|
|
2264
2307
|
/** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
|
|
2265
2308
|
const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
|
|
2309
|
+
const zipDialog = computed(() => {
|
|
2310
|
+
const notice = zipNotice.value;
|
|
2311
|
+
if (!notice) return null;
|
|
2312
|
+
if (notice.kind === "failed") return {
|
|
2313
|
+
title: t("batchFailedTitle"),
|
|
2314
|
+
text: notice.message,
|
|
2315
|
+
tip: t("batchFailedHint"),
|
|
2316
|
+
cancel: t("gotIt")
|
|
2317
|
+
};
|
|
2318
|
+
const warn = notice.kind === "warn";
|
|
2319
|
+
return {
|
|
2320
|
+
title: t(warn ? "zipWarnTitle" : "zipBlockTitle", { size: formatSize(notice.size) }),
|
|
2321
|
+
text: t(warn ? "zipWarnText" : "zipBlockText", { limit: formatSize(notice.limit) }),
|
|
2322
|
+
tip: zipTip.value,
|
|
2323
|
+
confirm: warn ? t("zipContinue") : void 0,
|
|
2324
|
+
cancel: warn ? t("cancel") : t("gotIt")
|
|
2325
|
+
};
|
|
2326
|
+
});
|
|
2266
2327
|
function zipName() {
|
|
2267
2328
|
return zipFileName(props.title || t("defaultZip"));
|
|
2268
2329
|
}
|
|
@@ -2331,7 +2392,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2331
2392
|
}));
|
|
2332
2393
|
selected.value = /* @__PURE__ */ new Set();
|
|
2333
2394
|
} catch (error) {
|
|
2334
|
-
|
|
2395
|
+
if (controller.signal.aborted) showToast(t("batchCanceled"));
|
|
2396
|
+
else zipNotice.value = {
|
|
2397
|
+
kind: "failed",
|
|
2398
|
+
message: errorText(error)
|
|
2399
|
+
};
|
|
2335
2400
|
} finally {
|
|
2336
2401
|
zipAbort = null;
|
|
2337
2402
|
batchBusy.value = false;
|
|
@@ -2779,12 +2844,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2779
2844
|
createElementVNode("div", _hoisted_73, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) })), 1)]),
|
|
2780
2845
|
toast.value ? (openBlock(), createElementBlock("div", _hoisted_74, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
|
|
2781
2846
|
createVNode(_sfc_main$3, {
|
|
2782
|
-
open: !!
|
|
2783
|
-
title:
|
|
2784
|
-
text:
|
|
2785
|
-
tip:
|
|
2786
|
-
"confirm-label":
|
|
2787
|
-
"cancel-label":
|
|
2847
|
+
open: !!zipDialog.value,
|
|
2848
|
+
title: zipDialog.value?.title ?? "",
|
|
2849
|
+
text: zipDialog.value?.text ?? "",
|
|
2850
|
+
tip: zipDialog.value?.tip,
|
|
2851
|
+
"confirm-label": zipDialog.value?.confirm,
|
|
2852
|
+
"cancel-label": zipDialog.value?.cancel ?? "",
|
|
2788
2853
|
onConfirm: confirmZip,
|
|
2789
2854
|
onClose: _cache[4] || (_cache[4] = ($event) => zipNotice.value = null)
|
|
2790
2855
|
}, null, 8, [
|
|
@@ -92,6 +92,11 @@ interface FileListLabels {
|
|
|
92
92
|
unlockFolder: string;
|
|
93
93
|
/** @deprecated Network failures now append `corsHint`. */
|
|
94
94
|
networkHint: string;
|
|
95
|
+
/** Title of the dialog when a batch ZIP fails (the reason is the text). */
|
|
96
|
+
batchFailedTitle: string;
|
|
97
|
+
batchFailedHint: string;
|
|
98
|
+
/** Single file handed to the browser's download manager. `{name}` */
|
|
99
|
+
downloadStarted: string;
|
|
95
100
|
/** Batch progress detail. `{done}` `{total}` sizes, `{files}` `{count}` file counts */
|
|
96
101
|
batchProgress: string;
|
|
97
102
|
/** Batch finished. `{name}` `{count}` `{size}` */
|
package/dist/client/ssr/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
579
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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,9 @@ const fileListLocales = {
|
|
|
821
852
|
forgetPassword: "忘记",
|
|
822
853
|
unlockFolder: "输入 {name} 的密码",
|
|
823
854
|
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
855
|
+
batchFailedTitle: "打包失败",
|
|
856
|
+
batchFailedHint: "可以减少勾选的文件后重试。",
|
|
857
|
+
downloadStarted: "已交给浏览器下载:{name}",
|
|
824
858
|
batchProgress: "{done} / {total} · {files} / {count} 个文件",
|
|
825
859
|
batchDone: "已打包 {name}({count} 个文件,{size})",
|
|
826
860
|
batchCanceled: "已取消下载",
|
|
@@ -898,6 +932,9 @@ const fileListLocales = {
|
|
|
898
932
|
forgetPassword: "Forget",
|
|
899
933
|
unlockFolder: "Enter the password for {name}",
|
|
900
934
|
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
935
|
+
batchFailedTitle: "Could not create the ZIP",
|
|
936
|
+
batchFailedHint: "Try again with fewer files selected.",
|
|
937
|
+
downloadStarted: "Downloading {name} in your browser",
|
|
901
938
|
batchProgress: "{done} / {total} · {files} / {count} files",
|
|
902
939
|
batchDone: "Saved {name} ({count} files, {size})",
|
|
903
940
|
batchCanceled: "Download cancelled",
|
|
@@ -1707,14 +1744,38 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1707
1744
|
return;
|
|
1708
1745
|
}
|
|
1709
1746
|
try {
|
|
1710
|
-
|
|
1747
|
+
const url = await fileUrlOf(node);
|
|
1748
|
+
openUrl(url, {
|
|
1749
|
+
sameTab: api.value.isAttachmentUrl(url),
|
|
1750
|
+
name: node.name
|
|
1751
|
+
});
|
|
1752
|
+
showToast(t("downloadStarted", { name: node.name }));
|
|
1711
1753
|
} catch (error) {
|
|
1712
1754
|
showToast(t("downloadFailed", { message: errorText(error) }));
|
|
1713
1755
|
}
|
|
1714
1756
|
}
|
|
1757
|
+
/** A ZIP question or notice: over the warning size, over the limit, or failed. */
|
|
1715
1758
|
const zipNotice = ref(null);
|
|
1716
1759
|
/** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
|
|
1717
1760
|
const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
|
|
1761
|
+
const zipDialog = computed(() => {
|
|
1762
|
+
const notice = zipNotice.value;
|
|
1763
|
+
if (!notice) return null;
|
|
1764
|
+
if (notice.kind === "failed") return {
|
|
1765
|
+
title: t("batchFailedTitle"),
|
|
1766
|
+
text: notice.message,
|
|
1767
|
+
tip: t("batchFailedHint"),
|
|
1768
|
+
cancel: t("gotIt")
|
|
1769
|
+
};
|
|
1770
|
+
const warn = notice.kind === "warn";
|
|
1771
|
+
return {
|
|
1772
|
+
title: t(warn ? "zipWarnTitle" : "zipBlockTitle", { size: formatSize(notice.size) }),
|
|
1773
|
+
text: t(warn ? "zipWarnText" : "zipBlockText", { limit: formatSize(notice.limit) }),
|
|
1774
|
+
tip: zipTip.value,
|
|
1775
|
+
confirm: warn ? t("zipContinue") : void 0,
|
|
1776
|
+
cancel: warn ? t("cancel") : t("gotIt")
|
|
1777
|
+
};
|
|
1778
|
+
});
|
|
1718
1779
|
function zipName() {
|
|
1719
1780
|
return zipFileName(props.title || t("defaultZip"));
|
|
1720
1781
|
}
|
|
@@ -1745,7 +1806,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1745
1806
|
}));
|
|
1746
1807
|
selected.value = /* @__PURE__ */ new Set();
|
|
1747
1808
|
} catch (error) {
|
|
1748
|
-
|
|
1809
|
+
if (controller.signal.aborted) showToast(t("batchCanceled"));
|
|
1810
|
+
else zipNotice.value = {
|
|
1811
|
+
kind: "failed",
|
|
1812
|
+
message: errorText(error)
|
|
1813
|
+
};
|
|
1749
1814
|
} finally {
|
|
1750
1815
|
zipAbort = null;
|
|
1751
1816
|
batchBusy.value = false;
|
|
@@ -2042,12 +2107,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2042
2107
|
if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
|
|
2043
2108
|
else _push(`<!---->`);
|
|
2044
2109
|
_push(ssrRenderComponent(_sfc_main$3, {
|
|
2045
|
-
open: !!
|
|
2046
|
-
title:
|
|
2047
|
-
text:
|
|
2048
|
-
tip:
|
|
2049
|
-
"confirm-label":
|
|
2050
|
-
"cancel-label":
|
|
2110
|
+
open: !!zipDialog.value,
|
|
2111
|
+
title: zipDialog.value?.title ?? "",
|
|
2112
|
+
text: zipDialog.value?.text ?? "",
|
|
2113
|
+
tip: zipDialog.value?.tip,
|
|
2114
|
+
"confirm-label": zipDialog.value?.confirm,
|
|
2115
|
+
"cancel-label": zipDialog.value?.cancel ?? "",
|
|
2051
2116
|
onConfirm: confirmZip,
|
|
2052
2117
|
onClose: ($event) => zipNotice.value = null
|
|
2053
2118
|
}, null, _parent));
|
package/dist/client/style.css
CHANGED
|
@@ -787,11 +787,13 @@
|
|
|
787
787
|
}
|
|
788
788
|
|
|
789
789
|
.fl-toast {
|
|
790
|
-
|
|
791
|
-
|
|
790
|
+
/* the viewport, not the card: a long list would push the card's bottom off screen */
|
|
791
|
+
position: fixed;
|
|
792
|
+
bottom: calc(24px + env(safe-area-inset-bottom));
|
|
792
793
|
left: 50%;
|
|
793
|
-
z-index:
|
|
794
|
-
|
|
794
|
+
z-index: 100;
|
|
795
|
+
box-sizing: border-box;
|
|
796
|
+
max-width: min(560px, calc(100vw - 32px));
|
|
795
797
|
padding: 7px 14px;
|
|
796
798
|
overflow: hidden;
|
|
797
799
|
font-size: 13px;
|
package/package.json
CHANGED