vitepress-plugin-filelist 0.5.0 → 0.6.0

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.
@@ -1,5 +1,809 @@
1
1
  import "../style.css";
2
- import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, useId, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
3
+ // src/client/utils.ts
4
+ function joinPath(parent, name) {
5
+ return (parent.endsWith("/") ? parent : `${parent}/`) + name;
6
+ }
7
+ /** Whether `path` is `folder` itself or inside it (segment-wise, trailing slashes ignored). */
8
+ function isWithin(path, folder) {
9
+ const p = path.replace(/\/+$/, "") || "/";
10
+ const f = folder.replace(/\/+$/, "") || "/";
11
+ return f === "/" || p === f || p.startsWith(`${f}/`);
12
+ }
13
+ function encodePath(path) {
14
+ return path.split("/").map(encodeURIComponent).join("/");
15
+ }
16
+ /**
17
+ * Make one path segment safe as a ZIP entry segment: no separators,
18
+ * no `.` / `..` traversal, no empty names.
19
+ */
20
+ function zipSegment(name) {
21
+ const cleaned = name.replace(/[\\/]/g, "_").trim();
22
+ if (!cleaned || cleaned === "." || cleaned === "..") return "_";
23
+ return cleaned;
24
+ }
25
+ /** Join path segments into a ZIP entry name, sanitising each segment. */
26
+ function zipEntryName(segments) {
27
+ return segments.map(zipSegment).join("/");
28
+ }
29
+ /**
30
+ * Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
31
+ * The suffix goes before the extension of the last segment. Adds the result to `used`.
32
+ */
33
+ function uniqueEntryName(name, used) {
34
+ let candidate = name;
35
+ if (used.has(candidate)) {
36
+ const slash = name.lastIndexOf("/");
37
+ const dir = name.slice(0, slash + 1);
38
+ const base = name.slice(slash + 1);
39
+ const dot = base.lastIndexOf(".");
40
+ const stem = dot > 0 ? base.slice(0, dot) : base;
41
+ const ext = dot > 0 ? base.slice(dot) : "";
42
+ let index = 2;
43
+ do {
44
+ candidate = `${dir}${stem} (${index})${ext}`;
45
+ index += 1;
46
+ } while (used.has(candidate));
47
+ }
48
+ used.add(candidate);
49
+ return candidate;
50
+ }
51
+ /** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
52
+ function formatSize(size) {
53
+ if (!Number.isFinite(size) || size < 0) return "-";
54
+ const units = [
55
+ "B",
56
+ "KB",
57
+ "MB",
58
+ "GB",
59
+ "TB"
60
+ ];
61
+ let index = 0;
62
+ let value = size;
63
+ while (value >= 1024 && index < units.length - 1) {
64
+ value /= 1024;
65
+ index += 1;
66
+ }
67
+ if (index === 0) return `${Math.round(value)} B`;
68
+ return `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
69
+ }
70
+ function formatDate(iso) {
71
+ if (!iso) return "-";
72
+ const date = new Date(iso);
73
+ if (Number.isNaN(date.getTime())) return iso.slice(0, 10);
74
+ const pad = (value) => String(value).padStart(2, "0");
75
+ return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
76
+ }
77
+ /** Lower-case extension (letters and digits only), or `''`. */
78
+ function extension(name) {
79
+ const match = name.match(/\.([a-z0-9]+)$/i);
80
+ return match ? match[1].toLowerCase() : "";
81
+ }
82
+ /** Upper-case extension for the type badge, e.g. `PDF`. */
83
+ function extOf(name) {
84
+ return extension(name).toUpperCase();
85
+ }
86
+ /**
87
+ * One table for every known extension: the badge family (`FileKind`) and how
88
+ * the preview dialog shows it (`PreviewKind`; Office and the like are `none`).
89
+ */
90
+ const EXTENSIONS = [
91
+ [
92
+ "doc",
93
+ "text",
94
+ "txt md markdown"
95
+ ],
96
+ [
97
+ "doc",
98
+ "none",
99
+ "doc docx odt rtf pages wps"
100
+ ],
101
+ [
102
+ "sheet",
103
+ "text",
104
+ "csv tsv"
105
+ ],
106
+ [
107
+ "sheet",
108
+ "none",
109
+ "xls xlsx ods numbers et"
110
+ ],
111
+ [
112
+ "slide",
113
+ "none",
114
+ "ppt pptx odp key dps"
115
+ ],
116
+ [
117
+ "pdf",
118
+ "pdf",
119
+ "pdf"
120
+ ],
121
+ [
122
+ "image",
123
+ "image",
124
+ "png jpg jpeg gif webp svg bmp ico avif"
125
+ ],
126
+ [
127
+ "image",
128
+ "none",
129
+ "heic"
130
+ ],
131
+ [
132
+ "media",
133
+ "video",
134
+ "mp4 webm m4v mov ogv"
135
+ ],
136
+ [
137
+ "media",
138
+ "audio",
139
+ "mp3 wav ogg oga m4a aac flac opus"
140
+ ],
141
+ [
142
+ "media",
143
+ "none",
144
+ "mkv avi"
145
+ ],
146
+ [
147
+ "archive",
148
+ "none",
149
+ "zip rar 7z tar gz tgz bz2 xz iso"
150
+ ],
151
+ [
152
+ "code",
153
+ "text",
154
+ "js mjs cjs ts tsx jsx vue json html htm css scss less py java kt c h cpp hpp cs go rs rb php sh bat ps1 sql yml yaml xml toml ini conf properties"
155
+ ],
156
+ [
157
+ "other",
158
+ "text",
159
+ "log srt vtt ass lrc gitignore env"
160
+ ]
161
+ ];
162
+ const BY_EXTENSION = /* @__PURE__ */ new Map();
163
+ for (const [kind, preview, list] of EXTENSIONS) for (const ext of list.split(" ")) BY_EXTENSION.set(ext, {
164
+ kind,
165
+ preview
166
+ });
167
+ /** Rough file family, used to colour the type badge. */
168
+ function fileKind(name) {
169
+ return BY_EXTENSION.get(extension(name))?.kind ?? "other";
170
+ }
171
+ /**
172
+ * How a file is shown in the preview dialog. Office documents and anything
173
+ * else the browser cannot render are `none` (info + download).
174
+ */
175
+ function previewKind(name) {
176
+ return BY_EXTENSION.get(extension(name))?.preview ?? "none";
177
+ }
178
+ /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
179
+ function totalSize(items) {
180
+ return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
181
+ }
182
+ function compareNodes(a, b, key, ascending = true) {
183
+ if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
184
+ let result = 0;
185
+ if (key === "size") result = a.size - b.size;
186
+ else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
187
+ else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
188
+ return ascending ? result : -result;
189
+ }
190
+ /**
191
+ * Viewers from OpenList's public `iframe_previews` setting that match a file,
192
+ * in the order OpenList lists them. Keys are comma-separated extensions or a
193
+ * `/regex/flags` tested against the extension, as in OpenList's frontend.
194
+ */
195
+ function iframePreviewsFor(name, setting) {
196
+ if (!setting) return [];
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(setting);
200
+ } catch {
201
+ return [];
202
+ }
203
+ if (!parsed || typeof parsed !== "object") return [];
204
+ const ext = extension(name);
205
+ const out = [];
206
+ for (const [key, viewers] of Object.entries(parsed)) {
207
+ let hit = false;
208
+ if (key.startsWith("/")) {
209
+ const end = key.lastIndexOf("/");
210
+ try {
211
+ hit = end > 0 && new RegExp(key.slice(1, end), key.slice(end + 1)).test(ext);
212
+ } catch {
213
+ hit = false;
214
+ }
215
+ } else hit = key.split(",").map((item) => item.trim().toLowerCase()).includes(ext);
216
+ if (!hit || !viewers || typeof viewers !== "object") continue;
217
+ for (const [label, scheme] of Object.entries(viewers)) if (typeof scheme === "string" && /^https?:\/\//i.test(scheme)) out.push({
218
+ label,
219
+ scheme
220
+ });
221
+ }
222
+ return out;
223
+ }
224
+ function applyUrlOps(placeholder, url) {
225
+ const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
226
+ let value = url;
227
+ for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
228
+ return value;
229
+ }
230
+ /**
231
+ * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
232
+ * (signed download URL), each optionally prefixed with `e` (URI-encode) or
233
+ * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
234
+ */
235
+ function fillPreviewUrl(scheme, args) {
236
+ return scheme.replace(/\$name/g, args.name).replace(/\$[eb_]*durl/g, (placeholder) => applyUrlOps(placeholder, args.downloadUrl)).replace(/\$[eb_]*url/g, (placeholder) => applyUrlOps(placeholder, args.rawUrl));
237
+ }
238
+ // src/client/api.ts
239
+ var ApiError = class extends Error {
240
+ code;
241
+ constructor(message, code) {
242
+ super(message);
243
+ this.code = code;
244
+ }
245
+ };
246
+ function errorText(error) {
247
+ return error instanceof Error ? error.message : String(error);
248
+ }
249
+ /**
250
+ * OpenList has no dedicated code for a missing / wrong folder password; its
251
+ * message says "password is incorrect or you have no permission".
252
+ */
253
+ function isPasswordError(error) {
254
+ return /password|密码/i.test(errorText(error));
255
+ }
256
+ /** Browser-level fetch failures (Chrome, Firefox, Safari wording), usually CORS. */
257
+ function isNetworkError(error) {
258
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
259
+ }
260
+ function createOpenListApi(base, token) {
261
+ const root = base.replace(/\/+$/, "");
262
+ const urls = /* @__PURE__ */ new Map();
263
+ async function request(apiPath, body) {
264
+ const response = await fetch(`${root}${apiPath}`, {
265
+ method: body === void 0 ? "GET" : "POST",
266
+ headers: {
267
+ ...body === void 0 ? {} : { "Content-Type": "application/json" },
268
+ ...token ? { Authorization: token } : {}
269
+ },
270
+ body: body === void 0 ? void 0 : JSON.stringify(body)
271
+ });
272
+ const payload = await response.json().catch(() => null);
273
+ if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
274
+ if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
275
+ return payload.data;
276
+ }
277
+ function absoluteUrl(raw) {
278
+ if (!raw) return "";
279
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
280
+ const origin = root || (typeof window !== "undefined" ? window.location.origin : "");
281
+ try {
282
+ return new URL(raw, `${origin}/`).href;
283
+ } catch {
284
+ return raw;
285
+ }
286
+ }
287
+ const directUrl = (file) => `${root}/d${encodePath(file.path)}${file.sign ? `?sign=${encodeURIComponent(file.sign)}` : ""}`;
288
+ return {
289
+ async list(path, password) {
290
+ return (await request("/api/fs/list", {
291
+ path,
292
+ password,
293
+ page: 1,
294
+ per_page: 0,
295
+ refresh: false
296
+ }))?.content ?? [];
297
+ },
298
+ get: (path, password) => request("/api/fs/get", {
299
+ path,
300
+ password
301
+ }),
302
+ async settings() {
303
+ try {
304
+ const data = await request("/api/public/settings");
305
+ return {
306
+ title: data?.site_title || data?.title || "",
307
+ iframePreviews: data?.iframe_previews || ""
308
+ };
309
+ } catch {
310
+ return {
311
+ title: "",
312
+ iframePreviews: ""
313
+ };
314
+ }
315
+ },
316
+ async fileUrl(file, password) {
317
+ if (file.raw_url) return absoluteUrl(file.raw_url);
318
+ const cached = urls.get(file.path);
319
+ if (cached) return cached;
320
+ let url = "";
321
+ try {
322
+ url = absoluteUrl((await request("/api/fs/get", {
323
+ path: file.path,
324
+ password
325
+ }))?.raw_url ?? "");
326
+ } catch {}
327
+ url ||= directUrl(file);
328
+ urls.set(file.path, url);
329
+ return url;
330
+ },
331
+ directUrl,
332
+ clearCache(path) {
333
+ if (!path) urls.clear();
334
+ else for (const key of [...urls.keys()]) if (isWithin(key, path)) urls.delete(key);
335
+ }
336
+ };
337
+ }
338
+ // src/client/demo.ts
339
+ /** Built-in sample data, used when no `base` is given. */
340
+ const TREE = {
341
+ "/": [
342
+ {
343
+ name: "assignments",
344
+ size: 0,
345
+ is_dir: true,
346
+ modified: "2026-09-01T08:00:00+08:00"
347
+ },
348
+ {
349
+ name: "exams",
350
+ size: 0,
351
+ is_dir: true,
352
+ modified: "2026-09-01T08:00:00+08:00"
353
+ },
354
+ {
355
+ name: "materials",
356
+ size: 0,
357
+ is_dir: true,
358
+ modified: "2026-09-01T08:00:00+08:00"
359
+ },
360
+ {
361
+ name: "notes",
362
+ size: 0,
363
+ is_dir: true,
364
+ modified: "2026-09-01T08:00:00+08:00"
365
+ },
366
+ {
367
+ name: "slides",
368
+ size: 0,
369
+ is_dir: true,
370
+ modified: "2026-09-01T08:00:00+08:00"
371
+ },
372
+ {
373
+ name: "加密示例(密码 1234)",
374
+ size: 0,
375
+ is_dir: true,
376
+ modified: "2026-10-20T18:00:00+08:00"
377
+ }
378
+ ],
379
+ "/assignments": [{
380
+ name: "线代作业参考答案",
381
+ size: 0,
382
+ is_dir: true,
383
+ modified: "2026-10-12T21:30:00+08:00"
384
+ }, {
385
+ name: "作业提交说明.pdf",
386
+ size: 186432,
387
+ is_dir: false,
388
+ modified: "2026-09-03T10:15:00+08:00"
389
+ }],
390
+ "/assignments/线代作业参考答案": [
391
+ {
392
+ name: "第1次作业参考答案.doc",
393
+ size: 412672,
394
+ is_dir: false,
395
+ modified: "2026-09-15T22:41:00+08:00"
396
+ },
397
+ {
398
+ name: "第2次作业参考答案.pdf",
399
+ size: 1048576,
400
+ is_dir: false,
401
+ modified: "2026-09-22T22:03:00+08:00"
402
+ },
403
+ {
404
+ name: "第3次作业参考答案.pdf",
405
+ size: 986112,
406
+ is_dir: false,
407
+ modified: "2026-09-29T21:57:00+08:00"
408
+ },
409
+ {
410
+ name: "第4次作业参考答案.pdf",
411
+ size: 1572864,
412
+ is_dir: false,
413
+ modified: "2026-10-13T22:10:00+08:00"
414
+ }
415
+ ],
416
+ "/exams": [
417
+ {
418
+ name: "2023 期末试题和答案.pdf",
419
+ size: 2345678,
420
+ is_dir: false,
421
+ modified: "2025-01-18T09:20:00+08:00"
422
+ },
423
+ {
424
+ name: "2024 期中试题.pdf",
425
+ size: 1234567,
426
+ is_dir: false,
427
+ modified: "2025-11-02T14:08:00+08:00"
428
+ },
429
+ {
430
+ name: "代数与几何-多年合集-试题和答案.pdf",
431
+ size: 18874368,
432
+ is_dir: false,
433
+ modified: "2025-01-18T09:25:00+08:00"
434
+ }
435
+ ],
436
+ "/materials": [
437
+ {
438
+ name: "线性代数及其应用(Lay 第5版).pdf",
439
+ size: 48231424,
440
+ is_dir: false,
441
+ modified: "2024-07-29T16:44:00+08:00"
442
+ },
443
+ {
444
+ name: "Introduction to Linear Algebra(Strang).pdf",
445
+ size: 36700160,
446
+ is_dir: false,
447
+ modified: "2024-07-29T16:50:00+08:00"
448
+ },
449
+ {
450
+ name: "疑难解答与习题指导.zip",
451
+ size: 9437184,
452
+ is_dir: false,
453
+ modified: "2024-08-05T11:12:00+08:00"
454
+ }
455
+ ],
456
+ "/notes": [{
457
+ name: "线性代数笔记(106页).pdf",
458
+ size: 12582912,
459
+ is_dir: false,
460
+ modified: "2025-06-30T20:31:00+08:00"
461
+ }, {
462
+ name: "期末复习提纲.docx",
463
+ size: 245760,
464
+ is_dir: false,
465
+ modified: "2026-01-05T19:02:00+08:00"
466
+ }],
467
+ "/加密示例(密码 1234)": [{
468
+ name: "期末答案(教师版).pdf",
469
+ size: 2621440,
470
+ is_dir: false,
471
+ modified: "2026-10-20T18:00:00+08:00"
472
+ }, {
473
+ name: "成绩登记表.xlsx",
474
+ size: 48128,
475
+ is_dir: false,
476
+ modified: "2026-10-21T09:12:00+08:00"
477
+ }],
478
+ "/slides": [{
479
+ name: "第一章 行列式.pptx",
480
+ size: 3145728,
481
+ is_dir: false,
482
+ modified: "2026-09-08T13:26:00+08:00"
483
+ }, {
484
+ name: "第二章 矩阵.pptx",
485
+ size: 4194304,
486
+ is_dir: false,
487
+ modified: "2026-09-15T13:30:00+08:00"
488
+ }]
489
+ };
490
+ /** Demo-only folder passwords, so the demo can show the password flow. */
491
+ const PASSWORDS = { "/加密示例(密码 1234)": "1234" };
492
+ function guard(path, password) {
493
+ const lock = Object.entries(PASSWORDS).find(([folder]) => isWithin(path, folder));
494
+ if (lock && lock[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
495
+ }
496
+ function parentOf(path) {
497
+ return path.slice(0, path.lastIndexOf("/")) || "/";
498
+ }
499
+ /** Demo OpenList: same API, no network; files have no URL. */
500
+ function createDemoApi(delay = 220) {
501
+ return {
502
+ async list(path, password) {
503
+ await new Promise((resolve) => setTimeout(resolve, delay));
504
+ guard(path, password);
505
+ return TREE[path] ?? [];
506
+ },
507
+ async get(path, password) {
508
+ guard(path, password);
509
+ const name = path.split("/").filter(Boolean).pop() ?? "";
510
+ const item = (TREE[parentOf(path)] ?? []).find((entry) => entry.name === name);
511
+ if (!item) throw new ApiError("object not found", 404);
512
+ return item;
513
+ },
514
+ settings: async () => ({
515
+ title: "",
516
+ iframePreviews: ""
517
+ }),
518
+ fileUrl: async () => "",
519
+ directUrl: () => "",
520
+ clearCache: () => {}
521
+ };
522
+ }
523
+ // src/client/ConfirmDialog.vue
524
+ const _hoisted_1$3 = ["aria-label"];
525
+ const _hoisted_2$3 = {
526
+ key: 0,
527
+ class: "fl-confirm-panel"
528
+ };
529
+ const _hoisted_3$2 = { class: "fl-confirm-title" };
530
+ const _hoisted_4$2 = { class: "fl-confirm-text" };
531
+ const _hoisted_5$2 = {
532
+ key: 0,
533
+ class: "fl-confirm-text"
534
+ };
535
+ const _hoisted_6$2 = { class: "fl-confirm-actions" };
536
+ const _sfc_main$3 = /*@__PURE__*/ defineComponent({
537
+ __name: "ConfirmDialog",
538
+ props: {
539
+ open: { type: Boolean },
540
+ title: {},
541
+ text: {},
542
+ tip: {},
543
+ confirmLabel: {},
544
+ cancelLabel: {}
545
+ },
546
+ emits: ["confirm", "close"],
547
+ setup(__props, { emit: __emit }) {
548
+ /** Small native <dialog> for a yes / no question or a notice (bottom sheet on phones). */
549
+ const props = __props;
550
+ const emit = __emit;
551
+ const dialog = ref();
552
+ watch(() => props.open, (open) => {
553
+ const el = dialog.value;
554
+ if (!el) return;
555
+ if (open && !el.open) {
556
+ if (typeof el.showModal === "function") el.showModal();
557
+ else el.setAttribute("open", "");
558
+ } else if (!open && el.open) {
559
+ if (typeof el.close === "function") el.close();
560
+ else el.removeAttribute("open");
561
+ }
562
+ }, { flush: "post" });
563
+ function onNativeClose() {
564
+ if (props.open) emit("close");
565
+ }
566
+ function onBackdrop(event) {
567
+ if (event.target === dialog.value) emit("close");
568
+ }
569
+ return (_ctx, _cache) => {
570
+ return openBlock(), createElementBlock("dialog", {
571
+ ref_key: "dialog",
572
+ ref: dialog,
573
+ class: "fl-confirm",
574
+ "aria-label": __props.title,
575
+ onClose: onNativeClose,
576
+ onClick: onBackdrop
577
+ }, [__props.open ? (openBlock(), createElementBlock("div", _hoisted_2$3, [
578
+ createElementVNode("p", _hoisted_3$2, toDisplayString(__props.title), 1),
579
+ createElementVNode("p", _hoisted_4$2, toDisplayString(__props.text), 1),
580
+ __props.tip ? (openBlock(), createElementBlock("p", _hoisted_5$2, toDisplayString(__props.tip), 1)) : createCommentVNode("v-if", true),
581
+ createElementVNode("div", _hoisted_6$2, [__props.confirmLabel ? (openBlock(), createElementBlock("button", {
582
+ key: 0,
583
+ class: "fl-btn",
584
+ type: "button",
585
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
586
+ }, toDisplayString(__props.cancelLabel), 1)) : createCommentVNode("v-if", true), createElementVNode("button", {
587
+ class: "fl-btn fl-btn-primary",
588
+ type: "button",
589
+ autofocus: "",
590
+ onClick: _cache[1] || (_cache[1] = ($event) => __props.confirmLabel ? emit("confirm") : emit("close"))
591
+ }, toDisplayString(__props.confirmLabel || __props.cancelLabel), 1)])
592
+ ])) : createCommentVNode("v-if", true)], 40, _hoisted_1$3);
593
+ };
594
+ }
595
+ });
596
+ // src/client/download.ts
597
+ /** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
598
+ function openUrl(url) {
599
+ const anchor = document.createElement("a");
600
+ anchor.href = url;
601
+ anchor.target = "_blank";
602
+ anchor.rel = "noopener";
603
+ document.body.appendChild(anchor);
604
+ anchor.click();
605
+ anchor.remove();
606
+ }
607
+ function saveBlob(blob, name) {
608
+ const href = URL.createObjectURL(blob);
609
+ const anchor = document.createElement("a");
610
+ anchor.href = href;
611
+ anchor.download = name;
612
+ document.body.appendChild(anchor);
613
+ anchor.click();
614
+ anchor.remove();
615
+ setTimeout(() => URL.revokeObjectURL(href), 3e4);
616
+ }
617
+ /** File-system-safe ZIP name from a title. */
618
+ function zipFileName(title) {
619
+ return `${title.replace(/[\\/:*?"<>|]/g, "_")}.zip`;
620
+ }
621
+ /** Whether the browser can write a ZIP straight into a file the user picks (Chromium). */
622
+ function canStreamToDisk() {
623
+ return typeof window !== "undefined" && window.isSecureContext && "showSaveFilePicker" in window;
624
+ }
625
+ /**
626
+ * Ask where to save the ZIP. Call it straight from the click, before any other
627
+ * `await`, or the browser refuses. Resolves `null` when the user cancels.
628
+ */
629
+ async function pickZipFile(name) {
630
+ try {
631
+ return await window.showSaveFilePicker({
632
+ suggestedName: name,
633
+ types: [{
634
+ description: "ZIP",
635
+ accept: { "application/zip": [".zip"] }
636
+ }]
637
+ });
638
+ } catch (error) {
639
+ if (error instanceof DOMException && error.name === "AbortError") return null;
640
+ throw error;
641
+ }
642
+ }
643
+ /**
644
+ * The ZIP as a stream: files are fetched one after another and passed through
645
+ * as they arrive, stored without compression (most shared files are already
646
+ * compressed). Clashing entry names get ` (2)`, ` (3)`...
647
+ */
648
+ async function zipStream(files, options) {
649
+ const { makeZip } = await import("client-zip");
650
+ const progress = {
651
+ bytes: 0,
652
+ totalBytes: files.reduce((sum, file) => sum + (Number.isFinite(file.size) ? file.size : 0), 0),
653
+ files: 0,
654
+ totalFiles: files.length
655
+ };
656
+ const report = () => options.onProgress({ ...progress });
657
+ report();
658
+ async function* entries() {
659
+ const used = /* @__PURE__ */ new Set();
660
+ for (const file of files) {
661
+ const response = await fetch(await options.urlOf(file), { signal: options.signal });
662
+ if (!response.ok || !response.body) throw new Error(`${file.name}:HTTP ${response.status}`);
663
+ const counted = response.body.pipeThrough(new TransformStream({
664
+ transform(chunk, controller) {
665
+ progress.bytes += chunk.byteLength;
666
+ report();
667
+ controller.enqueue(chunk);
668
+ },
669
+ flush() {
670
+ progress.files += 1;
671
+ report();
672
+ }
673
+ }));
674
+ const modified = new Date(file.modified);
675
+ yield {
676
+ name: uniqueEntryName(zipEntryName(file.zipPath), used),
677
+ lastModified: Number.isNaN(modified.getTime()) ? void 0 : modified,
678
+ input: counted
679
+ };
680
+ }
681
+ }
682
+ return makeZip(entries());
683
+ }
684
+ /** Stream the ZIP into a picked file; memory use stays small whatever the size. */
685
+ async function zipToFile(handle, files, options) {
686
+ const stream = await zipStream(files, options);
687
+ const writable = await handle.createWritable();
688
+ await stream.pipeTo(writable, { signal: options.signal });
689
+ }
690
+ /** Build the ZIP in memory (browsers without file streaming). */
691
+ async function zipToBlob(files, options) {
692
+ const reader = (await zipStream(files, options)).getReader();
693
+ const chunks = [];
694
+ const abort = () => void reader.cancel(options.signal.reason).catch(() => {});
695
+ options.signal.addEventListener("abort", abort, { once: true });
696
+ if (options.signal.aborted) abort();
697
+ try {
698
+ for (;;) {
699
+ const { done, value } = await reader.read();
700
+ if (done) break;
701
+ chunks.push(value);
702
+ }
703
+ } finally {
704
+ options.signal.removeEventListener("abort", abort);
705
+ }
706
+ options.signal.throwIfAborted();
707
+ return new Blob(chunks, { type: "application/zip" });
708
+ }
709
+ // src/client/icons.ts
710
+ /** Stroke icons on a 24×24 grid, drawn by `Icon.vue`. */
711
+ const ICONS = {
712
+ lock: {
713
+ stroke: 2.2,
714
+ paths: ["M7 11h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2z", "M8 11V8a4 4 0 0 1 8 0v3"]
715
+ },
716
+ key: {
717
+ stroke: 2,
718
+ paths: [
719
+ "M4 15a4 4 0 1 0 8 0a4 4 0 1 0-8 0",
720
+ "m11 12 9-9",
721
+ "m17 6 3 3"
722
+ ]
723
+ },
724
+ close: {
725
+ stroke: 2.2,
726
+ paths: ["M6 6l12 12", "M18 6 6 18"]
727
+ },
728
+ download: {
729
+ stroke: 2.2,
730
+ paths: [
731
+ "M12 3v12",
732
+ "m7 10 5 5 5-5",
733
+ "M5 21h14"
734
+ ]
735
+ },
736
+ eye: {
737
+ stroke: 2,
738
+ paths: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0"]
739
+ },
740
+ refresh: {
741
+ stroke: 2,
742
+ paths: ["M21 12a9 9 0 1 1-3-6.7", "M21 4v5h-5"]
743
+ },
744
+ search: {
745
+ stroke: 2,
746
+ paths: ["M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0", "m20 20-3.5-3.5"]
747
+ },
748
+ next: {
749
+ stroke: 2.2,
750
+ paths: ["m9 6 6 6-6 6"]
751
+ },
752
+ previous: {
753
+ stroke: 2.2,
754
+ paths: ["m15 6-6 6 6 6"]
755
+ },
756
+ zoomIn: {
757
+ stroke: 2,
758
+ paths: [
759
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
760
+ "m20 20-3.5-3.5",
761
+ "M11 8v6M8 11h6"
762
+ ]
763
+ },
764
+ zoomOut: {
765
+ stroke: 2,
766
+ paths: [
767
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
768
+ "m20 20-3.5-3.5",
769
+ "M8 11h6"
770
+ ]
771
+ }
772
+ };
773
+ // src/client/Icon.vue
774
+ const _hoisted_1$2 = [
775
+ "width",
776
+ "height",
777
+ "stroke-width"
778
+ ];
779
+ const _hoisted_2$2 = ["d"];
780
+ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
781
+ __name: "Icon",
782
+ props: {
783
+ name: {},
784
+ size: { default: 16 }
785
+ },
786
+ setup(__props) {
787
+ return (_ctx, _cache) => {
788
+ return openBlock(), createElementBlock("svg", {
789
+ viewBox: "0 0 24 24",
790
+ width: __props.size,
791
+ height: __props.size,
792
+ fill: "none",
793
+ stroke: "currentColor",
794
+ "stroke-width": unref(ICONS)[__props.name].stroke,
795
+ "stroke-linecap": "round",
796
+ "stroke-linejoin": "round",
797
+ "aria-hidden": "true"
798
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(ICONS)[__props.name].paths, (d) => {
799
+ return openBlock(), createElementBlock("path", {
800
+ key: d,
801
+ d
802
+ }, null, 8, _hoisted_2$2);
803
+ }), 128))], 8, _hoisted_1$2);
804
+ };
805
+ }
806
+ });
3
807
  // src/client/locales.ts
4
808
  const fileListLocales = {
5
809
  "zh-CN": {
@@ -63,7 +867,21 @@ const fileListLocales = {
63
867
  remembered: "已记住",
64
868
  forgetPassword: "忘记",
65
869
  unlockFolder: "输入 {name} 的密码",
66
- networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
870
+ networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
871
+ batchProgress: "{done} / {total} · {files} / {count} 个文件",
872
+ batchDone: "已打包 {name}({count} 个文件,{size})",
873
+ batchCanceled: "已取消下载",
874
+ zipWarnTitle: "文件较大({size})",
875
+ zipWarnText: "当前浏览器要先把文件全部放进内存再打包,超过 {limit} 可能失败或让页面崩溃。建议分批下载。",
876
+ zipStreamTip: "在 Chrome 或 Edge 中可以边下载边保存,不受大小限制。",
877
+ zipContinue: "仍然下载",
878
+ zipBlockTitle: "无法打包 {size}",
879
+ zipBlockText: "当前浏览器一次最多打包 {limit},请分批勾选下载。",
880
+ gotIt: "知道了",
881
+ moreRemaining: "还有 {count} 项",
882
+ showMore: "显示更多",
883
+ showAll: "全部显示",
884
+ showRest: "显示剩余 {count} 项"
67
885
  },
68
886
  "en-US": {
69
887
  allFiles: "All files",
@@ -101,354 +919,56 @@ const fileListLocales = {
101
919
  clear: "Clear",
102
920
  clearSelection: "Clear selection",
103
921
  totalSize: "{size} total",
104
- folderItems: "{count} items",
105
- sortBy: "Sort",
106
- selectAll: "Select all",
107
- selectItem: "Select {name}",
108
- expand: "Expand {name}",
109
- collapse: "Collapse {name}",
110
- close: "Close",
111
- previous: "Previous",
112
- next: "Next",
113
- rotate: "Rotate",
114
- actualSize: "Actual size",
115
- previewUnsupported: "This file type cannot be previewed here. Download it to view.",
116
- previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
117
- previewTooLarge: "This file is large ({size}). Download it to view.",
118
- textTruncated: "Showing the first {size} only. Download for the full file.",
119
- refreshing: "Refreshing",
120
- refreshFailed: "Refresh failed: {message}",
121
- cancel: "Cancel",
122
- passwordFor: "Password for {name}",
123
- passwordWrong: "Wrong password or no permission",
124
- passwordStale: "The saved password no longer works. Enter it again.",
125
- rememberPassword: "Remember in this tab (cleared when the tab closes)",
126
- remembered: "Remembered",
127
- forgetPassword: "Forget",
128
- unlockFolder: "Enter the password for {name}",
129
- networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
130
- }
131
- };
132
- function formatLabel(template, values = {}) {
133
- return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
134
- }
135
- function resolveLabels(locale = "zh-CN", overrides) {
136
- return {
137
- ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
138
- ...overrides
139
- };
140
- }
141
- // src/client/utils.ts
142
- function joinPath(parent, name) {
143
- return (parent.endsWith("/") ? parent : `${parent}/`) + name;
144
- }
145
- function encodePath(path) {
146
- return path.split("/").map(encodeURIComponent).join("/");
147
- }
148
- /**
149
- * Make one path segment safe as a ZIP entry segment: no separators,
150
- * no `.` / `..` traversal, no empty names.
151
- */
152
- function zipSegment(name) {
153
- const cleaned = name.replace(/[\\/]/g, "_").trim();
154
- if (!cleaned || cleaned === "." || cleaned === "..") return "_";
155
- return cleaned;
156
- }
157
- /** Join path segments into a ZIP entry name, sanitising each segment. */
158
- function zipEntryName(segments) {
159
- return segments.map(zipSegment).join("/");
160
- }
161
- /**
162
- * Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
163
- * The suffix goes before the extension of the last segment. Adds the result to `used`.
164
- */
165
- function uniqueEntryName(name, used) {
166
- let candidate = name;
167
- if (used.has(candidate)) {
168
- const slash = name.lastIndexOf("/");
169
- const dir = name.slice(0, slash + 1);
170
- const base = name.slice(slash + 1);
171
- const dot = base.lastIndexOf(".");
172
- const stem = dot > 0 ? base.slice(0, dot) : base;
173
- const ext = dot > 0 ? base.slice(dot) : "";
174
- let index = 2;
175
- do {
176
- candidate = `${dir}${stem} (${index})${ext}`;
177
- index += 1;
178
- } while (used.has(candidate));
179
- }
180
- used.add(candidate);
181
- return candidate;
182
- }
183
- function formatSize(size) {
184
- if (!size || !Number.isFinite(size)) return "-";
185
- const units = [
186
- "B",
187
- "KB",
188
- "MB",
189
- "GB",
190
- "TB"
191
- ];
192
- let index = 0;
193
- let value = size;
194
- while (value >= 1024 && index < units.length - 1) {
195
- value /= 1024;
196
- index += 1;
922
+ folderItems: "{count} items",
923
+ sortBy: "Sort",
924
+ selectAll: "Select all",
925
+ selectItem: "Select {name}",
926
+ expand: "Expand {name}",
927
+ collapse: "Collapse {name}",
928
+ close: "Close",
929
+ previous: "Previous",
930
+ next: "Next",
931
+ rotate: "Rotate",
932
+ actualSize: "Actual size",
933
+ previewUnsupported: "This file type cannot be previewed here. Download it to view.",
934
+ previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
935
+ previewTooLarge: "This file is large ({size}). Download it to view.",
936
+ textTruncated: "Showing the first {size} only. Download for the full file.",
937
+ refreshing: "Refreshing",
938
+ refreshFailed: "Refresh failed: {message}",
939
+ cancel: "Cancel",
940
+ passwordFor: "Password for {name}",
941
+ passwordWrong: "Wrong password or no permission",
942
+ passwordStale: "The saved password no longer works. Enter it again.",
943
+ rememberPassword: "Remember in this tab (cleared when the tab closes)",
944
+ remembered: "Remembered",
945
+ forgetPassword: "Forget",
946
+ unlockFolder: "Enter the password for {name}",
947
+ networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
948
+ batchProgress: "{done} / {total} · {files} / {count} files",
949
+ batchDone: "Saved {name} ({count} files, {size})",
950
+ batchCanceled: "Download cancelled",
951
+ zipWarnTitle: "Large download ({size})",
952
+ zipWarnText: "This browser has to hold every file in memory to build the ZIP. Above {limit} it may fail or crash the page. Consider downloading in smaller batches.",
953
+ zipStreamTip: "Chrome and Edge save while downloading, with no size limit.",
954
+ zipContinue: "Download anyway",
955
+ zipBlockTitle: "Cannot zip {size}",
956
+ zipBlockText: "This browser can zip at most {limit} at once. Select fewer files.",
957
+ gotIt: "OK",
958
+ moreRemaining: "{count} more",
959
+ showMore: "Show more",
960
+ showAll: "Show all",
961
+ showRest: "Show the remaining {count}"
197
962
  }
198
- if (index === 0) return `${Math.round(value)} B`;
199
- return `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
200
- }
201
- function formatDate(iso) {
202
- if (!iso) return "-";
203
- const date = new Date(iso);
204
- if (Number.isNaN(date.getTime())) return iso.slice(0, 10);
205
- const pad = (value) => String(value).padStart(2, "0");
206
- return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
207
- }
208
- function extOf(name) {
209
- const match = name.match(/\.([a-z0-9]+)$/i);
210
- return match ? match[1].toUpperCase() : "";
211
- }
212
- const FILE_KINDS = {};
213
- for (const [kind, exts] of Object.entries({
214
- doc: [
215
- "doc",
216
- "docx",
217
- "odt",
218
- "rtf",
219
- "txt",
220
- "md",
221
- "pages",
222
- "wps"
223
- ],
224
- sheet: [
225
- "xls",
226
- "xlsx",
227
- "ods",
228
- "csv",
229
- "tsv",
230
- "numbers",
231
- "et"
232
- ],
233
- slide: [
234
- "ppt",
235
- "pptx",
236
- "odp",
237
- "key",
238
- "dps"
239
- ],
240
- pdf: ["pdf"],
241
- image: [
242
- "png",
243
- "jpg",
244
- "jpeg",
245
- "gif",
246
- "webp",
247
- "svg",
248
- "bmp",
249
- "ico",
250
- "avif",
251
- "heic"
252
- ],
253
- media: [
254
- "mp3",
255
- "wav",
256
- "flac",
257
- "aac",
258
- "ogg",
259
- "m4a",
260
- "mp4",
261
- "webm",
262
- "mov",
263
- "mkv",
264
- "avi"
265
- ],
266
- archive: [
267
- "zip",
268
- "rar",
269
- "7z",
270
- "tar",
271
- "gz",
272
- "tgz",
273
- "bz2",
274
- "xz",
275
- "iso"
276
- ],
277
- code: [
278
- "js",
279
- "ts",
280
- "json",
281
- "html",
282
- "css",
283
- "py",
284
- "java",
285
- "c",
286
- "cpp",
287
- "go",
288
- "rs",
289
- "sh",
290
- "yml",
291
- "yaml",
292
- "xml"
293
- ]
294
- })) for (const ext of exts) FILE_KINDS[ext] = kind;
295
- /** Rough file family, used to colour the type badge. */
296
- function fileKind(name) {
297
- return FILE_KINDS[extOf(name).toLowerCase()] ?? "other";
298
- }
299
- /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
300
- function totalSize(items) {
301
- return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
302
- }
303
- function compareNodes(a, b, key, ascending = true) {
304
- if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
305
- let result = 0;
306
- if (key === "size") result = a.size - b.size;
307
- else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
308
- else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
309
- return ascending ? result : -result;
310
- }
311
- const PREVIEW_EXTS = {
312
- image: [
313
- "png",
314
- "jpg",
315
- "jpeg",
316
- "gif",
317
- "webp",
318
- "svg",
319
- "bmp",
320
- "ico",
321
- "avif"
322
- ],
323
- video: [
324
- "mp4",
325
- "webm",
326
- "m4v",
327
- "mov",
328
- "ogv"
329
- ],
330
- audio: [
331
- "mp3",
332
- "wav",
333
- "ogg",
334
- "oga",
335
- "m4a",
336
- "aac",
337
- "flac",
338
- "opus"
339
- ],
340
- text: [
341
- "txt",
342
- "md",
343
- "markdown",
344
- "log",
345
- "json",
346
- "csv",
347
- "tsv",
348
- "xml",
349
- "yml",
350
- "yaml",
351
- "toml",
352
- "ini",
353
- "conf",
354
- "properties",
355
- "js",
356
- "mjs",
357
- "cjs",
358
- "ts",
359
- "tsx",
360
- "jsx",
361
- "vue",
362
- "css",
363
- "scss",
364
- "less",
365
- "html",
366
- "htm",
367
- "py",
368
- "java",
369
- "kt",
370
- "c",
371
- "h",
372
- "cpp",
373
- "hpp",
374
- "cs",
375
- "go",
376
- "rs",
377
- "rb",
378
- "php",
379
- "sh",
380
- "bat",
381
- "ps1",
382
- "sql",
383
- "srt",
384
- "vtt",
385
- "ass",
386
- "lrc",
387
- "gitignore",
388
- "env"
389
- ],
390
- pdf: ["pdf"]
391
963
  };
392
- const PREVIEW_KINDS = {};
393
- for (const [kind, exts] of Object.entries(PREVIEW_EXTS)) for (const ext of exts) PREVIEW_KINDS[ext] = kind;
394
- function lowerExt(name) {
395
- const dot = name.lastIndexOf(".");
396
- return dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
397
- }
398
- /**
399
- * How a file is shown in the preview dialog. Office documents and anything
400
- * else the browser cannot render are `none` (info + download).
401
- */
402
- function previewKind(name) {
403
- return PREVIEW_KINDS[lowerExt(name)] ?? "none";
404
- }
405
- /**
406
- * Viewers from OpenList's public `iframe_previews` setting that match a file,
407
- * in the order OpenList lists them. Keys are comma-separated extensions or a
408
- * `/regex/flags` tested against the extension, as in OpenList's frontend.
409
- */
410
- function iframePreviewsFor(name, setting) {
411
- if (!setting) return [];
412
- let parsed;
413
- try {
414
- parsed = JSON.parse(setting);
415
- } catch {
416
- return [];
417
- }
418
- if (!parsed || typeof parsed !== "object") return [];
419
- const ext = lowerExt(name);
420
- const out = [];
421
- for (const [key, viewers] of Object.entries(parsed)) {
422
- let hit = false;
423
- if (key.startsWith("/")) {
424
- const end = key.lastIndexOf("/");
425
- try {
426
- hit = end > 0 && new RegExp(key.slice(1, end), key.slice(end + 1)).test(ext);
427
- } catch {
428
- hit = false;
429
- }
430
- } else hit = key.split(",").map((item) => item.trim().toLowerCase()).includes(ext);
431
- if (!hit || !viewers || typeof viewers !== "object") continue;
432
- for (const [label, scheme] of Object.entries(viewers)) if (typeof scheme === "string" && /^https?:\/\//i.test(scheme)) out.push({
433
- label,
434
- scheme
435
- });
436
- }
437
- return out;
438
- }
439
- function applyUrlOps(placeholder, url) {
440
- const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
441
- let value = url;
442
- for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
443
- return value;
964
+ function formatLabel(template, values = {}) {
965
+ return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
444
966
  }
445
- /**
446
- * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
447
- * (signed download URL), each optionally prefixed with `e` (URI-encode) or
448
- * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
449
- */
450
- function fillPreviewUrl(scheme, args) {
451
- return scheme.replace(/\$name/g, args.name).replace(/\$[eb_]*durl/g, (placeholder) => applyUrlOps(placeholder, args.downloadUrl)).replace(/\$[eb_]*url/g, (placeholder) => applyUrlOps(placeholder, args.rawUrl));
967
+ function resolveLabels(locale = "zh-CN", overrides) {
968
+ return {
969
+ ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
970
+ ...overrides
971
+ };
452
972
  }
453
973
  // src/client/PreviewDialog.vue
454
974
  const _hoisted_1$1 = ["aria-label"];
@@ -474,51 +994,33 @@ const _hoisted_13$1 = [
474
994
  "aria-label",
475
995
  "title"
476
996
  ];
477
- const _hoisted_14$1 = {
478
- viewBox: "0 0 24 24",
479
- width: "17",
480
- height: "17",
481
- fill: "none",
482
- stroke: "currentColor",
483
- "stroke-width": "2",
484
- "stroke-linecap": "round",
485
- "aria-hidden": "true"
486
- };
487
- const _hoisted_15$1 = {
488
- key: 0,
489
- d: "M11 8v6M8 11h6"
490
- };
491
- const _hoisted_16$1 = {
492
- key: 1,
493
- d: "M8 11h6"
494
- };
495
- const _hoisted_17$1 = { class: "fl-preview-tools fl-preview-actions" };
496
- const _hoisted_18$1 = ["aria-label", "title"];
497
- const _hoisted_19$1 = ["aria-label", "title"];
498
- const _hoisted_20$1 = ["data-mode"];
499
- const _hoisted_21$1 = {
997
+ const _hoisted_14$1 = { class: "fl-preview-tools fl-preview-actions" };
998
+ const _hoisted_15$1 = ["aria-label", "title"];
999
+ const _hoisted_16$1 = ["aria-label", "title"];
1000
+ const _hoisted_17$1 = ["data-mode"];
1001
+ const _hoisted_18$1 = {
500
1002
  key: 0,
501
1003
  class: "fl-preview-status",
502
1004
  role: "status"
503
1005
  };
504
- const _hoisted_22$1 = ["src", "alt"];
505
- const _hoisted_23$1 = ["src"];
506
- const _hoisted_24$1 = ["src"];
507
- const _hoisted_25$1 = {
1006
+ const _hoisted_19$1 = ["src", "alt"];
1007
+ const _hoisted_20$1 = ["src"];
1008
+ const _hoisted_21$1 = ["src"];
1009
+ const _hoisted_22$1 = {
508
1010
  key: 4,
509
1011
  class: "fl-preview-text"
510
1012
  };
511
- const _hoisted_26$1 = {
1013
+ const _hoisted_23$1 = {
512
1014
  key: 0,
513
1015
  class: "fl-preview-note"
514
1016
  };
515
- const _hoisted_27$1 = ["src", "title"];
516
- const _hoisted_28$1 = {
1017
+ const _hoisted_24$1 = ["src", "title"];
1018
+ const _hoisted_25$1 = {
517
1019
  key: 6,
518
1020
  class: "fl-preview-info"
519
1021
  };
520
- const _hoisted_29$1 = ["data-kind"];
521
- const _hoisted_30$1 = { class: "fl-preview-message" };
1022
+ const _hoisted_26$1 = ["data-kind"];
1023
+ const _hoisted_27$1 = { class: "fl-preview-message" };
522
1024
  const _sfc_main$1 = /*@__PURE__*/ defineComponent({
523
1025
  __name: "PreviewDialog",
524
1026
  props: {
@@ -549,11 +1051,12 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
549
1051
  ],
550
1052
  setup(__props, { emit: __emit }) {
551
1053
  /**
552
- * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
1054
+ * File preview in a native <dialog>, without leaving the page.
553
1055
  *
554
- * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
555
- * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
556
- * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
1056
+ * image / video / audio: native elements; text: the first `maxTextBytes`
1057
+ * through a Range request; pdf: the first matching OpenList `iframe_previews`
1058
+ * viewer, else the browser's reader through a Blob. Anything else (Office
1059
+ * included), failures, oversized PDFs and demo mode: file info + download.
557
1060
  */
558
1061
  const props = __props;
559
1062
  const emit = __emit;
@@ -737,17 +1240,10 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
737
1240
  "aria-label": t("previous"),
738
1241
  title: t("previous"),
739
1242
  onClick: _cache[0] || (_cache[0] = ($event) => emit("prev"))
740
- }, [..._cache[7] || (_cache[7] = [createElementVNode("svg", {
741
- viewBox: "0 0 24 24",
742
- width: "18",
743
- height: "18",
744
- fill: "none",
745
- stroke: "currentColor",
746
- "stroke-width": "2.2",
747
- "stroke-linecap": "round",
748
- "stroke-linejoin": "round",
749
- "aria-hidden": "true"
750
- }, [createElementVNode("path", { d: "m15 6-6 6 6 6" })], -1)])], 8, _hoisted_9$1),
1243
+ }, [createVNode(_sfc_main$2, {
1244
+ name: "previous",
1245
+ size: 18
1246
+ })], 8, _hoisted_9$1),
751
1247
  createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
752
1248
  createElementVNode("button", {
753
1249
  class: "fl-preview-tool",
@@ -755,98 +1251,62 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
755
1251
  "aria-label": t("next"),
756
1252
  title: t("next"),
757
1253
  onClick: _cache[1] || (_cache[1] = ($event) => emit("next"))
758
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
759
- viewBox: "0 0 24 24",
760
- width: "18",
761
- height: "18",
762
- fill: "none",
763
- stroke: "currentColor",
764
- "stroke-width": "2.2",
765
- "stroke-linecap": "round",
766
- "stroke-linejoin": "round",
767
- "aria-hidden": "true"
768
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 8, _hoisted_11$1)
1254
+ }, [createVNode(_sfc_main$2, {
1255
+ name: "next",
1256
+ size: 18
1257
+ })], 8, _hoisted_11$1)
769
1258
  ], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
770
1259
  class: "fl-preview-tool fl-preview-rotate",
771
1260
  type: "button",
772
1261
  "aria-label": t("rotate"),
773
1262
  title: t("rotate"),
774
1263
  onClick: _cache[2] || (_cache[2] = ($event) => rotation.value = (rotation.value + 90) % 360)
775
- }, [..._cache[9] || (_cache[9] = [createElementVNode("svg", {
776
- viewBox: "0 0 24 24",
777
- width: "17",
778
- height: "17",
779
- fill: "none",
780
- stroke: "currentColor",
781
- "stroke-width": "2",
782
- "stroke-linecap": "round",
783
- "stroke-linejoin": "round",
784
- "aria-hidden": "true"
785
- }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 8, _hoisted_12$1), createElementVNode("button", {
1264
+ }, [createVNode(_sfc_main$2, {
1265
+ name: "refresh",
1266
+ size: 17
1267
+ })], 8, _hoisted_12$1), createElementVNode("button", {
786
1268
  class: "fl-preview-tool fl-preview-zoom",
787
1269
  type: "button",
788
1270
  "aria-pressed": actualSize.value,
789
1271
  "aria-label": t("actualSize"),
790
1272
  title: t("actualSize"),
791
1273
  onClick: _cache[3] || (_cache[3] = ($event) => actualSize.value = !actualSize.value)
792
- }, [(openBlock(), createElementBlock("svg", _hoisted_14$1, [
793
- _cache[10] || (_cache[10] = createElementVNode("circle", {
794
- cx: "11",
795
- cy: "11",
796
- r: "7"
797
- }, null, -1)),
798
- _cache[11] || (_cache[11] = createElementVNode("path", { d: "m20 20-3.5-3.5" }, null, -1)),
799
- !actualSize.value ? (openBlock(), createElementBlock("path", _hoisted_15$1)) : (openBlock(), createElementBlock("path", _hoisted_16$1))
800
- ]))], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
801
- createElementVNode("div", _hoisted_17$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
1274
+ }, [createVNode(_sfc_main$2, {
1275
+ name: actualSize.value ? "zoomOut" : "zoomIn",
1276
+ size: 17
1277
+ }, null, 8, ["name"])], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
1278
+ createElementVNode("div", _hoisted_14$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
802
1279
  key: 0,
803
1280
  class: "fl-preview-tool fl-preview-download",
804
1281
  type: "button",
805
1282
  "aria-label": t("download"),
806
1283
  title: t("download"),
807
1284
  onClick: _cache[4] || (_cache[4] = ($event) => emit("download"))
808
- }, [..._cache[12] || (_cache[12] = [createElementVNode("svg", {
809
- viewBox: "0 0 24 24",
810
- width: "17",
811
- height: "17",
812
- fill: "none",
813
- stroke: "currentColor",
814
- "stroke-width": "2",
815
- "stroke-linecap": "round",
816
- "stroke-linejoin": "round",
817
- "aria-hidden": "true"
818
- }, [
819
- createElementVNode("path", { d: "M12 3v12" }),
820
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
821
- createElementVNode("path", { d: "M5 21h14" })
822
- ], -1)])], 8, _hoisted_18$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
1285
+ }, [createVNode(_sfc_main$2, {
1286
+ name: "download",
1287
+ size: 17
1288
+ })], 8, _hoisted_15$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
823
1289
  class: "fl-preview-tool fl-preview-close",
824
1290
  type: "button",
825
1291
  autofocus: "",
826
1292
  "aria-label": t("close"),
827
1293
  title: t("close"),
828
1294
  onClick: _cache[5] || (_cache[5] = ($event) => emit("close"))
829
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
830
- viewBox: "0 0 24 24",
831
- width: "18",
832
- height: "18",
833
- fill: "none",
834
- stroke: "currentColor",
835
- "stroke-width": "2.2",
836
- "stroke-linecap": "round",
837
- "aria-hidden": "true"
838
- }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_19$1)])
1295
+ }, [createVNode(_sfc_main$2, {
1296
+ name: "close",
1297
+ size: 18
1298
+ })], 8, _hoisted_16$1)])
839
1299
  ]), createElementVNode("div", {
840
1300
  class: "fl-preview-body",
841
1301
  "data-mode": busy.value ? "loading" : mode.value
842
- }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_21$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
1302
+ }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_18$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
843
1303
  key: 1,
844
1304
  class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
845
1305
  src: __props.src,
846
1306
  alt: __props.file.name,
847
1307
  style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
848
1308
  onError: markFailed
849
- }, null, 46, _hoisted_22$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
1309
+ }, null, 46, _hoisted_19$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
850
1310
  key: 2,
851
1311
  class: "fl-preview-video",
852
1312
  src: __props.src,
@@ -854,49 +1314,106 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
854
1314
  playsinline: "",
855
1315
  preload: "metadata",
856
1316
  onError: markFailed
857
- }, null, 40, _hoisted_23$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
1317
+ }, null, 40, _hoisted_20$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
858
1318
  key: 3,
859
1319
  class: "fl-preview-audio",
860
1320
  src: __props.src,
861
1321
  controls: "",
862
1322
  preload: "metadata",
863
1323
  onError: markFailed
864
- }, null, 40, _hoisted_24$1)) : mode.value === "text" ? (openBlock(), createElementBlock("div", _hoisted_25$1, [truncated.value ? (openBlock(), createElementBlock("p", _hoisted_26$1, toDisplayString(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) })), 1)) : createCommentVNode("v-if", true), createElementVNode("pre", null, toDisplayString(textContent.value), 1)])) : mode.value === "pdf" ? (openBlock(), createElementBlock("iframe", {
1324
+ }, null, 40, _hoisted_21$1)) : mode.value === "text" ? (openBlock(), createElementBlock("div", _hoisted_22$1, [truncated.value ? (openBlock(), createElementBlock("p", _hoisted_23$1, toDisplayString(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) })), 1)) : createCommentVNode("v-if", true), createElementVNode("pre", null, toDisplayString(textContent.value), 1)])) : mode.value === "pdf" ? (openBlock(), createElementBlock("iframe", {
865
1325
  key: 5,
866
1326
  class: "fl-preview-frame",
867
1327
  src: __props.viewer || blobUrl.value,
868
1328
  title: __props.file.name
869
- }, null, 8, _hoisted_27$1)) : (openBlock(), createElementBlock("div", _hoisted_28$1, [
1329
+ }, null, 8, _hoisted_24$1)) : (openBlock(), createElementBlock("div", _hoisted_25$1, [
870
1330
  createElementVNode("span", {
871
1331
  class: "fl-type fl-preview-badge",
872
1332
  "data-kind": unref(fileKind)(__props.file.name),
873
1333
  "aria-hidden": "true"
874
- }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_29$1),
875
- createElementVNode("p", _hoisted_30$1, toDisplayString(infoMessage.value), 1),
1334
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_26$1),
1335
+ createElementVNode("p", _hoisted_27$1, toDisplayString(infoMessage.value), 1),
876
1336
  !__props.demo ? (openBlock(), createElementBlock("button", {
877
1337
  key: 0,
878
1338
  class: "fl-btn fl-btn-primary",
879
1339
  type: "button",
880
1340
  onClick: _cache[6] || (_cache[6] = ($event) => emit("download"))
881
- }, [_cache[14] || (_cache[14] = createElementVNode("svg", {
882
- viewBox: "0 0 24 24",
883
- width: "15",
884
- height: "15",
885
- fill: "none",
886
- stroke: "currentColor",
887
- "stroke-width": "2.2",
888
- "stroke-linecap": "round",
889
- "stroke-linejoin": "round",
890
- "aria-hidden": "true"
891
- }, [
892
- createElementVNode("path", { d: "M12 3v12" }),
893
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
894
- createElementVNode("path", { d: "M5 21h14" })
895
- ], -1)), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
896
- ]))], 8, _hoisted_20$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
1341
+ }, [createVNode(_sfc_main$2, {
1342
+ name: "download",
1343
+ size: 15
1344
+ }), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
1345
+ ]))], 8, _hoisted_17$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
897
1346
  };
898
1347
  }
899
1348
  });
1349
+ // src/client/tree.ts
1350
+ function newLock(prompting = false, error = "") {
1351
+ return {
1352
+ prompting,
1353
+ input: "",
1354
+ error,
1355
+ remember: false,
1356
+ busy: false
1357
+ };
1358
+ }
1359
+ function toNodes(items, parent, password) {
1360
+ return items.map((item) => ({
1361
+ ...item,
1362
+ path: joinPath(parent.path, item.name),
1363
+ depth: parent.depth + 1,
1364
+ expanded: false,
1365
+ loaded: false,
1366
+ loading: false,
1367
+ children: [],
1368
+ password,
1369
+ zipPath: [...parent.zipPath, item.name]
1370
+ }));
1371
+ }
1372
+ function sortTree(nodes, key, ascending) {
1373
+ nodes.sort((a, b) => compareNodes(a, b, key, ascending));
1374
+ for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
1375
+ }
1376
+ /** Loaded files at or under a node. */
1377
+ function descendantFiles(node) {
1378
+ return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
1379
+ }
1380
+ function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1381
+ for (const node of nodes) if (node.is_dir && node.expanded) {
1382
+ out.add(node.path);
1383
+ expandedPathsOf(node.children, out);
1384
+ }
1385
+ return out;
1386
+ }
1387
+ /** Last path segment, for labels. */
1388
+ function sourceName(path) {
1389
+ return path.split("/").filter(Boolean).pop() || path;
1390
+ }
1391
+ /** Loaded file count, size and selected count for every folder, in one pass. */
1392
+ function folderStats(roots, selected) {
1393
+ const stats = /* @__PURE__ */ new Map();
1394
+ const walk = (node) => {
1395
+ if (!node.is_dir) return {
1396
+ files: 1,
1397
+ bytes: totalSize([node]),
1398
+ selected: selected.has(node.path) ? 1 : 0
1399
+ };
1400
+ const total = {
1401
+ files: 0,
1402
+ bytes: 0,
1403
+ selected: 0
1404
+ };
1405
+ for (const child of node.children) {
1406
+ const part = walk(child);
1407
+ total.files += part.files;
1408
+ total.bytes += part.bytes;
1409
+ total.selected += part.selected;
1410
+ }
1411
+ stats.set(node.path, total);
1412
+ return total;
1413
+ };
1414
+ roots.forEach(walk);
1415
+ return stats;
1416
+ }
900
1417
  // src/client/passwords.ts
901
1418
  /**
902
1419
  * Folder passwords remembered for the current browser tab.
@@ -934,12 +1451,6 @@ function write(base, map) {
934
1451
  function normalise(path) {
935
1452
  return path.replace(/\/+$/, "") || "/";
936
1453
  }
937
- /** Whether `path` is `folder` itself or inside it. */
938
- function isWithin(path, folder) {
939
- const p = normalise(path);
940
- const f = normalise(folder);
941
- return f === "/" || p === f || p.startsWith(`${f}/`);
942
- }
943
1454
  /** The closest remembered folder at or above `path`, if any. */
944
1455
  function findRememberedPassword(base, path) {
945
1456
  let best = null;
@@ -967,6 +1478,73 @@ function forgetFolderPassword(base, path) {
967
1478
  function rememberedFolders(base) {
968
1479
  return Object.keys(read(base));
969
1480
  }
1481
+ // src/client/usePasswords.ts
1482
+ /**
1483
+ * The one place that decides which folder password to send.
1484
+ *
1485
+ * Order: typed on this page for this exact folder → the item's own password
1486
+ * (site-configured, or inherited from the folder it was listed with) → typed
1487
+ * for an enclosing folder → remembered in this tab → the component default.
1488
+ */
1489
+ function usePasswords(options) {
1490
+ /** Typed on this page; not necessarily remembered. Survives refreshes. */
1491
+ const typed = /* @__PURE__ */ new Map();
1492
+ /** Remembered folders, mirrored from sessionStorage so templates react. */
1493
+ const remembered = ref(/* @__PURE__ */ new Set());
1494
+ function sync() {
1495
+ remembered.value = new Set(options.enabled() ? rememberedFolders(options.scope()) : []);
1496
+ }
1497
+ function typedAbove(path) {
1498
+ let best = "";
1499
+ let bestFolder = "";
1500
+ for (const [folder, password] of typed) if (isWithin(path, folder) && folder.length > bestFolder.length) {
1501
+ best = password;
1502
+ bestFolder = folder;
1503
+ }
1504
+ return best;
1505
+ }
1506
+ function resolve(path, own = "") {
1507
+ const exact = typed.get(path);
1508
+ if (exact) return { password: exact };
1509
+ if (own) return { password: own };
1510
+ const above = typedAbove(path);
1511
+ if (above) return { password: above };
1512
+ const stored = options.enabled() ? findRememberedPassword(options.scope(), path) : null;
1513
+ if (stored) return {
1514
+ password: stored.password,
1515
+ rememberedAt: stored.path
1516
+ };
1517
+ return { password: options.fallback() };
1518
+ }
1519
+ /** A password worked: keep it for this page, and in this tab when asked. */
1520
+ function accept(path, password, remember) {
1521
+ typed.set(path, password);
1522
+ if (remember && options.enabled()) {
1523
+ rememberFolderPassword(options.scope(), path, password);
1524
+ sync();
1525
+ }
1526
+ }
1527
+ function forget(path) {
1528
+ typed.delete(path);
1529
+ forgetFolderPassword(options.scope(), path);
1530
+ sync();
1531
+ }
1532
+ /** A remembered password stopped working: drop it. Returns whether one was dropped. */
1533
+ function dropStale(rememberedAt) {
1534
+ if (!rememberedAt) return false;
1535
+ forgetFolderPassword(options.scope(), rememberedAt);
1536
+ sync();
1537
+ return true;
1538
+ }
1539
+ return {
1540
+ remembered,
1541
+ sync,
1542
+ resolve,
1543
+ accept,
1544
+ forget,
1545
+ dropStale
1546
+ };
1547
+ }
970
1548
  // src/client/FileList.vue
971
1549
  const _hoisted_1 = ["aria-busy"];
972
1550
  const _hoisted_2 = { class: "fl-bar" };
@@ -976,129 +1554,129 @@ const _hoisted_3 = {
976
1554
  role: "status"
977
1555
  };
978
1556
  const _hoisted_4 = { class: "fl-selection-count" };
979
- const _hoisted_5 = { class: "fl-selection-size" };
1557
+ const _hoisted_5 = {
1558
+ key: 0,
1559
+ class: "fl-selection-size"
1560
+ };
980
1561
  const _hoisted_6 = {
981
1562
  key: 1,
1563
+ class: "fl-selection",
1564
+ role: "status"
1565
+ };
1566
+ const _hoisted_7 = { class: "fl-selection-count" };
1567
+ const _hoisted_8 = { class: "fl-selection-size" };
1568
+ const _hoisted_9 = {
1569
+ key: 2,
982
1570
  class: "fl-title"
983
1571
  };
984
- const _hoisted_7 = { class: "fl-title-text" };
985
- const _hoisted_8 = {
1572
+ const _hoisted_10 = { class: "fl-title-text" };
1573
+ const _hoisted_11 = {
986
1574
  key: 0,
987
1575
  class: "fl-demo-badge"
988
1576
  };
989
- const _hoisted_9 = ["aria-label", "title"];
990
- const _hoisted_10 = { class: "fl-clear-text" };
991
- const _hoisted_11 = { class: "fl-tools" };
992
- const _hoisted_12 = { class: "fl-search" };
993
- const _hoisted_13 = ["placeholder", "aria-label"];
994
- const _hoisted_14 = [
1577
+ const _hoisted_12 = ["aria-label", "title"];
1578
+ const _hoisted_13 = { class: "fl-clear-text" };
1579
+ const _hoisted_14 = { class: "fl-tools" };
1580
+ const _hoisted_15 = { class: "fl-search" };
1581
+ const _hoisted_16 = ["placeholder", "aria-label"];
1582
+ const _hoisted_17 = [
995
1583
  "disabled",
996
1584
  "aria-label",
997
1585
  "title"
998
1586
  ];
999
- const _hoisted_15 = ["disabled"];
1000
- const _hoisted_16 = { class: "fl-row fl-head" };
1001
- const _hoisted_17 = {
1587
+ const _hoisted_18 = { class: "fl-row fl-head" };
1588
+ const _hoisted_19 = {
1002
1589
  key: 0,
1003
1590
  class: "fl-progress",
1004
1591
  "aria-hidden": "true"
1005
1592
  };
1006
- const _hoisted_18 = { class: "fl-cell fl-cell-check" };
1007
- const _hoisted_19 = [
1593
+ const _hoisted_20 = { class: "fl-cell fl-cell-check" };
1594
+ const _hoisted_21 = [
1008
1595
  "aria-label",
1596
+ "disabled",
1009
1597
  "checked",
1010
1598
  ".indeterminate"
1011
1599
  ];
1012
- const _hoisted_20 = { class: "fl-cell fl-cell-name" };
1013
- const _hoisted_21 = ["aria-sort"];
1014
- const _hoisted_22 = {
1015
- class: "fl-sort",
1016
- "aria-hidden": "true"
1017
- };
1018
- const _hoisted_23 = { class: "fl-cell fl-cell-size" };
1019
- const _hoisted_24 = {
1600
+ const _hoisted_22 = ["aria-sort", "onClick"];
1601
+ const _hoisted_23 = {
1020
1602
  class: "fl-sort",
1021
1603
  "aria-hidden": "true"
1022
1604
  };
1023
- const _hoisted_25 = { class: "fl-cell fl-cell-date" };
1605
+ const _hoisted_24 = { class: "fl-cell fl-cell-op" };
1606
+ const _hoisted_25 = { class: "fl-sort-select" };
1024
1607
  const _hoisted_26 = {
1025
- class: "fl-sort",
1026
- "aria-hidden": "true"
1027
- };
1028
- const _hoisted_27 = { class: "fl-cell fl-cell-op" };
1029
- const _hoisted_28 = { class: "fl-sort-select" };
1030
- const _hoisted_29 = {
1031
1608
  class: "fl-sort-current",
1032
1609
  "aria-hidden": "true"
1033
1610
  };
1034
- const _hoisted_30 = ["aria-label"];
1035
- const _hoisted_31 = ["value"];
1036
- const _hoisted_32 = {
1611
+ const _hoisted_27 = ["aria-label"];
1612
+ const _hoisted_28 = ["value"];
1613
+ const _hoisted_29 = {
1037
1614
  key: 0,
1038
1615
  class: "fl-status"
1039
1616
  };
1040
- const _hoisted_33 = ["for"];
1041
- const _hoisted_34 = {
1042
- key: 0,
1043
- id: "fl-root-unlock-error",
1044
- class: "fl-unlock-message",
1045
- role: "alert"
1046
- };
1047
- const _hoisted_35 = { class: "fl-unlock" };
1048
- const _hoisted_36 = [
1617
+ const _hoisted_30 = ["for"];
1618
+ const _hoisted_31 = ["id"];
1619
+ const _hoisted_32 = { class: "fl-unlock" };
1620
+ const _hoisted_33 = [
1049
1621
  "id",
1050
1622
  "data-unlock",
1051
1623
  "placeholder",
1052
1624
  "aria-invalid",
1053
1625
  "aria-describedby"
1054
1626
  ];
1055
- const _hoisted_37 = [
1627
+ const _hoisted_34 = [
1056
1628
  "aria-pressed",
1057
1629
  "aria-label",
1058
1630
  "title"
1059
1631
  ];
1060
- const _hoisted_38 = ["disabled"];
1061
- const _hoisted_39 = {
1632
+ const _hoisted_35 = ["disabled"];
1633
+ const _hoisted_36 = {
1062
1634
  key: 2,
1063
1635
  class: "fl-status fl-error"
1064
1636
  };
1065
- const _hoisted_40 = {
1637
+ const _hoisted_37 = {
1066
1638
  key: 0,
1067
1639
  class: "fl-hint"
1068
1640
  };
1069
- const _hoisted_41 = {
1641
+ const _hoisted_38 = {
1070
1642
  key: 1,
1071
1643
  class: "fl-hint"
1072
1644
  };
1073
- const _hoisted_42 = {
1645
+ const _hoisted_39 = {
1074
1646
  key: 3,
1075
1647
  class: "fl-status"
1076
1648
  };
1077
- const _hoisted_43 = { class: "fl-cell fl-cell-check" };
1078
- const _hoisted_44 = [
1649
+ const _hoisted_40 = { class: "fl-cell fl-more-cell" };
1650
+ const _hoisted_41 = { class: "fl-more-count" };
1651
+ const _hoisted_42 = ["onClick"];
1652
+ const _hoisted_43 = { class: "fl-more-page" };
1653
+ const _hoisted_44 = ["onClick"];
1654
+ const _hoisted_45 = ["onClick"];
1655
+ const _hoisted_46 = { class: "fl-cell fl-cell-check" };
1656
+ const _hoisted_47 = [
1079
1657
  "disabled",
1080
1658
  "aria-label",
1081
1659
  "checked",
1082
1660
  ".indeterminate",
1083
1661
  "onChange"
1084
1662
  ];
1085
- const _hoisted_45 = ["title"];
1086
- const _hoisted_46 = [
1663
+ const _hoisted_48 = ["title"];
1664
+ const _hoisted_49 = [
1087
1665
  "aria-label",
1088
1666
  "title",
1089
1667
  "onClick"
1090
1668
  ];
1091
- const _hoisted_47 = [
1669
+ const _hoisted_50 = [
1092
1670
  "aria-label",
1093
1671
  "aria-expanded",
1094
1672
  "onClick"
1095
1673
  ];
1096
- const _hoisted_48 = {
1674
+ const _hoisted_51 = {
1097
1675
  key: 2,
1098
1676
  class: "fl-arrow-placeholder",
1099
1677
  "aria-hidden": "true"
1100
1678
  };
1101
- const _hoisted_49 = {
1679
+ const _hoisted_52 = {
1102
1680
  key: 3,
1103
1681
  class: "fl-folder-icon",
1104
1682
  viewBox: "0 0 24 24",
@@ -1106,27 +1684,27 @@ const _hoisted_49 = {
1106
1684
  height: "18",
1107
1685
  "aria-hidden": "true"
1108
1686
  };
1109
- const _hoisted_50 = ["data-kind"];
1110
- const _hoisted_51 = { class: "fl-label" };
1111
- const _hoisted_52 = ["onClick"];
1112
- const _hoisted_53 = { class: "fl-meta" };
1113
- const _hoisted_54 = {
1687
+ const _hoisted_53 = ["data-kind"];
1688
+ const _hoisted_54 = { class: "fl-label" };
1689
+ const _hoisted_55 = ["onClick"];
1690
+ const _hoisted_56 = { class: "fl-meta" };
1691
+ const _hoisted_57 = {
1114
1692
  key: 5,
1115
1693
  class: "fl-count"
1116
1694
  };
1117
- const _hoisted_55 = ["id"];
1118
- const _hoisted_56 = [
1695
+ const _hoisted_58 = ["id"];
1696
+ const _hoisted_59 = [
1119
1697
  "aria-label",
1120
1698
  "title",
1121
1699
  "onClick"
1122
1700
  ];
1123
- const _hoisted_57 = {
1701
+ const _hoisted_60 = {
1124
1702
  key: 8,
1125
1703
  class: "fl-loading",
1126
1704
  "aria-hidden": "true"
1127
1705
  };
1128
- const _hoisted_58 = ["onSubmit", "onKeydown"];
1129
- const _hoisted_59 = [
1706
+ const _hoisted_61 = ["onSubmit", "onKeydown"];
1707
+ const _hoisted_62 = [
1130
1708
  "onUpdate:modelValue",
1131
1709
  "data-unlock",
1132
1710
  "placeholder",
@@ -1134,43 +1712,45 @@ const _hoisted_59 = [
1134
1712
  "aria-invalid",
1135
1713
  "aria-describedby"
1136
1714
  ];
1137
- const _hoisted_60 = [
1715
+ const _hoisted_63 = [
1138
1716
  "aria-pressed",
1139
1717
  "aria-label",
1140
1718
  "title",
1141
1719
  "onClick"
1142
1720
  ];
1143
- const _hoisted_61 = ["disabled"];
1144
- const _hoisted_62 = [
1721
+ const _hoisted_64 = ["disabled"];
1722
+ const _hoisted_65 = [
1145
1723
  "aria-label",
1146
1724
  "title",
1147
1725
  "onClick"
1148
1726
  ];
1149
- const _hoisted_63 = { class: "fl-cell fl-cell-size" };
1150
- const _hoisted_64 = { class: "fl-cell fl-cell-date" };
1151
- const _hoisted_65 = { class: "fl-cell fl-cell-op" };
1152
- const _hoisted_66 = ["aria-label", "onClick"];
1153
- const _hoisted_67 = [
1727
+ const _hoisted_66 = { class: "fl-cell fl-cell-size" };
1728
+ const _hoisted_67 = { class: "fl-cell fl-cell-date" };
1729
+ const _hoisted_68 = { class: "fl-cell fl-cell-op" };
1730
+ const _hoisted_69 = ["aria-label", "onClick"];
1731
+ const _hoisted_70 = [
1154
1732
  "aria-label",
1155
1733
  "aria-expanded",
1156
1734
  "onClick"
1157
1735
  ];
1158
- const _hoisted_68 = [
1736
+ const _hoisted_71 = [
1159
1737
  "aria-label",
1160
1738
  "title",
1161
1739
  "onClick"
1162
1740
  ];
1163
- const _hoisted_69 = [
1741
+ const _hoisted_72 = [
1164
1742
  "aria-label",
1165
1743
  "title",
1166
1744
  "onClick"
1167
1745
  ];
1168
- const _hoisted_70 = { class: "fl-foot" };
1169
- const _hoisted_71 = {
1746
+ const _hoisted_73 = { class: "fl-foot" };
1747
+ const _hoisted_74 = {
1170
1748
  key: 4,
1171
1749
  class: "fl-toast",
1172
1750
  role: "status"
1173
1751
  };
1752
+ /** Rows shown per folder, beyond the first page; kept across refreshes. */
1753
+ const MB = 1048576;
1174
1754
  const _sfc_main = /*@__PURE__*/ defineComponent({
1175
1755
  __name: "FileList",
1176
1756
  props: {
@@ -1188,24 +1768,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1188
1768
  type: Boolean,
1189
1769
  default: true
1190
1770
  },
1771
+ pageSize: { default: 200 },
1772
+ zipWarnSize: {},
1773
+ zipMaxSize: {},
1191
1774
  locale: { default: "zh-CN" },
1192
1775
  labels: {}
1193
1776
  },
1194
1777
  setup(__props) {
1195
1778
  /**
1196
- * FileList.vue —— VitePress 网盘文件列表(界面沿用 FileList,数据来自 OpenList)
1197
- *
1198
- * 用法:
1199
- * <FileList base="https://pan.example.com" path="/课程资料" />
1200
- * <FileList /> // 演示模式
1779
+ * OpenList file browser: folder tree, batch ZIP, folder passwords and an
1780
+ * in-page preview. OpenList access is in api.ts (demo.ts when `base` is empty),
1781
+ * password choice in usePasswords.ts, tree helpers in tree.ts and downloads in
1782
+ * download.ts; this component wires them to the UI.
1201
1783
  */
1202
1784
  const props = __props;
1203
1785
  const text = computed(() => resolveLabels(props.locale, props.labels));
1204
1786
  function t(key, values) {
1205
1787
  return formatLabel(text.value[key], values);
1206
1788
  }
1789
+ /** Prefix for element ids, unique per component instance. */
1790
+ const uid = useId();
1207
1791
  const demo = computed(() => !props.base);
1208
- const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
1792
+ const api = computed(() => demo.value ? createDemoApi() : createOpenListApi(props.base, props.token));
1793
+ const canRemember = computed(() => props.rememberPassword !== false);
1794
+ const passwords = usePasswords({
1795
+ scope: () => props.base.replace(/\/+$/, "") || "demo",
1796
+ enabled: () => canRemember.value,
1797
+ fallback: () => props.password
1798
+ });
1799
+ const remembered = passwords.remembered;
1209
1800
  const rootEl = ref();
1210
1801
  const roots = ref([]);
1211
1802
  const loadingRoot = ref(false);
@@ -1213,413 +1804,155 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1213
1804
  const refreshing = ref(false);
1214
1805
  let refreshSeq = 0;
1215
1806
  /** Single-path mode: the listed folder itself needs a password. */
1216
- const rootLock = reactive({
1217
- active: false,
1218
- input: "",
1219
- error: "",
1220
- remember: false,
1221
- busy: false
1222
- });
1223
- /** Password the visitor typed for the listed folder (single-path mode). */
1224
- const rootPassword = ref("");
1807
+ const rootLock = ref(null);
1225
1808
  const keyword = ref("");
1226
1809
  const selected = ref(/* @__PURE__ */ new Set());
1227
1810
  const toast = ref("");
1228
1811
  const sortKey = ref("name");
1229
1812
  const sortAsc = ref(true);
1230
1813
  const siteTitle = ref("");
1231
- /** OpenList's public `iframe_previews` setting (JSON text). */
1232
1814
  const iframePreviews = ref("");
1233
1815
  const batchBusy = ref(false);
1234
- const batchProgress = ref(0);
1235
- const downloadCache = /* @__PURE__ */ new Map();
1816
+ const zipProgress = ref(null);
1817
+ let zipAbort = null;
1236
1818
  let toastTimer;
1237
- function showToast(msg) {
1238
- toast.value = msg;
1819
+ /** The folder listed in single-path mode, as the parent of the top rows. */
1820
+ const listedFolder = computed(() => ({
1821
+ path: props.path,
1822
+ depth: -1,
1823
+ zipPath: []
1824
+ }));
1825
+ function showToast(message) {
1826
+ toast.value = message;
1239
1827
  clearTimeout(toastTimer);
1240
1828
  toastTimer = setTimeout(() => toast.value = "", 3200);
1241
1829
  }
1242
- function errorText(error) {
1243
- return error instanceof Error ? error.message : String(error);
1244
- }
1245
- function isPasswordError(error) {
1246
- return /password|密码/i.test(errorText(error));
1247
- }
1248
- function isNetworkError(error) {
1249
- return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
1250
- }
1251
1830
  /** A failure toast; browser network errors are usually CORS, so say where to look. */
1252
1831
  function failureMessage(key, error) {
1253
1832
  const message = t(key, { message: errorText(error) });
1254
- return isNetworkError(error) ? `${message} ${t("networkHint")}` : message;
1255
- }
1256
- /** Total size for display: an empty total reads `0 B`, not `-`. */
1257
- function formatTotal(bytes) {
1258
- return bytes > 0 ? formatSize(bytes) : "0 B";
1259
- }
1260
- const canRemember = computed(() => props.rememberPassword !== false);
1261
- const passwordScope = computed(() => baseUrl.value || "demo");
1262
- /** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
1263
- const typedPasswords = /* @__PURE__ */ new Map();
1264
- function typedPasswordFor(path) {
1265
- let best = "";
1266
- let bestPath = "";
1267
- for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
1268
- best = password;
1269
- bestPath = folder;
1270
- }
1271
- return best;
1272
- }
1273
- /** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
1274
- const rememberedPaths = ref(/* @__PURE__ */ new Set());
1275
- function syncRemembered() {
1276
- rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
1277
- }
1278
- function storedPassword(path) {
1279
- return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
1280
- }
1281
- /**
1282
- * Password for an item: its own or inherited one first (site-configured or
1283
- * typed), then one remembered in this tab, then the component's `password`.
1284
- */
1285
- function passwordFor(node) {
1286
- if (node.password) return { password: node.password };
1287
- const typed = typedPasswordFor(node.path);
1288
- if (typed) return { password: typed };
1289
- const stored = storedPassword(node.path);
1290
- if (stored) return {
1291
- password: stored.password,
1292
- rememberedAt: stored.path
1293
- };
1294
- return { password: props.password };
1295
- }
1296
- function rootPasswordFor() {
1297
- if (rootPassword.value) return { password: rootPassword.value };
1298
- const stored = storedPassword(props.path);
1299
- if (stored) return {
1300
- password: stored.password,
1301
- rememberedAt: stored.path
1302
- };
1303
- return { password: props.password };
1304
- }
1305
- /** A remembered password stopped working: drop it so we do not keep retrying it. */
1306
- function dropStale(path) {
1307
- if (!path) return false;
1308
- forgetFolderPassword(passwordScope.value, path);
1309
- syncRemembered();
1310
- return true;
1311
- }
1312
- function focusUnlock(path) {
1313
- nextTick(() => {
1314
- (rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
1315
- });
1316
- }
1317
- /** Show the same-row password input; `focus` only for user actions, never on page load. */
1318
- function startPrompt(node, focus) {
1319
- node.prompting = true;
1320
- if (focus) focusUnlock(node.path);
1321
- }
1322
- function cancelUnlock(node) {
1323
- node.prompting = false;
1324
- node.unlockInput = "";
1325
- node.unlockError = "";
1326
- }
1327
- function unlockErrorText(error) {
1328
- if (error === "wrong") return t("passwordWrong");
1329
- if (error === "stale") return t("passwordStale");
1330
- return "";
1331
- }
1332
- const DEMO_TREE = {
1333
- "/": [
1334
- {
1335
- name: "assignments",
1336
- size: 0,
1337
- is_dir: true,
1338
- modified: "2026-09-01T08:00:00+08:00"
1339
- },
1340
- {
1341
- name: "exams",
1342
- size: 0,
1343
- is_dir: true,
1344
- modified: "2026-09-01T08:00:00+08:00"
1345
- },
1346
- {
1347
- name: "materials",
1348
- size: 0,
1349
- is_dir: true,
1350
- modified: "2026-09-01T08:00:00+08:00"
1351
- },
1352
- {
1353
- name: "notes",
1354
- size: 0,
1355
- is_dir: true,
1356
- modified: "2026-09-01T08:00:00+08:00"
1357
- },
1358
- {
1359
- name: "slides",
1360
- size: 0,
1361
- is_dir: true,
1362
- modified: "2026-09-01T08:00:00+08:00"
1363
- },
1364
- {
1365
- name: "加密示例(密码 1234)",
1366
- size: 0,
1367
- is_dir: true,
1368
- modified: "2026-10-20T18:00:00+08:00"
1369
- }
1370
- ],
1371
- "/assignments": [{
1372
- name: "线代作业参考答案",
1373
- size: 0,
1374
- is_dir: true,
1375
- modified: "2026-10-12T21:30:00+08:00"
1376
- }, {
1377
- name: "作业提交说明.pdf",
1378
- size: 186432,
1379
- is_dir: false,
1380
- modified: "2026-09-03T10:15:00+08:00"
1381
- }],
1382
- "/assignments/线代作业参考答案": [
1383
- {
1384
- name: "第1次作业参考答案.doc",
1385
- size: 412672,
1386
- is_dir: false,
1387
- modified: "2026-09-15T22:41:00+08:00"
1388
- },
1389
- {
1390
- name: "第2次作业参考答案.pdf",
1391
- size: 1048576,
1392
- is_dir: false,
1393
- modified: "2026-09-22T22:03:00+08:00"
1394
- },
1395
- {
1396
- name: "第3次作业参考答案.pdf",
1397
- size: 986112,
1398
- is_dir: false,
1399
- modified: "2026-09-29T21:57:00+08:00"
1400
- },
1401
- {
1402
- name: "第4次作业参考答案.pdf",
1403
- size: 1572864,
1404
- is_dir: false,
1405
- modified: "2026-10-13T22:10:00+08:00"
1406
- }
1407
- ],
1408
- "/exams": [
1409
- {
1410
- name: "2023 期末试题和答案.pdf",
1411
- size: 2345678,
1412
- is_dir: false,
1413
- modified: "2025-01-18T09:20:00+08:00"
1414
- },
1415
- {
1416
- name: "2024 期中试题.pdf",
1417
- size: 1234567,
1418
- is_dir: false,
1419
- modified: "2025-11-02T14:08:00+08:00"
1420
- },
1421
- {
1422
- name: "代数与几何-多年合集-试题和答案.pdf",
1423
- size: 18874368,
1424
- is_dir: false,
1425
- modified: "2025-01-18T09:25:00+08:00"
1426
- }
1427
- ],
1428
- "/materials": [
1429
- {
1430
- name: "线性代数及其应用(Lay 第5版).pdf",
1431
- size: 48231424,
1432
- is_dir: false,
1433
- modified: "2024-07-29T16:44:00+08:00"
1434
- },
1435
- {
1436
- name: "Introduction to Linear Algebra(Strang).pdf",
1437
- size: 36700160,
1438
- is_dir: false,
1439
- modified: "2024-07-29T16:50:00+08:00"
1440
- },
1441
- {
1442
- name: "疑难解答与习题指导.zip",
1443
- size: 9437184,
1444
- is_dir: false,
1445
- modified: "2024-08-05T11:12:00+08:00"
1446
- }
1447
- ],
1448
- "/notes": [{
1449
- name: "线性代数笔记(106页).pdf",
1450
- size: 12582912,
1451
- is_dir: false,
1452
- modified: "2025-06-30T20:31:00+08:00"
1453
- }, {
1454
- name: "期末复习提纲.docx",
1455
- size: 245760,
1456
- is_dir: false,
1457
- modified: "2026-01-05T19:02:00+08:00"
1458
- }],
1459
- "/加密示例(密码 1234)": [{
1460
- name: "期末答案(教师版).pdf",
1461
- size: 2621440,
1462
- is_dir: false,
1463
- modified: "2026-10-20T18:00:00+08:00"
1464
- }, {
1465
- name: "成绩登记表.xlsx",
1466
- size: 48128,
1467
- is_dir: false,
1468
- modified: "2026-10-21T09:12:00+08:00"
1469
- }],
1470
- "/slides": [{
1471
- name: "第一章 行列式.pptx",
1472
- size: 3145728,
1473
- is_dir: false,
1474
- modified: "2026-09-08T13:26:00+08:00"
1475
- }, {
1476
- name: "第二章 矩阵.pptx",
1477
- size: 4194304,
1478
- is_dir: false,
1479
- modified: "2026-09-15T13:30:00+08:00"
1480
- }]
1481
- };
1482
- /** Demo-only folder passwords, so the demo can show the password flow. */
1483
- const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
1484
- class ApiError extends Error {
1485
- code;
1486
- constructor(message, code) {
1487
- super(message);
1488
- this.code = code;
1489
- }
1833
+ return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
1490
1834
  }
1491
- /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1492
- function authHeaders(json) {
1493
- return {
1494
- ...json ? { "Content-Type": "application/json" } : {},
1495
- ...props.token ? { Authorization: props.token } : {}
1496
- };
1835
+ function sortNodes(nodes) {
1836
+ sortTree(nodes, sortKey.value, sortAsc.value);
1497
1837
  }
1498
- async function request(apiPath, body) {
1499
- const response = await fetch(`${baseUrl.value}${apiPath}`, {
1500
- method: body === void 0 ? "GET" : "POST",
1501
- headers: authHeaders(body !== void 0),
1502
- body: body === void 0 ? void 0 : JSON.stringify(body)
1838
+ function focusUnlock(path) {
1839
+ nextTick(() => {
1840
+ rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
1503
1841
  });
1504
- const payload = await response.json().catch(() => null);
1505
- if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
1506
- if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
1507
- return payload.data;
1508
- }
1509
- async function listDir(path, password) {
1510
- if (demo.value) {
1511
- await new Promise((resolve) => setTimeout(resolve, 220));
1512
- const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
1513
- if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
1514
- return DEMO_TREE[path] ?? [];
1515
- }
1516
- return (await request("/api/fs/list", {
1517
- path,
1518
- password,
1519
- page: 1,
1520
- per_page: 0,
1521
- refresh: false
1522
- }))?.content ?? [];
1523
1842
  }
1524
- function absoluteUrl(raw) {
1525
- if (!raw) return "";
1526
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
1527
- const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
1528
- try {
1529
- return new URL(raw, `${root}/`).href;
1530
- } catch {
1531
- return raw;
1532
- }
1843
+ /** Show a folder's same-row input; focus only on user actions, never on page load. */
1844
+ function startPrompt(node, focus) {
1845
+ node.lock ??= newLock();
1846
+ node.lock.prompting = true;
1847
+ if (focus) focusUnlock(node.path);
1533
1848
  }
1534
- async function resolveUrl(node) {
1535
- if (demo.value) return "";
1536
- if (node.raw_url) return absoluteUrl(node.raw_url);
1537
- const cached = downloadCache.get(node.path);
1538
- if (cached) return cached;
1539
- let url = "";
1540
- try {
1541
- url = absoluteUrl((await request("/api/fs/get", {
1542
- path: node.path,
1543
- password: passwordFor(node).password
1544
- }))?.raw_url ?? "");
1545
- } catch {
1546
- url = "";
1547
- }
1548
- if (!url) url = directUrl(node);
1549
- downloadCache.set(node.path, url);
1550
- return url;
1849
+ function cancelUnlock(lock) {
1850
+ Object.assign(lock, {
1851
+ prompting: false,
1852
+ input: "",
1853
+ error: ""
1854
+ });
1551
1855
  }
1552
- /** OpenList's signed `/d/` download link for a file. */
1553
- function directUrl(node) {
1554
- return `${baseUrl.value}/d${encodePath(node.path)}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
1856
+ function unlockErrorText(error) {
1857
+ if (error === "wrong") return t("passwordWrong");
1858
+ if (error === "stale") return t("passwordStale");
1859
+ return "";
1555
1860
  }
1556
- async function loadSiteTitle() {
1557
- if (demo.value) return;
1861
+ /**
1862
+ * Try the typed password with `open`. On success keep it (and remember it in
1863
+ * this tab when asked); on a wrong one clear the input and refocus it.
1864
+ */
1865
+ async function tryUnlock(lock, path, open, failure) {
1866
+ const password = lock.input;
1867
+ if (!password || lock.busy) return false;
1868
+ lock.busy = true;
1558
1869
  try {
1559
- const data = await request("/api/public/settings");
1560
- siteTitle.value = data?.site_title || data?.title || "";
1561
- iframePreviews.value = data?.iframe_previews || "";
1562
- } catch {
1563
- siteTitle.value = "";
1564
- iframePreviews.value = "";
1870
+ await open(password);
1871
+ passwords.accept(path, password, lock.remember);
1872
+ return true;
1873
+ } catch (error) {
1874
+ if (isPasswordError(error)) {
1875
+ Object.assign(lock, {
1876
+ error: "wrong",
1877
+ input: ""
1878
+ });
1879
+ focusUnlock(path);
1880
+ } else showToast(failureMessage(failure, error));
1881
+ return false;
1882
+ } finally {
1883
+ lock.busy = false;
1565
1884
  }
1566
1885
  }
1567
- function sortTree(nodes) {
1568
- nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
1569
- for (const node of nodes) if (node.children.length) sortTree(node.children);
1886
+ async function submitUnlock(node) {
1887
+ if (!node.lock) return;
1888
+ if (await tryUnlock(node.lock, node.path, async (password) => {
1889
+ if (node.unresolved) {
1890
+ const data = await api.value.get(node.path, password);
1891
+ const name = node.name;
1892
+ Object.assign(node, data, {
1893
+ name,
1894
+ unresolved: false
1895
+ });
1896
+ node.zipPath = [node.is_dir ? name : data.name || node.zipPath[0]];
1897
+ }
1898
+ if (node.is_dir) {
1899
+ await fetchChildren(node, password);
1900
+ node.expanded = true;
1901
+ }
1902
+ }, "folderFailed")) node.lock = void 0;
1903
+ }
1904
+ async function submitRootUnlock() {
1905
+ if (!rootLock.value) return;
1906
+ let nodes = [];
1907
+ if (!await tryUnlock(rootLock.value, props.path, async (password) => {
1908
+ nodes = await listRoot(password);
1909
+ }, "loadFailed")) return;
1910
+ roots.value = nodes;
1911
+ rootLock.value = null;
1912
+ selected.value = /* @__PURE__ */ new Set();
1913
+ api.value.clearCache();
1570
1914
  }
1571
- function toNodes(items, parent, depth, password = "", zipParent = []) {
1572
- return items.map((item) => ({
1573
- ...item,
1574
- path: joinPath(parent, item.name),
1575
- depth,
1915
+ /** Forget a remembered password: the folder folds up and asks again next time. */
1916
+ function forgetPassword(node) {
1917
+ passwords.forget(node.path);
1918
+ Object.assign(node, {
1576
1919
  expanded: false,
1577
1920
  loaded: false,
1578
- loading: false,
1579
1921
  children: [],
1580
- password,
1581
- zipPath: [...zipParent, item.name]
1582
- }));
1583
- }
1584
- function setSort(key) {
1585
- if (sortKey.value === key) sortAsc.value = !sortAsc.value;
1586
- else {
1587
- sortKey.value = key;
1588
- sortAsc.value = true;
1589
- }
1590
- resortAll();
1922
+ lock: void 0
1923
+ });
1924
+ selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
1925
+ api.value.clearCache(node.path);
1591
1926
  }
1592
- function resortAll() {
1593
- if (props.sources.length) {
1594
- for (const root of roots.value) if (root.children.length) sortTree(root.children);
1595
- } else sortTree(roots.value);
1927
+ async function fetchChildren(node, password) {
1928
+ node.children = toNodes(await api.value.list(node.path, password), node, password);
1929
+ sortNodes(node.children);
1930
+ node.loaded = true;
1596
1931
  }
1597
- function sourceName(path) {
1598
- const parts = path.split("/").filter(Boolean);
1599
- return parts[parts.length - 1] || path;
1932
+ async function listRoot(password) {
1933
+ const nodes = toNodes(await api.value.list(props.path, password), listedFolder.value, password);
1934
+ sortNodes(nodes);
1935
+ return nodes;
1600
1936
  }
1601
1937
  /**
1602
- * Load a folder's children. A password error marks the folder locked instead of
1938
+ * Load a folder's children. A password error locks the folder instead of
1603
1939
  * toasting; `prompt` also shows its same-row input (and focuses it).
1604
1940
  */
1605
1941
  async function loadChildren(node, prompt = false) {
1606
1942
  if (node.loaded) return true;
1607
- if (node.locked) {
1943
+ if (node.lock) {
1608
1944
  if (prompt) startPrompt(node, prompt === "focus");
1609
1945
  return false;
1610
1946
  }
1611
1947
  node.loading = true;
1612
- const { password, rememberedAt } = passwordFor(node);
1948
+ const { password, rememberedAt } = passwords.resolve(node.path, node.password);
1613
1949
  try {
1614
- node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
1615
- sortTree(node.children);
1616
- node.loaded = true;
1950
+ await fetchChildren(node, password);
1617
1951
  return true;
1618
1952
  } catch (error) {
1619
1953
  if (isPasswordError(error)) {
1620
- node.locked = true;
1621
1954
  node.expanded = false;
1622
- node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1955
+ node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
1623
1956
  if (prompt) startPrompt(node, prompt === "focus");
1624
1957
  } else showToast(failureMessage("folderFailed", error));
1625
1958
  return false;
@@ -1627,63 +1960,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1627
1960
  node.loading = false;
1628
1961
  }
1629
1962
  }
1630
- /** Submit the same-row password for a locked folder (or a locked root of unknown type). */
1631
- async function submitUnlock(node) {
1632
- const password = node.unlockInput ?? "";
1633
- if (!password || node.unlockBusy) return;
1634
- node.unlockBusy = true;
1635
- try {
1636
- if (node.unresolved) {
1637
- const data = await request("/api/fs/get", {
1638
- path: node.path,
1639
- password
1640
- });
1641
- const name = node.name;
1642
- Object.assign(node, data, { name });
1643
- if (node.is_dir) node.zipPath = [name];
1644
- else node.zipPath = [data.name || node.zipPath[0]];
1645
- node.unresolved = false;
1646
- }
1647
- if (node.is_dir) {
1648
- node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, password, node.zipPath);
1649
- sortTree(node.children);
1650
- node.loaded = true;
1651
- node.expanded = true;
1652
- }
1653
- node.password = password;
1654
- typedPasswords.set(node.path, password);
1655
- if (node.unlockRemember && canRemember.value) {
1656
- rememberFolderPassword(passwordScope.value, node.path, password);
1657
- syncRemembered();
1658
- }
1659
- node.locked = false;
1660
- node.prompting = false;
1661
- node.unlockError = "";
1662
- node.unlockInput = "";
1663
- node.unlockRemember = false;
1664
- } catch (error) {
1665
- if (isPasswordError(error)) {
1666
- node.unlockError = "wrong";
1667
- node.unlockInput = "";
1668
- focusUnlock(node.path);
1669
- } else showToast(failureMessage("folderFailed", error));
1670
- } finally {
1671
- node.unlockBusy = false;
1672
- }
1673
- }
1674
- /** Forget a remembered password: the folder folds up and asks again next time. */
1675
- function forgetPassword(node) {
1676
- forgetFolderPassword(passwordScope.value, node.path);
1677
- typedPasswords.delete(node.path);
1678
- syncRemembered();
1679
- node.password = node.configuredPassword || "";
1680
- node.expanded = false;
1681
- node.loaded = false;
1682
- node.children = [];
1683
- node.locked = false;
1684
- selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
1685
- for (const key of [...downloadCache.keys()]) if (isWithin(key, node.path)) downloadCache.delete(key);
1686
- }
1687
1963
  /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1688
1964
  function looksLikeFile(path) {
1689
1965
  const name = sourceName(path);
@@ -1691,44 +1967,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1691
1967
  }
1692
1968
  async function loadSourceRoot(source, expanded) {
1693
1969
  const path = source.path;
1694
- const configured = source.password ?? "";
1970
+ const own = source.password ?? "";
1695
1971
  let item = {
1696
1972
  name: sourceName(path),
1697
1973
  size: 0,
1698
1974
  is_dir: !looksLikeFile(path),
1699
1975
  modified: ""
1700
1976
  };
1701
- let locked = false;
1702
- let unlockError = "";
1703
- if (!demo.value) {
1704
- const { password, rememberedAt } = configured ? {
1705
- password: configured,
1706
- rememberedAt: void 0
1707
- } : (() => {
1708
- const stored = storedPassword(path);
1709
- return stored ? {
1710
- password: stored.password,
1711
- rememberedAt: stored.path
1712
- } : {
1713
- password: props.password,
1714
- rememberedAt: void 0
1715
- };
1716
- })();
1717
- try {
1718
- const data = await request("/api/fs/get", {
1719
- path,
1720
- password
1721
- });
1722
- item = {
1723
- ...item,
1724
- ...data
1725
- };
1726
- } catch (error) {
1727
- if (isPasswordError(error)) {
1728
- locked = true;
1729
- unlockError = dropStale(rememberedAt) ? "stale" : "";
1730
- }
1731
- }
1977
+ let lock;
1978
+ const { password, rememberedAt } = passwords.resolve(path, own);
1979
+ try {
1980
+ item = {
1981
+ ...item,
1982
+ ...await api.value.get(path, password)
1983
+ };
1984
+ } catch (error) {
1985
+ if (isPasswordError(error)) lock = newLock(expanded, passwords.dropStale(rememberedAt) ? "stale" : "");
1732
1986
  }
1733
1987
  const name = source.title || item.name || sourceName(path);
1734
1988
  return {
@@ -1736,17 +1990,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1736
1990
  name,
1737
1991
  path,
1738
1992
  depth: 0,
1739
- expanded,
1993
+ expanded: expanded && !lock,
1740
1994
  loaded: false,
1741
1995
  loading: false,
1742
1996
  children: [],
1743
- password: configured,
1744
- configuredPassword: configured,
1997
+ password: own,
1745
1998
  isRoot: true,
1746
- locked,
1747
- unresolved: locked,
1748
- unlockError,
1749
- prompting: locked && expanded,
1999
+ lock,
2000
+ unresolved: !!lock,
1750
2001
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
1751
2002
  };
1752
2003
  }
@@ -1765,20 +2016,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1765
2016
  let nodes;
1766
2017
  if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1767
2018
  else {
1768
- const { password, rememberedAt } = rootPasswordFor();
1769
- let items;
2019
+ const { password, rememberedAt } = passwords.resolve(props.path);
1770
2020
  try {
1771
- items = await listDir(props.path, password);
2021
+ nodes = await listRoot(password);
1772
2022
  } catch (error) {
1773
- if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1774
- throw error;
2023
+ throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
1775
2024
  }
1776
- nodes = toNodes(items, props.path, 0, rootPassword.value);
1777
- sortTree(nodes);
1778
2025
  }
1779
2026
  const open = async (list) => {
1780
2027
  for (const node of list) {
1781
- if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
2028
+ const wanted = node.expanded || expandedPaths?.has(node.path);
2029
+ if (!node.is_dir || node.lock || !wanted) continue;
1782
2030
  node.expanded = await loadChildren(node, "show");
1783
2031
  if (node.expanded && expandedPaths) await open(node.children);
1784
2032
  }
@@ -1786,6 +2034,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1786
2034
  await open(nodes);
1787
2035
  return nodes;
1788
2036
  }
2037
+ function lockRoot(error, fallback) {
2038
+ roots.value = [];
2039
+ selected.value = /* @__PURE__ */ new Set();
2040
+ rootLock.value = newLock(true, passwords.dropStale(error.rememberedAt) ? "stale" : fallback);
2041
+ }
1789
2042
  /** First load, or a retry after an error: shows the loading state. */
1790
2043
  async function loadRoot() {
1791
2044
  refreshSeq += 1;
@@ -1794,33 +2047,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1794
2047
  errorMsg.value = "";
1795
2048
  try {
1796
2049
  roots.value = await buildTree();
1797
- rootLock.active = false;
2050
+ shown.clear();
2051
+ rootLock.value = null;
1798
2052
  selected.value = /* @__PURE__ */ new Set();
1799
- downloadCache.clear();
2053
+ api.value.clearCache();
1800
2054
  } catch (error) {
1801
- if (error instanceof RootLockedError) {
1802
- roots.value = [];
1803
- rootLock.active = true;
1804
- rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
1805
- } else errorMsg.value = errorText(error);
2055
+ if (error instanceof RootLockedError) lockRoot(error, "");
2056
+ else errorMsg.value = errorText(error);
1806
2057
  } finally {
1807
2058
  loadingRoot.value = false;
1808
2059
  }
1809
2060
  }
1810
- function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1811
- for (const node of nodes) if (node.is_dir && node.expanded) {
1812
- out.add(node.path);
1813
- expandedPathsOf(node.children, out);
1814
- }
1815
- return out;
1816
- }
1817
2061
  /**
1818
2062
  * Refresh keeps the old rows in place (dimmed) until the new tree is ready, then
1819
2063
  * swaps it in. Open folders stay open; selected files that still exist stay selected.
1820
2064
  */
1821
2065
  async function refresh() {
1822
2066
  if (refreshing.value || loadingRoot.value) return;
1823
- if (!roots.value.length || errorMsg.value || rootLock.active) {
2067
+ if (!roots.value.length || errorMsg.value || rootLock.value) {
1824
2068
  await loadRoot();
1825
2069
  return;
1826
2070
  }
@@ -1832,54 +2076,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1832
2076
  roots.value = fresh;
1833
2077
  const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
1834
2078
  selected.value = new Set([...selected.value].filter((path) => existing.has(path)));
1835
- downloadCache.clear();
2079
+ api.value.clearCache();
1836
2080
  } catch (error) {
1837
2081
  if (seq !== refreshSeq) return;
1838
- if (error instanceof RootLockedError) {
1839
- roots.value = [];
1840
- rootLock.active = true;
1841
- rootLock.error = dropStale(error.rememberedAt) ? "stale" : "wrong";
1842
- selected.value = /* @__PURE__ */ new Set();
1843
- } else showToast(failureMessage("refreshFailed", error));
2082
+ if (error instanceof RootLockedError) lockRoot(error, "wrong");
2083
+ else showToast(failureMessage("refreshFailed", error));
1844
2084
  } finally {
1845
2085
  if (seq === refreshSeq) refreshing.value = false;
1846
2086
  }
1847
2087
  }
1848
- /** Single-path mode: unlock the listed folder itself. */
1849
- async function submitRootUnlock() {
1850
- const password = rootLock.input;
1851
- if (!password || rootLock.busy) return;
1852
- rootLock.busy = true;
1853
- try {
1854
- const items = await listDir(props.path, password);
1855
- rootPassword.value = password;
1856
- if (rootLock.remember && canRemember.value) {
1857
- rememberFolderPassword(passwordScope.value, props.path, password);
1858
- syncRemembered();
1859
- }
1860
- const nodes = toNodes(items, props.path, 0, password);
1861
- sortTree(nodes);
1862
- roots.value = nodes;
1863
- Object.assign(rootLock, {
1864
- active: false,
1865
- input: "",
1866
- error: "",
1867
- remember: false
1868
- });
1869
- selected.value = /* @__PURE__ */ new Set();
1870
- downloadCache.clear();
1871
- } catch (error) {
1872
- if (isPasswordError(error)) {
1873
- rootLock.error = "wrong";
1874
- rootLock.input = "";
1875
- focusUnlock(props.path);
1876
- } else showToast(failureMessage("loadFailed", error));
1877
- } finally {
1878
- rootLock.busy = false;
1879
- }
1880
- }
1881
2088
  async function toggleExpand(node) {
1882
- if (node.locked) {
2089
+ if (node.lock) {
1883
2090
  startPrompt(node, true);
1884
2091
  return;
1885
2092
  }
@@ -1888,35 +2095,92 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1888
2095
  node.expanded = false;
1889
2096
  return;
1890
2097
  }
1891
- if (!await loadChildren(node, "focus")) return;
1892
- node.expanded = true;
2098
+ node.expanded = await loadChildren(node, "focus");
2099
+ }
2100
+ function applySort(key, ascending) {
2101
+ sortKey.value = key;
2102
+ sortAsc.value = ascending;
2103
+ if (props.sources.length) roots.value.forEach((root) => sortNodes(root.children));
2104
+ else sortNodes(roots.value);
2105
+ }
2106
+ function setSort(key) {
2107
+ applySort(key, sortKey.value === key ? !sortAsc.value : true);
2108
+ }
2109
+ /** Compact sort control for narrow containers, where header cells are hidden. */
2110
+ const sortOptions = computed(() => [
2111
+ "name",
2112
+ "size",
2113
+ "modified"
2114
+ ].flatMap((key) => {
2115
+ const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
2116
+ return [{
2117
+ value: `${key}:asc`,
2118
+ label: `${label} ↑`
2119
+ }, {
2120
+ value: `${key}:desc`,
2121
+ label: `${label} ↓`
2122
+ }];
2123
+ }));
2124
+ const sortValue = computed({
2125
+ get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
2126
+ set: (value) => {
2127
+ const [key, direction] = value.split(":");
2128
+ applySort(key, direction !== "desc");
2129
+ }
2130
+ });
2131
+ /** A “show more” row closing a folder whose children are cut at `pageSize`. */
2132
+ const shown = reactive(/* @__PURE__ */ new Map());
2133
+ function limitOf(key) {
2134
+ const size = Math.max(0, Math.floor(props.pageSize ?? 200));
2135
+ return size ? shown.get(key) ?? size : Number.POSITIVE_INFINITY;
2136
+ }
2137
+ function showMore(row, all) {
2138
+ const size = Math.max(1, Math.floor(props.pageSize ?? 200));
2139
+ shown.set(row.key, all ? Number.POSITIVE_INFINITY : limitOf(row.key) + size);
1893
2140
  }
1894
- const visible = computed(() => {
2141
+ function isMore(row) {
2142
+ return "more" in row;
2143
+ }
2144
+ const rows = computed(() => {
1895
2145
  const out = [];
1896
2146
  const query = keyword.value.trim().toLowerCase();
1897
2147
  const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
1898
- const walk = (nodes) => {
1899
- for (const node of nodes) {
1900
- if (!matches(node)) continue;
2148
+ const walk = (nodes, key, depth) => {
2149
+ const list = nodes.filter(matches);
2150
+ const limit = query ? list.length : Math.min(list.length, limitOf(key));
2151
+ for (const node of list.slice(0, limit)) {
1901
2152
  out.push(node);
1902
- if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children);
2153
+ if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children, node.path, node.depth + 1);
1903
2154
  }
2155
+ if (limit < list.length) out.push({
2156
+ more: true,
2157
+ key,
2158
+ depth,
2159
+ remaining: list.length - limit
2160
+ });
1904
2161
  };
1905
- walk(roots.value);
2162
+ walk(roots.value, props.sources.length ? "" : props.path, 0);
1906
2163
  return out;
1907
2164
  });
1908
- function descendantFiles(node) {
1909
- if (!node.is_dir) return [node];
1910
- return node.children.flatMap(descendantFiles);
2165
+ const visible = computed(() => rows.value.filter((row) => !isMore(row)));
2166
+ const stats = computed(() => folderStats(roots.value, selected.value));
2167
+ const noStats = {
2168
+ files: 0,
2169
+ bytes: 0,
2170
+ selected: 0
2171
+ };
2172
+ function statsOf(node) {
2173
+ return stats.value.get(node.path) ?? noStats;
1911
2174
  }
1912
2175
  function isChecked(node) {
1913
- const files = descendantFiles(node);
1914
- return files.length > 0 && files.every((file) => selected.value.has(file.path));
2176
+ if (!node.is_dir) return selected.value.has(node.path);
2177
+ const { files, selected: hit } = statsOf(node);
2178
+ return files > 0 && hit === files;
1915
2179
  }
1916
2180
  function isIndeterminate(node) {
1917
- const files = descendantFiles(node);
1918
- const hit = files.filter((file) => selected.value.has(file.path)).length;
1919
- return hit > 0 && hit < files.length;
2181
+ if (!node.is_dir) return false;
2182
+ const { files, selected: hit } = statsOf(node);
2183
+ return hit > 0 && hit < files;
1920
2184
  }
1921
2185
  /**
1922
2186
  * Load a directory and all of its sub-directories. Stops at the first folder
@@ -1924,7 +2188,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1924
2188
  */
1925
2189
  async function loadTree(node) {
1926
2190
  if (!node.is_dir) return { ok: true };
1927
- if (!await loadChildren(node)) return node.locked ? {
2191
+ if (!await loadChildren(node)) return node.lock ? {
1928
2192
  ok: false,
1929
2193
  locked: node
1930
2194
  } : { ok: false };
@@ -1942,12 +2206,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1942
2206
  const input = event?.target;
1943
2207
  if (node.is_dir && !isChecked(node)) {
1944
2208
  const result = await loadTree(node);
1945
- if (result.ok) node.expanded = true;
1946
- else {
2209
+ if (!result.ok) {
1947
2210
  if (result.locked) startPrompt(result.locked, true);
1948
2211
  if (input) input.checked = isChecked(node);
1949
2212
  return;
1950
2213
  }
2214
+ node.expanded = true;
1951
2215
  }
1952
2216
  const files = descendantFiles(node);
1953
2217
  if (!files.length) {
@@ -1955,67 +2219,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1955
2219
  return;
1956
2220
  }
1957
2221
  const next = new Set(selected.value);
1958
- if (files.every((file) => next.has(file.path))) files.forEach((file) => next.delete(file.path));
1959
- else files.forEach((file) => next.add(file.path));
2222
+ const adding = files.some((file) => !next.has(file.path));
2223
+ for (const file of files) if (adding) next.add(file.path);
2224
+ else next.delete(file.path);
1960
2225
  selected.value = next;
1961
2226
  }
1962
2227
  const allFiles = computed(() => roots.value.flatMap(descendantFiles));
1963
- const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
1964
- const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
1965
2228
  const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
1966
- const selectedBytes = computed(() => totalSize(selectedFiles.value));
1967
- const loadedBytes = computed(() => totalSize(allFiles.value));
1968
- function clearSelection() {
1969
- selected.value = /* @__PURE__ */ new Set();
1970
- }
1971
- function folderFiles(node) {
1972
- return node.is_dir && node.loaded ? descendantFiles(node) : [];
1973
- }
1974
- /** Compact sort control for narrow containers, where header cells are hidden. */
1975
- const sortOptions = computed(() => [
1976
- "name",
1977
- "size",
1978
- "modified"
1979
- ].flatMap((key) => {
1980
- const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
1981
- return [{
1982
- value: `${key}:asc`,
1983
- label: `${label} ↑`
1984
- }, {
1985
- value: `${key}:desc`,
1986
- label: `${label} ↓`
1987
- }];
1988
- }));
1989
- const sortValue = computed({
1990
- get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
1991
- set: (value) => {
1992
- const [key, direction] = value.split(":");
1993
- sortKey.value = key;
1994
- sortAsc.value = direction !== "desc";
1995
- resortAll();
1996
- }
1997
- });
2229
+ const allChecked = computed(() => allFiles.value.length > 0 && selectedFiles.value.length === allFiles.value.length);
2230
+ const someChecked = computed(() => !allChecked.value && selectedFiles.value.length > 0);
1998
2231
  function toggleCheckAll() {
1999
2232
  selected.value = allChecked.value ? /* @__PURE__ */ new Set() : new Set(allFiles.value.map((file) => file.path));
2000
2233
  }
2001
- function triggerUrl(url) {
2002
- const anchor = document.createElement("a");
2003
- anchor.href = url;
2004
- anchor.target = "_blank";
2005
- anchor.rel = "noopener";
2006
- document.body.appendChild(anchor);
2007
- anchor.click();
2008
- anchor.remove();
2009
- }
2010
- function triggerBlob(blob, name) {
2011
- const href = URL.createObjectURL(blob);
2012
- const anchor = document.createElement("a");
2013
- anchor.href = href;
2014
- anchor.download = name;
2015
- document.body.appendChild(anchor);
2016
- anchor.click();
2017
- anchor.remove();
2018
- URL.revokeObjectURL(href);
2234
+ function clearSelection() {
2235
+ selected.value = /* @__PURE__ */ new Set();
2236
+ }
2237
+ function fileUrlOf(node) {
2238
+ return api.value.fileUrl(node, passwords.resolve(node.path, node.password).password);
2019
2239
  }
2020
2240
  async function download(node) {
2021
2241
  if (node.is_dir) return;
@@ -2024,11 +2244,109 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2024
2244
  return;
2025
2245
  }
2026
2246
  try {
2027
- triggerUrl(await resolveUrl(node));
2247
+ openUrl(await fileUrlOf(node));
2028
2248
  } catch (error) {
2029
2249
  showToast(t("downloadFailed", { message: errorText(error) }));
2030
2250
  }
2031
2251
  }
2252
+ function zipLimits() {
2253
+ const touch = typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches;
2254
+ const bytes = (value, fallback) => {
2255
+ const mb = value ?? fallback;
2256
+ return mb > 0 ? mb * MB : Number.POSITIVE_INFINITY;
2257
+ };
2258
+ return {
2259
+ warn: bytes(props.zipWarnSize, touch ? 200 : 500),
2260
+ max: bytes(props.zipMaxSize, touch ? 1024 : 2048)
2261
+ };
2262
+ }
2263
+ const zipNotice = ref(null);
2264
+ /** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
2265
+ const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
2266
+ function zipName() {
2267
+ return zipFileName(props.title || t("defaultZip"));
2268
+ }
2269
+ /**
2270
+ * Chrome / Edge stream into a file the user picks (no size limit). Other
2271
+ * browsers build the ZIP in memory, so large selections ask first or are refused.
2272
+ */
2273
+ async function batchDownload() {
2274
+ const files = selectedFiles.value;
2275
+ if (!files.length || batchBusy.value) return;
2276
+ if (demo.value) {
2277
+ showToast(t("demoBatch", { count: files.length }));
2278
+ return;
2279
+ }
2280
+ if (canStreamToDisk()) {
2281
+ let handle;
2282
+ try {
2283
+ handle = await pickZipFile(zipName());
2284
+ } catch (error) {
2285
+ showToast(t("batchFailed", { message: errorText(error) }));
2286
+ return;
2287
+ }
2288
+ if (handle) await runZip(files, (options) => zipToFile(handle, files, options), handle.name);
2289
+ return;
2290
+ }
2291
+ const size = totalSize(files);
2292
+ const { warn, max } = zipLimits();
2293
+ if (size > max) zipNotice.value = {
2294
+ kind: "block",
2295
+ size,
2296
+ limit: max,
2297
+ files
2298
+ };
2299
+ else if (size > warn) zipNotice.value = {
2300
+ kind: "warn",
2301
+ size,
2302
+ limit: warn,
2303
+ files
2304
+ };
2305
+ else await zipInMemory(files);
2306
+ }
2307
+ async function zipInMemory(files) {
2308
+ const name = zipName();
2309
+ await runZip(files, async (options) => saveBlob(await zipToBlob(files, options), name), name);
2310
+ }
2311
+ function confirmZip() {
2312
+ const notice = zipNotice.value;
2313
+ zipNotice.value = null;
2314
+ if (notice?.kind === "warn") zipInMemory(notice.files);
2315
+ }
2316
+ async function runZip(files, run, name) {
2317
+ const controller = new AbortController();
2318
+ zipAbort = controller;
2319
+ batchBusy.value = true;
2320
+ zipProgress.value = null;
2321
+ try {
2322
+ await run({
2323
+ urlOf: fileUrlOf,
2324
+ signal: controller.signal,
2325
+ onProgress: (progress) => zipProgress.value = progress
2326
+ });
2327
+ showToast(t("batchDone", {
2328
+ name,
2329
+ count: files.length,
2330
+ size: formatSize(totalSize(files))
2331
+ }));
2332
+ selected.value = /* @__PURE__ */ new Set();
2333
+ } catch (error) {
2334
+ showToast(controller.signal.aborted ? t("batchCanceled") : t("batchFailed", { message: errorText(error) }));
2335
+ } finally {
2336
+ zipAbort = null;
2337
+ batchBusy.value = false;
2338
+ zipProgress.value = null;
2339
+ }
2340
+ }
2341
+ function cancelZip() {
2342
+ zipAbort?.abort();
2343
+ }
2344
+ const zipPercent = computed(() => {
2345
+ const progress = zipProgress.value;
2346
+ if (!progress) return 0;
2347
+ const ratio = progress.totalBytes ? progress.bytes / progress.totalBytes : progress.totalFiles ? progress.files / progress.totalFiles : 0;
2348
+ return Math.min(100, Math.floor(ratio * 100));
2349
+ });
2032
2350
  /** Only types the dialog can actually render get a preview button. */
2033
2351
  function canPreview(node) {
2034
2352
  return !node.is_dir && previewKind(node.name) !== "none";
@@ -2052,17 +2370,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2052
2370
  if (demo.value || !canPreview(node)) return;
2053
2371
  previewResolving.value = true;
2054
2372
  try {
2055
- const url = await resolveUrl(node);
2373
+ const url = await fileUrlOf(node);
2056
2374
  if (seq !== previewSeq) return;
2057
2375
  previewSrc.value = url;
2058
- if (previewKind(node.name) === "pdf") {
2059
- const viewer = iframePreviewsFor(node.name, iframePreviews.value)[0];
2060
- if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
2061
- name: node.name,
2062
- rawUrl: url,
2063
- downloadUrl: directUrl(node)
2064
- });
2065
- }
2376
+ const viewer = previewKind(node.name) === "pdf" ? iframePreviewsFor(node.name, iframePreviews.value)[0] : void 0;
2377
+ if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
2378
+ name: node.name,
2379
+ rawUrl: url,
2380
+ downloadUrl: api.value.directUrl(node)
2381
+ });
2066
2382
  } catch (error) {
2067
2383
  if (seq === previewSeq) previewError.value = errorText(error);
2068
2384
  } finally {
@@ -2078,49 +2394,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2078
2394
  previewSeq += 1;
2079
2395
  previewNode.value = null;
2080
2396
  }
2081
- async function batchDownload() {
2082
- const files = allFiles.value.filter((file) => selected.value.has(file.path));
2083
- if (!files.length) return;
2084
- if (demo.value) {
2085
- showToast(t("demoBatch", { count: files.length }));
2086
- return;
2087
- }
2088
- batchBusy.value = true;
2089
- batchProgress.value = 0;
2090
- try {
2091
- const entries = {};
2092
- const usedNames = /* @__PURE__ */ new Set();
2093
- const queue = [...files];
2094
- let done = 0;
2095
- const worker = async () => {
2096
- while (queue.length) {
2097
- const file = queue.shift();
2098
- if (!file) break;
2099
- const response = await fetch(await resolveUrl(file));
2100
- if (!response.ok) throw new Error(`${file.name}:HTTP ${response.status}`);
2101
- entries[uniqueEntryName(zipEntryName(file.zipPath), usedNames)] = new Uint8Array(await response.arrayBuffer());
2102
- done += 1;
2103
- batchProgress.value = Math.round(done / files.length * 90);
2104
- }
2105
- };
2106
- await Promise.all(Array.from({ length: Math.min(3, files.length) }, () => worker()));
2107
- const { zip } = await import("fflate");
2108
- const data = await new Promise((resolve, reject) => {
2109
- zip(entries, (error, output) => error ? reject(error) : resolve(output));
2110
- });
2111
- batchProgress.value = 100;
2112
- triggerBlob(new Blob([data], { type: "application/zip" }), `${(props.title || t("defaultZip")).replace(/[\\/:*?"<>|]/g, "_")}.zip`);
2113
- selected.value = /* @__PURE__ */ new Set();
2114
- } catch (error) {
2115
- showToast(t("batchFailed", { message: errorText(error) }));
2116
- } finally {
2117
- batchBusy.value = false;
2118
- }
2119
- }
2120
2397
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
2398
+ async function loadSettings() {
2399
+ const settings = await api.value.settings();
2400
+ siteTitle.value = settings.title;
2401
+ iframePreviews.value = settings.iframePreviews;
2402
+ }
2121
2403
  const stickyTop = ref(0);
2122
2404
  function measureStickyTop() {
2123
- if (typeof document === "undefined") return;
2124
2405
  let top = 0;
2125
2406
  for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
2126
2407
  const style = getComputedStyle(el);
@@ -2130,13 +2411,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2130
2411
  stickyTop.value = top;
2131
2412
  }
2132
2413
  onMounted(() => {
2133
- syncRemembered();
2414
+ passwords.sync();
2134
2415
  loadRoot();
2135
- loadSiteTitle();
2416
+ loadSettings();
2136
2417
  measureStickyTop();
2137
2418
  window.addEventListener("resize", measureStickyTop, { passive: true });
2138
2419
  });
2139
2420
  onBeforeUnmount(() => {
2421
+ zipAbort?.abort();
2140
2422
  window.removeEventListener("resize", measureStickyTop);
2141
2423
  clearTimeout(toastTimer);
2142
2424
  });
@@ -2152,490 +2434,367 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2152
2434
  "aria-busy": refreshing.value || loadingRoot.value
2153
2435
  }, [
2154
2436
  createElementVNode("div", _hoisted_2, [
2155
- selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_5, toDisplayString(t("totalSize", { size: formatTotal(selectedBytes.value) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("span", _hoisted_7, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
2156
- selected.value.size ? (openBlock(), createElementBlock("button", {
2157
- key: 2,
2437
+ batchBusy.value ? (openBlock(), createElementBlock("div", _hoisted_3, [
2438
+ createElementVNode("span", _hoisted_4, toDisplayString(t("batching", { percent: zipPercent.value })), 1),
2439
+ zipProgress.value ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(t("batchProgress", {
2440
+ done: unref(formatSize)(zipProgress.value.bytes),
2441
+ total: unref(formatSize)(zipProgress.value.totalBytes),
2442
+ files: zipProgress.value.files,
2443
+ count: zipProgress.value.totalFiles
2444
+ })), 1)) : createCommentVNode("v-if", true),
2445
+ createElementVNode("span", {
2446
+ class: "fl-zip-bar",
2447
+ style: normalizeStyle({ width: `${zipPercent.value}%` }),
2448
+ "aria-hidden": "true"
2449
+ }, null, 4)
2450
+ ])) : selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("span", _hoisted_7, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_8, toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(selectedFiles.value)) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_9, [createElementVNode("span", _hoisted_10, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_11, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
2451
+ selected.value.size && !batchBusy.value ? (openBlock(), createElementBlock("button", {
2452
+ key: 3,
2158
2453
  class: "fl-clear",
2159
2454
  type: "button",
2160
2455
  "aria-label": t("clearSelection"),
2161
2456
  title: t("clearSelection"),
2162
2457
  onClick: clearSelection
2163
- }, [_cache[10] || (_cache[10] = createElementVNode("svg", {
2458
+ }, [createVNode(_sfc_main$2, {
2164
2459
  class: "fl-clear-icon",
2165
- viewBox: "0 0 24 24",
2166
- width: "18",
2167
- height: "18",
2168
- fill: "none",
2169
- stroke: "currentColor",
2170
- "stroke-width": "2.2",
2171
- "stroke-linecap": "round",
2172
- "aria-hidden": "true"
2173
- }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)), createElementVNode("span", _hoisted_10, toDisplayString(t("clear")), 1)], 8, _hoisted_9)) : createCommentVNode("v-if", true),
2174
- createElementVNode("div", _hoisted_11, [createElementVNode("label", _hoisted_12, [_cache[11] || (_cache[11] = createElementVNode("svg", {
2175
- viewBox: "0 0 24 24",
2176
- width: "15",
2177
- height: "15",
2178
- fill: "none",
2179
- stroke: "currentColor",
2180
- "stroke-width": "2",
2181
- "stroke-linecap": "round",
2182
- "aria-hidden": "true"
2183
- }, [createElementVNode("circle", {
2184
- cx: "11",
2185
- cy: "11",
2186
- r: "7"
2187
- }), createElementVNode("path", { d: "m20 20-3.5-3.5" })], -1)), withDirectives(createElementVNode("input", {
2460
+ name: "close",
2461
+ size: 18
2462
+ }), createElementVNode("span", _hoisted_13, toDisplayString(t("clear")), 1)], 8, _hoisted_12)) : createCommentVNode("v-if", true),
2463
+ createElementVNode("div", _hoisted_14, [createElementVNode("label", _hoisted_15, [createVNode(_sfc_main$2, {
2464
+ name: "search",
2465
+ size: 15
2466
+ }), withDirectives(createElementVNode("input", {
2188
2467
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
2189
2468
  type: "text",
2190
2469
  placeholder: t("filter"),
2191
2470
  "aria-label": t("filter")
2192
- }, null, 8, _hoisted_13), [[vModelText, keyword.value]])]), createElementVNode("button", {
2471
+ }, null, 8, _hoisted_16), [[vModelText, keyword.value]])]), createElementVNode("button", {
2193
2472
  class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
2194
2473
  type: "button",
2195
2474
  disabled: refreshing.value || loadingRoot.value,
2196
2475
  "aria-label": refreshing.value ? t("refreshing") : t("refresh"),
2197
2476
  title: refreshing.value ? t("refreshing") : t("refresh"),
2198
2477
  onClick: refresh
2199
- }, [..._cache[12] || (_cache[12] = [createElementVNode("svg", {
2200
- viewBox: "0 0 24 24",
2201
- width: "15",
2202
- height: "15",
2203
- fill: "none",
2204
- stroke: "currentColor",
2205
- "stroke-width": "2",
2206
- "stroke-linecap": "round",
2207
- "stroke-linejoin": "round",
2208
- "aria-hidden": "true"
2209
- }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 10, _hoisted_14)]),
2210
- selected.value.size ? (openBlock(), createElementBlock("button", {
2211
- key: 3,
2478
+ }, [createVNode(_sfc_main$2, {
2479
+ name: "refresh",
2480
+ size: 15
2481
+ })], 10, _hoisted_17)]),
2482
+ batchBusy.value ? (openBlock(), createElementBlock("button", {
2483
+ key: 4,
2484
+ class: "fl-btn fl-zip-cancel",
2485
+ type: "button",
2486
+ onClick: cancelZip
2487
+ }, toDisplayString(t("cancel")), 1)) : selected.value.size ? (openBlock(), createElementBlock("button", {
2488
+ key: 5,
2212
2489
  class: "fl-btn fl-btn-primary",
2213
2490
  type: "button",
2214
- disabled: batchBusy.value,
2215
2491
  onClick: batchDownload
2216
- }, [_cache[13] || (_cache[13] = createElementVNode("svg", {
2217
- viewBox: "0 0 24 24",
2218
- width: "15",
2219
- height: "15",
2220
- fill: "none",
2221
- stroke: "currentColor",
2222
- "stroke-width": "2.2",
2223
- "stroke-linecap": "round",
2224
- "stroke-linejoin": "round",
2225
- "aria-hidden": "true"
2226
- }, [
2227
- createElementVNode("path", { d: "M12 3v12" }),
2228
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
2229
- createElementVNode("path", { d: "M5 21h14" })
2230
- ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_15)) : createCommentVNode("v-if", true)
2492
+ }, [createVNode(_sfc_main$2, {
2493
+ name: "download",
2494
+ size: 15
2495
+ }), createTextVNode(" " + toDisplayString(t("batch")), 1)])) : createCommentVNode("v-if", true)
2231
2496
  ]),
2232
- createElementVNode("div", _hoisted_16, [
2233
- refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_17)) : createCommentVNode("v-if", true),
2234
- createElementVNode("label", _hoisted_18, [createElementVNode("input", {
2497
+ createElementVNode("div", _hoisted_18, [
2498
+ refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_19)) : createCommentVNode("v-if", true),
2499
+ createElementVNode("label", _hoisted_20, [createElementVNode("input", {
2235
2500
  type: "checkbox",
2236
2501
  "aria-label": t("selectAll"),
2502
+ disabled: batchBusy.value,
2237
2503
  checked: allChecked.value,
2238
2504
  ".indeterminate": someChecked.value,
2239
2505
  onChange: toggleCheckAll
2240
- }, null, 40, _hoisted_19)]),
2241
- createElementVNode("span", _hoisted_20, [createElementVNode("button", {
2242
- class: "fl-sort-btn",
2243
- type: "button",
2244
- "aria-sort": sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0,
2245
- onClick: _cache[1] || (_cache[1] = ($event) => setSort("name"))
2246
- }, [createTextVNode(toDisplayString(t("columnName")), 1), createElementVNode("span", _hoisted_22, toDisplayString(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_21)]),
2247
- createElementVNode("span", _hoisted_23, [createElementVNode("button", {
2248
- class: "fl-sort-btn",
2249
- type: "button",
2250
- onClick: _cache[2] || (_cache[2] = ($event) => setSort("size"))
2251
- }, [createTextVNode(toDisplayString(t("columnSize")), 1), createElementVNode("span", _hoisted_24, toDisplayString(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
2252
- createElementVNode("span", _hoisted_25, [createElementVNode("button", {
2253
- class: "fl-sort-btn",
2254
- type: "button",
2255
- onClick: _cache[3] || (_cache[3] = ($event) => setSort("modified"))
2256
- }, [createTextVNode(toDisplayString(t("columnDate")), 1), createElementVNode("span", _hoisted_26, toDisplayString(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
2257
- createElementVNode("span", _hoisted_27, [createElementVNode("label", _hoisted_28, [
2506
+ }, null, 40, _hoisted_21)]),
2507
+ (openBlock(), createElementBlock(Fragment, null, renderList([
2508
+ "name",
2509
+ "size",
2510
+ "modified"
2511
+ ], (column) => {
2512
+ return createElementVNode("span", {
2513
+ key: column,
2514
+ class: normalizeClass(["fl-cell", `fl-cell-${column === "modified" ? "date" : column}`])
2515
+ }, [createElementVNode("button", {
2516
+ class: "fl-sort-btn",
2517
+ type: "button",
2518
+ "aria-sort": sortKey.value === column ? sortAsc.value ? "ascending" : "descending" : void 0,
2519
+ onClick: ($event) => setSort(column)
2520
+ }, [createTextVNode(toDisplayString(t(column === "name" ? "columnName" : column === "size" ? "columnSize" : "columnDate")), 1), createElementVNode("span", _hoisted_23, toDisplayString(sortKey.value === column ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_22)], 2);
2521
+ }), 64)),
2522
+ createElementVNode("span", _hoisted_24, [createElementVNode("label", _hoisted_25, [
2258
2523
  createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
2259
- createElementVNode("span", _hoisted_29, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
2524
+ createElementVNode("span", _hoisted_26, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
2260
2525
  withDirectives(createElementVNode("select", {
2261
- "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => sortValue.value = $event),
2526
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => sortValue.value = $event),
2262
2527
  "aria-label": t("sortBy")
2263
2528
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
2264
2529
  return openBlock(), createElementBlock("option", {
2265
2530
  key: option.value,
2266
2531
  value: option.value
2267
- }, toDisplayString(option.label), 9, _hoisted_31);
2268
- }), 128))], 8, _hoisted_30), [[vModelSelect, sortValue.value]])
2532
+ }, toDisplayString(option.label), 9, _hoisted_28);
2533
+ }), 128))], 8, _hoisted_27), [[vModelSelect, sortValue.value]])
2269
2534
  ])])
2270
2535
  ]),
2271
- loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : rootLock.active ? (openBlock(), createElementBlock("form", {
2536
+ loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_29, toDisplayString(t("loading")), 1)) : rootLock.value ? (openBlock(), createElementBlock("form", {
2272
2537
  key: 1,
2273
- class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.error }]),
2538
+ class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.value.error }]),
2274
2539
  onSubmit: withModifiers(submitRootUnlock, ["prevent"])
2275
2540
  }, [
2276
- _cache[15] || (_cache[15] = createElementVNode("svg", {
2541
+ createVNode(_sfc_main$2, {
2277
2542
  class: "fl-unlock-lock",
2278
- viewBox: "0 0 24 24",
2279
- width: "16",
2280
- height: "16",
2281
- fill: "none",
2282
- stroke: "currentColor",
2283
- "stroke-width": "2.2",
2284
- "stroke-linecap": "round",
2285
- "stroke-linejoin": "round",
2286
- "aria-hidden": "true"
2287
- }, [createElementVNode("rect", {
2288
- x: "5",
2289
- y: "11",
2290
- width: "14",
2291
- height: "10",
2292
- rx: "2"
2293
- }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
2543
+ name: "lock"
2544
+ }),
2294
2545
  createElementVNode("label", {
2295
2546
  class: "fl-unlock-label",
2296
- for: `fl-root-unlock-${__props.path}`
2297
- }, toDisplayString(t("passwordPrompt")), 9, _hoisted_33),
2298
- rootLock.error ? (openBlock(), createElementBlock("span", _hoisted_34, toDisplayString(unlockErrorText(rootLock.error)), 1)) : createCommentVNode("v-if", true),
2299
- createElementVNode("span", _hoisted_35, [
2547
+ for: `${unref(uid)}-root`
2548
+ }, toDisplayString(t("passwordPrompt")), 9, _hoisted_30),
2549
+ rootLock.value.error ? (openBlock(), createElementBlock("span", {
2550
+ key: 0,
2551
+ id: `${unref(uid)}-root-error`,
2552
+ class: "fl-unlock-message",
2553
+ role: "alert"
2554
+ }, toDisplayString(unlockErrorText(rootLock.value.error)), 9, _hoisted_31)) : createCommentVNode("v-if", true),
2555
+ createElementVNode("span", _hoisted_32, [
2300
2556
  withDirectives(createElementVNode("input", {
2301
- id: `fl-root-unlock-${__props.path}`,
2302
- "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => rootLock.input = $event),
2557
+ id: `${unref(uid)}-root`,
2558
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => rootLock.value.input = $event),
2303
2559
  type: "password",
2304
2560
  autocomplete: "off",
2305
2561
  "data-unlock": __props.path,
2306
2562
  placeholder: t("passwordPlaceholder"),
2307
- "aria-invalid": !!rootLock.error,
2308
- "aria-describedby": rootLock.error ? "fl-root-unlock-error" : void 0
2309
- }, null, 8, _hoisted_36), [[vModelText, rootLock.input]]),
2563
+ "aria-invalid": !!rootLock.value.error,
2564
+ "aria-describedby": rootLock.value.error ? `${unref(uid)}-root-error` : void 0
2565
+ }, null, 8, _hoisted_33), [[vModelText, rootLock.value.input]]),
2310
2566
  canRemember.value ? (openBlock(), createElementBlock("button", {
2311
2567
  key: 0,
2312
2568
  class: "fl-unlock-remember",
2313
2569
  type: "button",
2314
- "aria-pressed": rootLock.remember,
2570
+ "aria-pressed": rootLock.value.remember,
2315
2571
  "aria-label": t("rememberPassword"),
2316
2572
  title: t("rememberPassword"),
2317
- onClick: _cache[6] || (_cache[6] = ($event) => rootLock.remember = !rootLock.remember)
2318
- }, [..._cache[14] || (_cache[14] = [createElementVNode("svg", {
2319
- viewBox: "0 0 24 24",
2320
- width: "16",
2321
- height: "16",
2322
- fill: "none",
2323
- stroke: "currentColor",
2324
- "stroke-width": "2",
2325
- "stroke-linecap": "round",
2326
- "stroke-linejoin": "round",
2327
- "aria-hidden": "true"
2328
- }, [
2329
- createElementVNode("circle", {
2330
- cx: "8",
2331
- cy: "15",
2332
- r: "4"
2333
- }),
2334
- createElementVNode("path", { d: "m11 12 9-9" }),
2335
- createElementVNode("path", { d: "m17 6 3 3" })
2336
- ], -1)])], 8, _hoisted_37)) : createCommentVNode("v-if", true),
2573
+ onClick: _cache[3] || (_cache[3] = ($event) => rootLock.value.remember = !rootLock.value.remember)
2574
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_34)) : createCommentVNode("v-if", true),
2337
2575
  createElementVNode("button", {
2338
2576
  class: "fl-unlock-submit",
2339
2577
  type: "submit",
2340
- disabled: rootLock.busy || !rootLock.input
2341
- }, toDisplayString(t("open")), 9, _hoisted_38)
2578
+ disabled: rootLock.value.busy || !rootLock.value.input
2579
+ }, toDisplayString(t("open")), 9, _hoisted_35)
2342
2580
  ])
2343
- ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_39, [
2581
+ ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_36, [
2344
2582
  createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
2345
- /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_40, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
2346
- /failed to fetch|networkerror/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_41, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
2583
+ /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_37, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
2584
+ unref(isNetworkError)(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_38, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
2347
2585
  createElementVNode("button", {
2348
2586
  class: "fl-btn",
2349
2587
  type: "button",
2350
2588
  onClick: loadRoot
2351
2589
  }, toDisplayString(t("retry")), 1)
2352
- ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_42, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2353
- (openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (node) => {
2354
- return openBlock(), createElementBlock("div", {
2355
- key: node.path,
2590
+ ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_39, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2591
+ (openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (node, index) => {
2592
+ return openBlock(), createElementBlock(Fragment, { key: isMore(node) ? `more:${node.key}` : node.path }, [isMore(node) ? (openBlock(), createElementBlock("div", {
2593
+ key: 0,
2594
+ class: "fl-row fl-more",
2595
+ style: normalizeStyle({ "--fl-depth": node.depth })
2596
+ }, [_cache[9] || (_cache[9] = createElementVNode("span", { class: "fl-cell fl-cell-check" }, null, -1)), createElementVNode("span", _hoisted_40, [
2597
+ _cache[8] || (_cache[8] = createElementVNode("span", {
2598
+ class: "fl-indent",
2599
+ "aria-hidden": "true"
2600
+ }, null, -1)),
2601
+ createElementVNode("span", _hoisted_41, toDisplayString(t("moreRemaining", { count: node.remaining })), 1),
2602
+ node.remaining > (__props.pageSize || 0) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("button", {
2603
+ class: "fl-more-btn",
2604
+ type: "button",
2605
+ onClick: ($event) => showMore(node, false)
2606
+ }, [createTextVNode(toDisplayString(t("showMore")), 1), createElementVNode("span", _hoisted_43, toDisplayString(` ${t("folderItems", { count: __props.pageSize })}`), 1)], 8, _hoisted_42), createElementVNode("button", {
2607
+ class: "fl-more-btn fl-more-all",
2608
+ type: "button",
2609
+ onClick: ($event) => showMore(node, true)
2610
+ }, toDisplayString(t("showAll")), 9, _hoisted_44)], 64)) : (openBlock(), createElementBlock("button", {
2611
+ key: 1,
2612
+ class: "fl-more-btn",
2613
+ type: "button",
2614
+ onClick: ($event) => showMore(node, true)
2615
+ }, toDisplayString(t("showRest", { count: node.remaining })), 9, _hoisted_45))
2616
+ ])], 4)) : (openBlock(), createElementBlock("div", {
2617
+ key: 1,
2356
2618
  class: normalizeClass(["fl-row", {
2357
2619
  "fl-row-dir": node.is_dir,
2358
2620
  "fl-row-root": node.isRoot,
2359
2621
  "fl-row-selected": !node.is_dir && selected.value.has(node.path),
2360
- "fl-row-locked": node.locked,
2361
- "fl-row-prompting": node.locked && node.prompting,
2362
- "fl-row-error": node.locked && node.prompting && !!node.unlockError
2622
+ "fl-row-locked": !!node.lock,
2623
+ "fl-row-prompting": node.lock?.prompting,
2624
+ "fl-row-error": node.lock?.prompting && !!node.lock.error
2363
2625
  }]),
2364
2626
  style: normalizeStyle({ "--fl-depth": node.depth })
2365
2627
  }, [
2366
- createElementVNode("label", _hoisted_43, [createElementVNode("input", {
2628
+ createElementVNode("label", _hoisted_46, [createElementVNode("input", {
2367
2629
  type: "checkbox",
2368
- disabled: node.locked,
2630
+ disabled: !!node.lock || batchBusy.value,
2369
2631
  "aria-label": t("selectItem", { name: node.name }),
2370
2632
  checked: isChecked(node),
2371
2633
  ".indeterminate": isIndeterminate(node),
2372
2634
  onChange: ($event) => toggleCheck(node, $event)
2373
- }, null, 40, _hoisted_44)]),
2635
+ }, null, 40, _hoisted_47)]),
2374
2636
  createElementVNode("span", {
2375
2637
  class: "fl-cell fl-cell-name",
2376
2638
  title: node.isRoot ? node.path : void 0
2377
2639
  }, [
2378
- _cache[20] || (_cache[20] = createElementVNode("span", {
2640
+ _cache[11] || (_cache[11] = createElementVNode("span", {
2379
2641
  class: "fl-indent",
2380
2642
  "aria-hidden": "true"
2381
2643
  }, null, -1)),
2382
- node.locked ? (openBlock(), createElementBlock("button", {
2644
+ node.lock ? (openBlock(), createElementBlock("button", {
2383
2645
  key: 0,
2384
2646
  class: "fl-arrow fl-lock",
2385
2647
  type: "button",
2386
2648
  "aria-label": t("unlockFolder", { name: node.name }),
2387
2649
  title: t("unlockFolder", { name: node.name }),
2388
2650
  onClick: ($event) => toggleExpand(node)
2389
- }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
2390
- viewBox: "0 0 24 24",
2391
- width: "14",
2392
- height: "14",
2393
- fill: "none",
2394
- stroke: "currentColor",
2395
- "stroke-width": "2.2",
2396
- "stroke-linecap": "round",
2397
- "stroke-linejoin": "round",
2398
- "aria-hidden": "true"
2399
- }, [createElementVNode("rect", {
2400
- x: "5",
2401
- y: "11",
2402
- width: "14",
2403
- height: "10",
2404
- rx: "2"
2405
- }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_46)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2651
+ }, [createVNode(_sfc_main$2, {
2652
+ name: "lock",
2653
+ size: 14
2654
+ })], 8, _hoisted_49)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2406
2655
  key: 1,
2407
2656
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
2408
2657
  type: "button",
2409
2658
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
2410
2659
  "aria-expanded": node.expanded,
2411
2660
  onClick: ($event) => toggleExpand(node)
2412
- }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
2413
- viewBox: "0 0 24 24",
2414
- width: "14",
2415
- height: "14",
2416
- fill: "none",
2417
- stroke: "currentColor",
2418
- "stroke-width": "2.4",
2419
- "stroke-linecap": "round",
2420
- "stroke-linejoin": "round",
2421
- "aria-hidden": "true"
2422
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_47)) : (openBlock(), createElementBlock("span", _hoisted_48)),
2423
- node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_49, [..._cache[18] || (_cache[18] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
2661
+ }, [createVNode(_sfc_main$2, {
2662
+ name: "next",
2663
+ size: 14
2664
+ })], 10, _hoisted_50)) : (openBlock(), createElementBlock("span", _hoisted_51)),
2665
+ node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_52, [..._cache[10] || (_cache[10] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
2424
2666
  key: 4,
2425
2667
  class: "fl-type",
2426
2668
  "data-kind": unref(fileKind)(node.name),
2427
2669
  "aria-hidden": "true"
2428
- }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_50)),
2429
- createElementVNode("span", _hoisted_51, [createElementVNode("span", {
2670
+ }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_53)),
2671
+ createElementVNode("span", _hoisted_54, [createElementVNode("span", {
2430
2672
  class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
2431
- onClick: ($event) => node.locked || node.is_dir ? toggleExpand(node) : download(node)
2432
- }, toDisplayString(node.name), 11, _hoisted_52), createElementVNode("span", _hoisted_53, [node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("folderItems", { count: folderFiles(node).length })) + " · " + toDisplayString(formatTotal(unref(totalSize)(folderFiles(node)))), 1)], 64)) : createCommentVNode("v-if", true)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64))])]),
2433
- node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_54, toDisplayString(t("folderItems", { count: folderFiles(node).length })), 1)) : createCommentVNode("v-if", true),
2434
- node.locked && node.prompting && node.unlockError ? (openBlock(), createElementBlock("span", {
2673
+ onClick: ($event) => node.lock || node.is_dir ? toggleExpand(node) : download(node)
2674
+ }, toDisplayString(node.name), 11, _hoisted_55), createElementVNode("span", _hoisted_56, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64)) : node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(t("folderItems", { count: statsOf(node).files })) + " · " + toDisplayString(unref(formatSize)(statsOf(node).bytes)), 1)], 64)) : createCommentVNode("v-if", true)])]),
2675
+ node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_57, toDisplayString(t("folderItems", { count: statsOf(node).files })), 1)) : createCommentVNode("v-if", true),
2676
+ node.lock?.prompting && node.lock.error ? (openBlock(), createElementBlock("span", {
2435
2677
  key: 6,
2436
- id: `fl-unlock-error-${node.path}`,
2678
+ id: `${unref(uid)}-error-${index}`,
2437
2679
  class: "fl-unlock-message",
2438
2680
  role: "alert"
2439
- }, toDisplayString(unlockErrorText(node.unlockError)), 9, _hoisted_55)) : createCommentVNode("v-if", true),
2440
- !node.locked && rememberedPaths.value.has(node.path) ? (openBlock(), createElementBlock("button", {
2681
+ }, toDisplayString(unlockErrorText(node.lock.error)), 9, _hoisted_58)) : createCommentVNode("v-if", true),
2682
+ !node.lock && unref(remembered).has(node.path) ? (openBlock(), createElementBlock("button", {
2441
2683
  key: 7,
2442
2684
  class: "fl-remembered",
2443
2685
  type: "button",
2444
2686
  "aria-label": `${t("forgetPassword")} ${node.name}`,
2445
2687
  title: t("forgetPassword"),
2446
2688
  onClick: ($event) => forgetPassword(node)
2447
- }, [_cache[19] || (_cache[19] = createElementVNode("svg", {
2448
- viewBox: "0 0 24 24",
2449
- width: "11",
2450
- height: "11",
2451
- fill: "none",
2452
- stroke: "currentColor",
2453
- "stroke-width": "2.2",
2454
- "stroke-linecap": "round",
2455
- "stroke-linejoin": "round",
2456
- "aria-hidden": "true"
2457
- }, [
2458
- createElementVNode("circle", {
2459
- cx: "8",
2460
- cy: "15",
2461
- r: "4"
2462
- }),
2463
- createElementVNode("path", { d: "m11 12 9-9" }),
2464
- createElementVNode("path", { d: "m17 6 3 3" })
2465
- ], -1)), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_56)) : createCommentVNode("v-if", true),
2466
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_57, "…")) : createCommentVNode("v-if", true)
2467
- ], 8, _hoisted_45),
2468
- node.locked && node.prompting ? (openBlock(), createElementBlock("form", {
2689
+ }, [createVNode(_sfc_main$2, {
2690
+ name: "key",
2691
+ size: 11
2692
+ }), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_59)) : createCommentVNode("v-if", true),
2693
+ node.loading ? (openBlock(), createElementBlock("span", _hoisted_60, "…")) : createCommentVNode("v-if", true)
2694
+ ], 8, _hoisted_48),
2695
+ node.lock?.prompting ? (openBlock(), createElementBlock("form", {
2469
2696
  key: 0,
2470
2697
  class: "fl-unlock fl-unlock-row",
2471
2698
  onSubmit: withModifiers(($event) => submitUnlock(node), ["prevent"]),
2472
- onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node), ["prevent"]), ["esc"])
2699
+ onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node.lock), ["prevent"]), ["esc"])
2473
2700
  }, [
2474
- _cache[23] || (_cache[23] = createElementVNode("svg", {
2701
+ createVNode(_sfc_main$2, {
2475
2702
  class: "fl-unlock-lock",
2476
- viewBox: "0 0 24 24",
2477
- width: "18",
2478
- height: "18",
2479
- fill: "none",
2480
- stroke: "currentColor",
2481
- "stroke-width": "2.2",
2482
- "stroke-linecap": "round",
2483
- "stroke-linejoin": "round",
2484
- "aria-hidden": "true"
2485
- }, [createElementVNode("rect", {
2486
- x: "5",
2487
- y: "11",
2488
- width: "14",
2489
- height: "10",
2490
- rx: "2"
2491
- }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
2703
+ name: "lock",
2704
+ size: 18
2705
+ }),
2492
2706
  withDirectives(createElementVNode("input", {
2493
- "onUpdate:modelValue": ($event) => node.unlockInput = $event,
2707
+ "onUpdate:modelValue": ($event) => node.lock.input = $event,
2494
2708
  type: "password",
2495
2709
  autocomplete: "off",
2496
2710
  "data-unlock": node.path,
2497
- placeholder: node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"),
2711
+ placeholder: node.lock.error ? unlockErrorText(node.lock.error) : t("passwordPlaceholder"),
2498
2712
  "aria-label": t("passwordFor", { name: node.name }),
2499
- "aria-invalid": !!node.unlockError,
2500
- "aria-describedby": node.unlockError ? `fl-unlock-error-${node.path}` : void 0
2501
- }, null, 8, _hoisted_59), [[vModelText, node.unlockInput]]),
2713
+ "aria-invalid": !!node.lock.error,
2714
+ "aria-describedby": node.lock.error ? `${unref(uid)}-error-${index}` : void 0
2715
+ }, null, 8, _hoisted_62), [[vModelText, node.lock.input]]),
2502
2716
  canRemember.value ? (openBlock(), createElementBlock("button", {
2503
2717
  key: 0,
2504
2718
  class: "fl-unlock-remember",
2505
2719
  type: "button",
2506
- "aria-pressed": !!node.unlockRemember,
2720
+ "aria-pressed": node.lock.remember,
2507
2721
  "aria-label": t("rememberPassword"),
2508
2722
  title: t("rememberPassword"),
2509
- onClick: ($event) => node.unlockRemember = !node.unlockRemember
2510
- }, [..._cache[21] || (_cache[21] = [createElementVNode("svg", {
2511
- viewBox: "0 0 24 24",
2512
- width: "16",
2513
- height: "16",
2514
- fill: "none",
2515
- stroke: "currentColor",
2516
- "stroke-width": "2",
2517
- "stroke-linecap": "round",
2518
- "stroke-linejoin": "round",
2519
- "aria-hidden": "true"
2520
- }, [
2521
- createElementVNode("circle", {
2522
- cx: "8",
2523
- cy: "15",
2524
- r: "4"
2525
- }),
2526
- createElementVNode("path", { d: "m11 12 9-9" }),
2527
- createElementVNode("path", { d: "m17 6 3 3" })
2528
- ], -1)])], 8, _hoisted_60)) : createCommentVNode("v-if", true),
2723
+ onClick: ($event) => node.lock.remember = !node.lock.remember
2724
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_63)) : createCommentVNode("v-if", true),
2529
2725
  createElementVNode("button", {
2530
2726
  class: "fl-unlock-submit",
2531
2727
  type: "submit",
2532
- disabled: node.unlockBusy || !node.unlockInput
2533
- }, toDisplayString(t("open")), 9, _hoisted_61),
2728
+ disabled: node.lock.busy || !node.lock.input
2729
+ }, toDisplayString(t("open")), 9, _hoisted_64),
2534
2730
  createElementVNode("button", {
2535
2731
  class: "fl-unlock-cancel",
2536
2732
  type: "button",
2537
2733
  "aria-label": t("cancel"),
2538
2734
  title: t("cancel"),
2539
- onClick: ($event) => cancelUnlock(node)
2540
- }, [..._cache[22] || (_cache[22] = [createElementVNode("svg", {
2541
- viewBox: "0 0 24 24",
2542
- width: "14",
2543
- height: "14",
2544
- fill: "none",
2545
- stroke: "currentColor",
2546
- "stroke-width": "2.2",
2547
- "stroke-linecap": "round",
2548
- "aria-hidden": "true"
2549
- }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_62)
2550
- ], 40, _hoisted_58)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2551
- createElementVNode("span", _hoisted_63, toDisplayString(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size)), 1),
2552
- createElementVNode("span", _hoisted_64, toDisplayString(unref(formatDate)(node.modified)), 1),
2553
- createElementVNode("span", _hoisted_65, [node.locked ? (openBlock(), createElementBlock("button", {
2735
+ onClick: ($event) => cancelUnlock(node.lock)
2736
+ }, [createVNode(_sfc_main$2, {
2737
+ name: "close",
2738
+ size: 14
2739
+ })], 8, _hoisted_65)
2740
+ ], 40, _hoisted_61)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2741
+ createElementVNode("span", _hoisted_66, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
2742
+ createElementVNode("span", _hoisted_67, toDisplayString(unref(formatDate)(node.modified)), 1),
2743
+ createElementVNode("span", _hoisted_68, [node.lock ? (openBlock(), createElementBlock("button", {
2554
2744
  key: 0,
2555
2745
  class: "fl-action fl-arrow-end fl-lock",
2556
2746
  type: "button",
2557
2747
  "aria-label": t("unlockFolder", { name: node.name }),
2558
2748
  onClick: ($event) => toggleExpand(node)
2559
- }, [..._cache[24] || (_cache[24] = [createElementVNode("svg", {
2560
- viewBox: "0 0 24 24",
2561
- width: "18",
2562
- height: "18",
2563
- fill: "none",
2564
- stroke: "currentColor",
2565
- "stroke-width": "2.2",
2566
- "stroke-linecap": "round",
2567
- "stroke-linejoin": "round",
2568
- "aria-hidden": "true"
2569
- }, [createElementVNode("rect", {
2570
- x: "5",
2571
- y: "11",
2572
- width: "14",
2573
- height: "10",
2574
- rx: "2"
2575
- }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_66)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2749
+ }, [createVNode(_sfc_main$2, {
2750
+ name: "lock",
2751
+ size: 18
2752
+ })], 8, _hoisted_69)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2576
2753
  key: 1,
2577
2754
  class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
2578
2755
  type: "button",
2579
2756
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
2580
2757
  "aria-expanded": node.expanded,
2581
2758
  onClick: ($event) => toggleExpand(node)
2582
- }, [..._cache[25] || (_cache[25] = [createElementVNode("svg", {
2583
- viewBox: "0 0 24 24",
2584
- width: "18",
2585
- height: "18",
2586
- fill: "none",
2587
- stroke: "currentColor",
2588
- "stroke-width": "2.2",
2589
- "stroke-linecap": "round",
2590
- "stroke-linejoin": "round",
2591
- "aria-hidden": "true"
2592
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_67)) : !node.locked ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2759
+ }, [createVNode(_sfc_main$2, {
2760
+ name: "next",
2761
+ size: 18
2762
+ })], 10, _hoisted_70)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2593
2763
  key: 0,
2594
- class: "fl-action fl-download",
2764
+ class: "fl-action fl-preview-btn",
2595
2765
  type: "button",
2596
2766
  "aria-label": `${t("preview")} ${node.name}`,
2597
2767
  title: t("preview"),
2598
2768
  onClick: ($event) => preview(node)
2599
- }, [..._cache[26] || (_cache[26] = [createElementVNode("svg", {
2600
- viewBox: "0 0 24 24",
2601
- width: "16",
2602
- height: "16",
2603
- fill: "none",
2604
- stroke: "currentColor",
2605
- "stroke-width": "2",
2606
- "stroke-linecap": "round",
2607
- "stroke-linejoin": "round",
2608
- "aria-hidden": "true"
2609
- }, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
2610
- cx: "12",
2611
- cy: "12",
2612
- r: "3"
2613
- })], -1)])], 8, _hoisted_68)) : createCommentVNode("v-if", true), createElementVNode("button", {
2769
+ }, [createVNode(_sfc_main$2, { name: "eye" })], 8, _hoisted_71)) : createCommentVNode("v-if", true), createElementVNode("button", {
2614
2770
  class: "fl-action fl-download",
2615
2771
  type: "button",
2616
2772
  "aria-label": `${t("download")} ${node.name}`,
2617
2773
  title: t("download"),
2618
2774
  onClick: ($event) => download(node)
2619
- }, [..._cache[27] || (_cache[27] = [createElementVNode("svg", {
2620
- viewBox: "0 0 24 24",
2621
- width: "16",
2622
- height: "16",
2623
- fill: "none",
2624
- stroke: "currentColor",
2625
- "stroke-width": "2",
2626
- "stroke-linecap": "round",
2627
- "stroke-linejoin": "round",
2628
- "aria-hidden": "true"
2629
- }, [
2630
- createElementVNode("path", { d: "M12 3v12" }),
2631
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
2632
- createElementVNode("path", { d: "M5 21h14" })
2633
- ], -1)])], 8, _hoisted_69)], 64)) : createCommentVNode("v-if", true)])
2775
+ }, [createVNode(_sfc_main$2, { name: "download" })], 8, _hoisted_72)], 64))])
2634
2776
  ], 64))
2635
- ], 6);
2777
+ ], 6))], 64);
2636
2778
  }), 128)),
2637
- createElementVNode("div", _hoisted_70, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: formatTotal(loadedBytes.value) })), 1)]),
2638
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_71, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
2779
+ 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
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_74, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
2781
+ createVNode(_sfc_main$3, {
2782
+ open: !!zipNotice.value,
2783
+ title: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnTitle" : "zipBlockTitle", { size: unref(formatSize)(zipNotice.value.size) }) : "",
2784
+ text: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnText" : "zipBlockText", { limit: unref(formatSize)(zipNotice.value.limit) }) : "",
2785
+ tip: zipTip.value,
2786
+ "confirm-label": zipNotice.value?.kind === "warn" ? t("zipContinue") : void 0,
2787
+ "cancel-label": zipNotice.value?.kind === "warn" ? t("cancel") : t("gotIt"),
2788
+ onConfirm: confirmZip,
2789
+ onClose: _cache[4] || (_cache[4] = ($event) => zipNotice.value = null)
2790
+ }, null, 8, [
2791
+ "open",
2792
+ "title",
2793
+ "text",
2794
+ "tip",
2795
+ "confirm-label",
2796
+ "cancel-label"
2797
+ ]),
2639
2798
  createVNode(_sfc_main$1, {
2640
2799
  open: !!previewNode.value,
2641
2800
  file: previewNode.value,
@@ -2648,9 +2807,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2648
2807
  position: previewPosition.value,
2649
2808
  total: previewPosition.value ? previewList.value.length : 0,
2650
2809
  onClose: closePreview,
2651
- onPrev: _cache[7] || (_cache[7] = ($event) => stepPreview(-1)),
2652
- onNext: _cache[8] || (_cache[8] = ($event) => stepPreview(1)),
2653
- onDownload: _cache[9] || (_cache[9] = ($event) => previewNode.value && download(previewNode.value))
2810
+ onPrev: _cache[5] || (_cache[5] = ($event) => stepPreview(-1)),
2811
+ onNext: _cache[6] || (_cache[6] = ($event) => stepPreview(1)),
2812
+ onDownload: _cache[7] || (_cache[7] = ($event) => previewNode.value && download(previewNode.value))
2654
2813
  }, null, 8, [
2655
2814
  "open",
2656
2815
  "file",