vitepress-plugin-filelist 0.3.1 → 0.5.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,5 @@
1
1
  import "../style.css";
2
- import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, withDirectives, withKeys } from "vue";
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";
3
3
  // src/client/locales.ts
4
4
  const fileListLocales = {
5
5
  "zh-CN": {
@@ -11,7 +11,7 @@ const fileListLocales = {
11
11
  batchCount: "批量下载({count})",
12
12
  batching: "打包中 {percent}%",
13
13
  passwordPrompt: "该目录受密码保护:",
14
- passwordPlaceholder: "输入目录密码",
14
+ passwordPlaceholder: "输入密码",
15
15
  open: "打开",
16
16
  columnName: "文件名",
17
17
  columnSize: "文件大小",
@@ -19,7 +19,7 @@ const fileListLocales = {
19
19
  loading: "加载中…",
20
20
  loadFailed: "加载失败:{message}",
21
21
  guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
22
- corsHint: "可能是跨域或混合内容被拦截,检查 OpenList 的 cors 配置与页面协议。",
22
+ corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
23
23
  retry: "重试",
24
24
  empty: "没有文件",
25
25
  preview: "预览",
@@ -43,7 +43,27 @@ const fileListLocales = {
43
43
  selectAll: "全选",
44
44
  selectItem: "选择 {name}",
45
45
  expand: "展开 {name}",
46
- collapse: "收起 {name}"
46
+ collapse: "收起 {name}",
47
+ close: "关闭",
48
+ previous: "上一个",
49
+ next: "下一个",
50
+ rotate: "旋转",
51
+ actualSize: "原始大小",
52
+ previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
53
+ previewUnavailable: "该文件无法在页面内预览,可下载后查看",
54
+ previewTooLarge: "文件较大({size}),请下载后查看",
55
+ textTruncated: "仅显示前 {size},完整内容请下载",
56
+ refreshing: "刷新中",
57
+ refreshFailed: "刷新失败:{message}",
58
+ cancel: "取消",
59
+ passwordFor: "{name} 的密码",
60
+ passwordWrong: "密码不正确或无权访问",
61
+ passwordStale: "已保存的密码已失效,请重新输入",
62
+ rememberPassword: "在此标签页中记住(关闭标签页后失效)",
63
+ remembered: "已记住",
64
+ forgetPassword: "忘记",
65
+ unlockFolder: "输入 {name} 的密码",
66
+ networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
47
67
  },
48
68
  "en-US": {
49
69
  allFiles: "All files",
@@ -62,7 +82,7 @@ const fileListLocales = {
62
82
  loading: "Loading…",
63
83
  loadFailed: "Failed to load: {message}",
64
84
  guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
65
- corsHint: "Possibly blocked by CORS or mixed content. Check OpenList cors settings and the page protocol.",
85
+ corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
66
86
  retry: "Retry",
67
87
  empty: "No files",
68
88
  preview: "Preview",
@@ -86,7 +106,27 @@ const fileListLocales = {
86
106
  selectAll: "Select all",
87
107
  selectItem: "Select {name}",
88
108
  expand: "Expand {name}",
89
- collapse: "Collapse {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."
90
130
  }
91
131
  };
92
132
  function formatLabel(template, values = {}) {
@@ -268,37 +308,701 @@ function compareNodes(a, b, key, ascending = true) {
268
308
  else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
269
309
  return ascending ? result : -result;
270
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
+ };
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;
444
+ }
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));
452
+ }
453
+ // src/client/PreviewDialog.vue
454
+ const _hoisted_1$1 = ["aria-label"];
455
+ const _hoisted_2$1 = {
456
+ key: 0,
457
+ class: "fl-preview-panel"
458
+ };
459
+ const _hoisted_3$1 = { class: "fl-preview-head" };
460
+ const _hoisted_4$1 = ["data-kind"];
461
+ const _hoisted_5$1 = { class: "fl-preview-title" };
462
+ const _hoisted_6$1 = { class: "fl-preview-name" };
463
+ const _hoisted_7$1 = { class: "fl-preview-meta" };
464
+ const _hoisted_8$1 = {
465
+ key: 0,
466
+ class: "fl-preview-tools fl-preview-nav"
467
+ };
468
+ const _hoisted_9$1 = ["aria-label", "title"];
469
+ const _hoisted_10$1 = { class: "fl-preview-count" };
470
+ const _hoisted_11$1 = ["aria-label", "title"];
471
+ const _hoisted_12$1 = ["aria-label", "title"];
472
+ const _hoisted_13$1 = [
473
+ "aria-pressed",
474
+ "aria-label",
475
+ "title"
476
+ ];
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 = {
500
+ key: 0,
501
+ class: "fl-preview-status",
502
+ role: "status"
503
+ };
504
+ const _hoisted_22$1 = ["src", "alt"];
505
+ const _hoisted_23$1 = ["src"];
506
+ const _hoisted_24$1 = ["src"];
507
+ const _hoisted_25$1 = {
508
+ key: 4,
509
+ class: "fl-preview-text"
510
+ };
511
+ const _hoisted_26$1 = {
512
+ key: 0,
513
+ class: "fl-preview-note"
514
+ };
515
+ const _hoisted_27$1 = ["src", "title"];
516
+ const _hoisted_28$1 = {
517
+ key: 6,
518
+ class: "fl-preview-info"
519
+ };
520
+ const _hoisted_29$1 = ["data-kind"];
521
+ const _hoisted_30$1 = { class: "fl-preview-message" };
522
+ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
523
+ __name: "PreviewDialog",
524
+ props: {
525
+ open: { type: Boolean },
526
+ file: {},
527
+ labels: {},
528
+ src: { default: "" },
529
+ viewer: { default: "" },
530
+ resolving: {
531
+ type: Boolean,
532
+ default: false
533
+ },
534
+ error: { default: "" },
535
+ demo: {
536
+ type: Boolean,
537
+ default: false
538
+ },
539
+ position: { default: 0 },
540
+ total: { default: 0 },
541
+ maxTextBytes: { default: 1048576 },
542
+ maxPdfBytes: { default: 52428800 }
543
+ },
544
+ emits: [
545
+ "close",
546
+ "prev",
547
+ "next",
548
+ "download"
549
+ ],
550
+ setup(__props, { emit: __emit }) {
551
+ /**
552
+ * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
553
+ *
554
+ * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
555
+ * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
556
+ * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
557
+ */
558
+ const props = __props;
559
+ const emit = __emit;
560
+ function t(key, values) {
561
+ return formatLabel(props.labels[key], values);
562
+ }
563
+ const dialog = ref();
564
+ const kind = computed(() => props.file ? previewKind(props.file.name) : "none");
565
+ const loadingBody = ref(false);
566
+ const bodyFailed = ref(false);
567
+ const textContent = ref("");
568
+ const truncated = ref(false);
569
+ const blobUrl = ref("");
570
+ const rotation = ref(0);
571
+ const actualSize = ref(false);
572
+ let controller;
573
+ let loadSeq = 0;
574
+ const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
575
+ const mode = computed(() => {
576
+ if (props.demo) return "demo";
577
+ if (props.error || bodyFailed.value) return "failed";
578
+ if (kind.value === "none") return "unsupported";
579
+ if (tooLarge.value) return "tooLarge";
580
+ return kind.value;
581
+ });
582
+ const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
583
+ const infoMessage = computed(() => {
584
+ const file = props.file;
585
+ if (!file) return "";
586
+ if (mode.value === "demo") return t("demoPreview", { name: file.name });
587
+ if (mode.value === "tooLarge") return t("previewTooLarge", { size: formatSize(file.size) });
588
+ if (mode.value === "failed") return t("previewUnavailable");
589
+ return t("previewUnsupported");
590
+ });
591
+ function revokeBlob() {
592
+ if (blobUrl.value) URL.revokeObjectURL(blobUrl.value);
593
+ blobUrl.value = "";
594
+ }
595
+ function resetBody() {
596
+ controller?.abort();
597
+ controller = void 0;
598
+ loadSeq += 1;
599
+ revokeBlob();
600
+ loadingBody.value = false;
601
+ bodyFailed.value = false;
602
+ textContent.value = "";
603
+ truncated.value = false;
604
+ rotation.value = 0;
605
+ actualSize.value = false;
606
+ }
607
+ async function readPrefix(response, limit) {
608
+ const reader = response.body?.getReader();
609
+ if (!reader) {
610
+ const all = new Uint8Array(await response.arrayBuffer());
611
+ return {
612
+ bytes: all.slice(0, limit),
613
+ more: all.length > limit
614
+ };
615
+ }
616
+ const chunks = [];
617
+ let length = 0;
618
+ let more = false;
619
+ while (true) {
620
+ const { done, value } = await reader.read();
621
+ if (done) break;
622
+ chunks.push(value);
623
+ length += value.length;
624
+ if (length >= limit) {
625
+ more = length > limit;
626
+ await reader.cancel().catch(() => {});
627
+ break;
628
+ }
629
+ }
630
+ const bytes = new Uint8Array(Math.min(length, limit));
631
+ let offset = 0;
632
+ for (const chunk of chunks) {
633
+ const part = chunk.subarray(0, bytes.length - offset);
634
+ bytes.set(part, offset);
635
+ offset += part.length;
636
+ if (offset >= bytes.length) break;
637
+ }
638
+ return {
639
+ bytes,
640
+ more
641
+ };
642
+ }
643
+ async function loadBody() {
644
+ resetBody();
645
+ const file = props.file;
646
+ if (!props.open || !file || props.demo || !props.src || props.error) return;
647
+ if (kind.value !== "text" && !(kind.value === "pdf" && !props.viewer && !tooLarge.value)) return;
648
+ const seq = loadSeq;
649
+ controller = new AbortController();
650
+ loadingBody.value = true;
651
+ try {
652
+ if (kind.value === "text") {
653
+ const limit = props.maxTextBytes;
654
+ const response = await fetch(props.src, {
655
+ headers: { Range: `bytes=0-${limit - 1}` },
656
+ signal: controller.signal
657
+ });
658
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
659
+ const { bytes, more } = await readPrefix(response, limit);
660
+ if (seq !== loadSeq) return;
661
+ textContent.value = new TextDecoder("utf-8").decode(bytes);
662
+ truncated.value = more || file.size > limit;
663
+ } else {
664
+ const response = await fetch(props.src, { signal: controller.signal });
665
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
666
+ const buffer = await response.arrayBuffer();
667
+ if (seq !== loadSeq) return;
668
+ blobUrl.value = URL.createObjectURL(new Blob([buffer], { type: "application/pdf" }));
669
+ }
670
+ } catch {
671
+ if (seq === loadSeq && !controller?.signal.aborted) bodyFailed.value = true;
672
+ } finally {
673
+ if (seq === loadSeq) loadingBody.value = false;
674
+ }
675
+ }
676
+ watch(() => [
677
+ props.open,
678
+ props.file?.name,
679
+ props.src,
680
+ props.viewer,
681
+ props.error,
682
+ props.demo
683
+ ], () => void loadBody(), { immediate: true });
684
+ watch(() => props.open, (open) => {
685
+ const el = dialog.value;
686
+ if (!el) return;
687
+ if (open && !el.open) {
688
+ if (typeof el.showModal === "function") el.showModal();
689
+ else el.setAttribute("open", "");
690
+ } else if (!open && el.open) {
691
+ if (typeof el.close === "function") el.close();
692
+ else el.removeAttribute("open");
693
+ }
694
+ }, { flush: "post" });
695
+ onBeforeUnmount(resetBody);
696
+ function onNativeClose() {
697
+ if (props.open) emit("close");
698
+ }
699
+ function onBackdrop(event) {
700
+ if (event.target === dialog.value) emit("close");
701
+ }
702
+ function onKeydown(event) {
703
+ if (props.total < 2) return;
704
+ const target = event.target;
705
+ if (target && /^(INPUT|TEXTAREA|SELECT|VIDEO|AUDIO)$/.test(target.tagName)) return;
706
+ if (event.key === "ArrowLeft") {
707
+ event.preventDefault();
708
+ emit("prev");
709
+ } else if (event.key === "ArrowRight") {
710
+ event.preventDefault();
711
+ emit("next");
712
+ }
713
+ }
714
+ function markFailed() {
715
+ bodyFailed.value = true;
716
+ }
717
+ return (_ctx, _cache) => {
718
+ return openBlock(), createElementBlock("dialog", {
719
+ ref_key: "dialog",
720
+ ref: dialog,
721
+ class: "fl-preview",
722
+ "aria-label": __props.file?.name,
723
+ onClose: onNativeClose,
724
+ onClick: onBackdrop,
725
+ onKeydown
726
+ }, [__props.open && __props.file ? (openBlock(), createElementBlock("div", _hoisted_2$1, [createElementVNode("header", _hoisted_3$1, [
727
+ createElementVNode("span", {
728
+ class: "fl-type",
729
+ "data-kind": unref(fileKind)(__props.file.name),
730
+ "aria-hidden": "true"
731
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_4$1),
732
+ createElementVNode("div", _hoisted_5$1, [createElementVNode("span", _hoisted_6$1, toDisplayString(__props.file.name), 1), createElementVNode("span", _hoisted_7$1, toDisplayString(unref(formatSize)(__props.file.size)) + " · " + toDisplayString(unref(formatDate)(__props.file.modified)), 1)]),
733
+ __props.total > 1 || mode.value === "image" && !busy.value ? (openBlock(), createElementBlock("div", _hoisted_8$1, [__props.total > 1 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
734
+ createElementVNode("button", {
735
+ class: "fl-preview-tool",
736
+ type: "button",
737
+ "aria-label": t("previous"),
738
+ title: t("previous"),
739
+ 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),
751
+ createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
752
+ createElementVNode("button", {
753
+ class: "fl-preview-tool",
754
+ type: "button",
755
+ "aria-label": t("next"),
756
+ title: t("next"),
757
+ 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)
769
+ ], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
770
+ class: "fl-preview-tool fl-preview-rotate",
771
+ type: "button",
772
+ "aria-label": t("rotate"),
773
+ title: t("rotate"),
774
+ 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", {
786
+ class: "fl-preview-tool fl-preview-zoom",
787
+ type: "button",
788
+ "aria-pressed": actualSize.value,
789
+ "aria-label": t("actualSize"),
790
+ title: t("actualSize"),
791
+ 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", {
802
+ key: 0,
803
+ class: "fl-preview-tool fl-preview-download",
804
+ type: "button",
805
+ "aria-label": t("download"),
806
+ title: t("download"),
807
+ 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", {
823
+ class: "fl-preview-tool fl-preview-close",
824
+ type: "button",
825
+ autofocus: "",
826
+ "aria-label": t("close"),
827
+ title: t("close"),
828
+ 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)])
839
+ ]), createElementVNode("div", {
840
+ class: "fl-preview-body",
841
+ "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", {
843
+ key: 1,
844
+ class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
845
+ src: __props.src,
846
+ alt: __props.file.name,
847
+ style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
848
+ onError: markFailed
849
+ }, null, 46, _hoisted_22$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
850
+ key: 2,
851
+ class: "fl-preview-video",
852
+ src: __props.src,
853
+ controls: "",
854
+ playsinline: "",
855
+ preload: "metadata",
856
+ onError: markFailed
857
+ }, null, 40, _hoisted_23$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
858
+ key: 3,
859
+ class: "fl-preview-audio",
860
+ src: __props.src,
861
+ controls: "",
862
+ preload: "metadata",
863
+ 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", {
865
+ key: 5,
866
+ class: "fl-preview-frame",
867
+ src: __props.viewer || blobUrl.value,
868
+ title: __props.file.name
869
+ }, null, 8, _hoisted_27$1)) : (openBlock(), createElementBlock("div", _hoisted_28$1, [
870
+ createElementVNode("span", {
871
+ class: "fl-type fl-preview-badge",
872
+ "data-kind": unref(fileKind)(__props.file.name),
873
+ "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),
876
+ !__props.demo ? (openBlock(), createElementBlock("button", {
877
+ key: 0,
878
+ class: "fl-btn fl-btn-primary",
879
+ type: "button",
880
+ 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);
897
+ };
898
+ }
899
+ });
900
+ // src/client/passwords.ts
901
+ /**
902
+ * Folder passwords remembered for the current browser tab.
903
+ *
904
+ * Stored in `sessionStorage`, so they live only in this tab and are gone when
905
+ * it closes. Keyed by OpenList base URL, then by folder path; a remembered
906
+ * folder also covers its sub-folders. Every access is guarded: private mode
907
+ * or blocked storage simply means nothing is remembered.
908
+ */
909
+ const PREFIX = "vitepress-plugin-filelist:passwords:";
910
+ function store() {
911
+ try {
912
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
913
+ } catch {
914
+ return null;
915
+ }
916
+ }
917
+ function read(base) {
918
+ try {
919
+ const raw = store()?.getItem(PREFIX + base);
920
+ const parsed = raw ? JSON.parse(raw) : {};
921
+ return parsed && typeof parsed === "object" ? parsed : {};
922
+ } catch {
923
+ return {};
924
+ }
925
+ }
926
+ function write(base, map) {
927
+ try {
928
+ const target = store();
929
+ if (!target) return;
930
+ if (Object.keys(map).length) target.setItem(PREFIX + base, JSON.stringify(map));
931
+ else target.removeItem(PREFIX + base);
932
+ } catch {}
933
+ }
934
+ function normalise(path) {
935
+ return path.replace(/\/+$/, "") || "/";
936
+ }
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
+ /** The closest remembered folder at or above `path`, if any. */
944
+ function findRememberedPassword(base, path) {
945
+ let best = null;
946
+ for (const [folder, password] of Object.entries(read(base))) {
947
+ if (typeof password !== "string" || !isWithin(path, folder)) continue;
948
+ if (!best || folder.length > best.path.length) best = {
949
+ path: folder,
950
+ password
951
+ };
952
+ }
953
+ return best;
954
+ }
955
+ function rememberFolderPassword(base, path, password) {
956
+ write(base, {
957
+ ...read(base),
958
+ [normalise(path)]: password
959
+ });
960
+ }
961
+ function forgetFolderPassword(base, path) {
962
+ const map = read(base);
963
+ delete map[normalise(path)];
964
+ write(base, map);
965
+ }
966
+ /** Folders with a remembered password, for showing the "remembered" tag. */
967
+ function rememberedFolders(base) {
968
+ return Object.keys(read(base));
969
+ }
271
970
  // src/client/FileList.vue
272
- const _hoisted_1 = { class: "fl-bar" };
273
- const _hoisted_2 = {
971
+ const _hoisted_1 = ["aria-busy"];
972
+ const _hoisted_2 = { class: "fl-bar" };
973
+ const _hoisted_3 = {
274
974
  key: 0,
275
975
  class: "fl-selection",
276
976
  role: "status"
277
977
  };
278
- const _hoisted_3 = { class: "fl-selection-count" };
279
- const _hoisted_4 = { class: "fl-selection-size" };
280
- const _hoisted_5 = {
978
+ const _hoisted_4 = { class: "fl-selection-count" };
979
+ const _hoisted_5 = { class: "fl-selection-size" };
980
+ const _hoisted_6 = {
281
981
  key: 1,
282
982
  class: "fl-title"
283
983
  };
284
- const _hoisted_6 = { class: "fl-title-text" };
285
- const _hoisted_7 = {
984
+ const _hoisted_7 = { class: "fl-title-text" };
985
+ const _hoisted_8 = {
286
986
  key: 0,
287
987
  class: "fl-demo-badge"
288
988
  };
289
- const _hoisted_8 = ["aria-label", "title"];
290
- const _hoisted_9 = { class: "fl-clear-text" };
291
- const _hoisted_10 = { class: "fl-tools" };
292
- const _hoisted_11 = { class: "fl-search" };
293
- const _hoisted_12 = ["placeholder", "aria-label"];
294
- const _hoisted_13 = ["aria-label", "title"];
295
- const _hoisted_14 = ["disabled"];
296
- const _hoisted_15 = {
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 = [
995
+ "disabled",
996
+ "aria-label",
997
+ "title"
998
+ ];
999
+ const _hoisted_15 = ["disabled"];
1000
+ const _hoisted_16 = { class: "fl-row fl-head" };
1001
+ const _hoisted_17 = {
297
1002
  key: 0,
298
- class: "fl-password"
1003
+ class: "fl-progress",
1004
+ "aria-hidden": "true"
299
1005
  };
300
- const _hoisted_16 = ["placeholder", "aria-label"];
301
- const _hoisted_17 = { class: "fl-row fl-head" };
302
1006
  const _hoisted_18 = { class: "fl-cell fl-cell-check" };
303
1007
  const _hoisted_19 = [
304
1008
  "aria-label",
@@ -330,84 +1034,139 @@ const _hoisted_29 = {
330
1034
  const _hoisted_30 = ["aria-label"];
331
1035
  const _hoisted_31 = ["value"];
332
1036
  const _hoisted_32 = {
333
- key: 1,
1037
+ key: 0,
334
1038
  class: "fl-status"
335
1039
  };
336
- const _hoisted_33 = {
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 = [
1049
+ "id",
1050
+ "data-unlock",
1051
+ "placeholder",
1052
+ "aria-invalid",
1053
+ "aria-describedby"
1054
+ ];
1055
+ const _hoisted_37 = [
1056
+ "aria-pressed",
1057
+ "aria-label",
1058
+ "title"
1059
+ ];
1060
+ const _hoisted_38 = ["disabled"];
1061
+ const _hoisted_39 = {
337
1062
  key: 2,
338
1063
  class: "fl-status fl-error"
339
1064
  };
340
- const _hoisted_34 = {
1065
+ const _hoisted_40 = {
341
1066
  key: 0,
342
1067
  class: "fl-hint"
343
1068
  };
344
- const _hoisted_35 = {
1069
+ const _hoisted_41 = {
345
1070
  key: 1,
346
1071
  class: "fl-hint"
347
1072
  };
348
- const _hoisted_36 = {
1073
+ const _hoisted_42 = {
349
1074
  key: 3,
350
1075
  class: "fl-status"
351
1076
  };
352
- const _hoisted_37 = { class: "fl-cell fl-cell-check" };
353
- const _hoisted_38 = [
1077
+ const _hoisted_43 = { class: "fl-cell fl-cell-check" };
1078
+ const _hoisted_44 = [
1079
+ "disabled",
354
1080
  "aria-label",
355
1081
  "checked",
356
1082
  ".indeterminate",
357
1083
  "onChange"
358
1084
  ];
359
- const _hoisted_39 = ["title"];
360
- const _hoisted_40 = [
1085
+ const _hoisted_45 = ["title"];
1086
+ const _hoisted_46 = [
1087
+ "aria-label",
1088
+ "title",
1089
+ "onClick"
1090
+ ];
1091
+ const _hoisted_47 = [
361
1092
  "aria-label",
362
1093
  "aria-expanded",
363
1094
  "onClick"
364
1095
  ];
365
- const _hoisted_41 = {
366
- key: 1,
1096
+ const _hoisted_48 = {
1097
+ key: 2,
367
1098
  class: "fl-arrow-placeholder",
368
1099
  "aria-hidden": "true"
369
1100
  };
370
- const _hoisted_42 = {
371
- key: 2,
1101
+ const _hoisted_49 = {
1102
+ key: 3,
372
1103
  class: "fl-folder-icon",
373
1104
  viewBox: "0 0 24 24",
374
1105
  width: "18",
375
1106
  height: "18",
376
1107
  "aria-hidden": "true"
377
1108
  };
378
- const _hoisted_43 = ["data-kind"];
379
- const _hoisted_44 = { class: "fl-label" };
380
- const _hoisted_45 = ["onClick"];
381
- const _hoisted_46 = { class: "fl-meta" };
382
- const _hoisted_47 = {
383
- key: 4,
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 = {
1114
+ key: 5,
384
1115
  class: "fl-count"
385
1116
  };
386
- const _hoisted_48 = {
387
- key: 5,
1117
+ const _hoisted_55 = ["id"];
1118
+ const _hoisted_56 = [
1119
+ "aria-label",
1120
+ "title",
1121
+ "onClick"
1122
+ ];
1123
+ const _hoisted_57 = {
1124
+ key: 8,
388
1125
  class: "fl-loading",
389
1126
  "aria-hidden": "true"
390
1127
  };
391
- const _hoisted_49 = { class: "fl-cell fl-cell-size" };
392
- const _hoisted_50 = { class: "fl-cell fl-cell-date" };
393
- const _hoisted_51 = { class: "fl-cell fl-cell-op" };
394
- const _hoisted_52 = [
1128
+ const _hoisted_58 = ["onSubmit", "onKeydown"];
1129
+ const _hoisted_59 = [
1130
+ "onUpdate:modelValue",
1131
+ "data-unlock",
1132
+ "placeholder",
1133
+ "aria-label",
1134
+ "aria-invalid",
1135
+ "aria-describedby"
1136
+ ];
1137
+ const _hoisted_60 = [
1138
+ "aria-pressed",
1139
+ "aria-label",
1140
+ "title",
1141
+ "onClick"
1142
+ ];
1143
+ const _hoisted_61 = ["disabled"];
1144
+ const _hoisted_62 = [
1145
+ "aria-label",
1146
+ "title",
1147
+ "onClick"
1148
+ ];
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 = [
395
1154
  "aria-label",
396
1155
  "aria-expanded",
397
1156
  "onClick"
398
1157
  ];
399
- const _hoisted_53 = [
1158
+ const _hoisted_68 = [
400
1159
  "aria-label",
401
1160
  "title",
402
1161
  "onClick"
403
1162
  ];
404
- const _hoisted_54 = [
1163
+ const _hoisted_69 = [
405
1164
  "aria-label",
406
1165
  "title",
407
1166
  "onClick"
408
1167
  ];
409
- const _hoisted_55 = { class: "fl-foot" };
410
- const _hoisted_56 = {
1168
+ const _hoisted_70 = { class: "fl-foot" };
1169
+ const _hoisted_71 = {
411
1170
  key: 4,
412
1171
  class: "fl-toast",
413
1172
  role: "status"
@@ -425,6 +1184,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
425
1184
  token: { default: "" },
426
1185
  password: { default: "" },
427
1186
  title: { default: "" },
1187
+ rememberPassword: {
1188
+ type: Boolean,
1189
+ default: true
1190
+ },
428
1191
  locale: { default: "zh-CN" },
429
1192
  labels: {}
430
1193
  },
@@ -443,18 +1206,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
443
1206
  }
444
1207
  const demo = computed(() => !props.base);
445
1208
  const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
446
- const activePassword = ref(props.password);
1209
+ const rootEl = ref();
447
1210
  const roots = ref([]);
448
1211
  const loadingRoot = ref(false);
449
1212
  const errorMsg = ref("");
450
- const needsPassword = ref(false);
451
- const passwordInput = ref("");
1213
+ const refreshing = ref(false);
1214
+ let refreshSeq = 0;
1215
+ /** 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("");
452
1225
  const keyword = ref("");
453
1226
  const selected = ref(/* @__PURE__ */ new Set());
454
1227
  const toast = ref("");
455
1228
  const sortKey = ref("name");
456
1229
  const sortAsc = ref(true);
457
1230
  const siteTitle = ref("");
1231
+ /** OpenList's public `iframe_previews` setting (JSON text). */
1232
+ const iframePreviews = ref("");
458
1233
  const batchBusy = ref(false);
459
1234
  const batchProgress = ref(0);
460
1235
  const downloadCache = /* @__PURE__ */ new Map();
@@ -470,6 +1245,90 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
470
1245
  function isPasswordError(error) {
471
1246
  return /password|密码/i.test(errorText(error));
472
1247
  }
1248
+ function isNetworkError(error) {
1249
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
1250
+ }
1251
+ /** A failure toast; browser network errors are usually CORS, so say where to look. */
1252
+ function failureMessage(key, error) {
1253
+ 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
+ }
473
1332
  const DEMO_TREE = {
474
1333
  "/": [
475
1334
  {
@@ -501,6 +1360,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
501
1360
  size: 0,
502
1361
  is_dir: true,
503
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"
504
1369
  }
505
1370
  ],
506
1371
  "/assignments": [{
@@ -591,6 +1456,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
591
1456
  is_dir: false,
592
1457
  modified: "2026-01-05T19:02:00+08:00"
593
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
+ }],
594
1470
  "/slides": [{
595
1471
  name: "第一章 行列式.pptx",
596
1472
  size: 3145728,
@@ -603,6 +1479,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
603
1479
  modified: "2026-09-15T13:30:00+08:00"
604
1480
  }]
605
1481
  };
1482
+ /** Demo-only folder passwords, so the demo can show the password flow. */
1483
+ const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
606
1484
  class ApiError extends Error {
607
1485
  code;
608
1486
  constructor(message, code) {
@@ -610,16 +1488,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
610
1488
  this.code = code;
611
1489
  }
612
1490
  }
613
- function authHeaders() {
1491
+ /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1492
+ function authHeaders(json) {
614
1493
  return {
615
- "Content-Type": "application/json",
1494
+ ...json ? { "Content-Type": "application/json" } : {},
616
1495
  ...props.token ? { Authorization: props.token } : {}
617
1496
  };
618
1497
  }
619
1498
  async function request(apiPath, body) {
620
1499
  const response = await fetch(`${baseUrl.value}${apiPath}`, {
621
1500
  method: body === void 0 ? "GET" : "POST",
622
- headers: authHeaders(),
1501
+ headers: authHeaders(body !== void 0),
623
1502
  body: body === void 0 ? void 0 : JSON.stringify(body)
624
1503
  });
625
1504
  const payload = await response.json().catch(() => null);
@@ -627,9 +1506,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
627
1506
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
628
1507
  return payload.data;
629
1508
  }
630
- async function listDir(path, password = activePassword.value) {
1509
+ async function listDir(path, password) {
631
1510
  if (demo.value) {
632
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);
633
1514
  return DEMO_TREE[path] ?? [];
634
1515
  }
635
1516
  return (await request("/api/fs/list", {
@@ -659,25 +1540,28 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
659
1540
  try {
660
1541
  url = absoluteUrl((await request("/api/fs/get", {
661
1542
  path: node.path,
662
- password: node.password || activePassword.value
1543
+ password: passwordFor(node).password
663
1544
  }))?.raw_url ?? "");
664
1545
  } catch {
665
1546
  url = "";
666
1547
  }
667
- if (!url) {
668
- const encoded = node.path.split("/").map(encodeURIComponent).join("/");
669
- url = `${baseUrl.value}/d${encoded}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
670
- }
1548
+ if (!url) url = directUrl(node);
671
1549
  downloadCache.set(node.path, url);
672
1550
  return url;
673
1551
  }
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)}` : ""}`;
1555
+ }
674
1556
  async function loadSiteTitle() {
675
1557
  if (demo.value) return;
676
1558
  try {
677
1559
  const data = await request("/api/public/settings");
678
1560
  siteTitle.value = data?.site_title || data?.title || "";
1561
+ iframePreviews.value = data?.iframe_previews || "";
679
1562
  } catch {
680
1563
  siteTitle.value = "";
1564
+ iframePreviews.value = "";
681
1565
  }
682
1566
  }
683
1567
  function sortTree(nodes) {
@@ -714,95 +1598,297 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
714
1598
  const parts = path.split("/").filter(Boolean);
715
1599
  return parts[parts.length - 1] || path;
716
1600
  }
717
- async function loadChildren(node) {
1601
+ /**
1602
+ * Load a folder's children. A password error marks the folder locked instead of
1603
+ * toasting; `prompt` also shows its same-row input (and focuses it).
1604
+ */
1605
+ async function loadChildren(node, prompt = false) {
718
1606
  if (node.loaded) return true;
1607
+ if (node.locked) {
1608
+ if (prompt) startPrompt(node, prompt === "focus");
1609
+ return false;
1610
+ }
719
1611
  node.loading = true;
1612
+ const { password, rememberedAt } = passwordFor(node);
720
1613
  try {
721
- node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
1614
+ node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
722
1615
  sortTree(node.children);
723
1616
  node.loaded = true;
724
1617
  return true;
725
1618
  } catch (error) {
726
- if (isPasswordError(error)) needsPassword.value = true;
727
- showToast(t("folderFailed", { message: errorText(error) }));
1619
+ if (isPasswordError(error)) {
1620
+ node.locked = true;
1621
+ node.expanded = false;
1622
+ node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1623
+ if (prompt) startPrompt(node, prompt === "focus");
1624
+ } else showToast(failureMessage("folderFailed", error));
728
1625
  return false;
729
1626
  } finally {
730
1627
  node.loading = false;
731
1628
  }
732
1629
  }
733
- async function loadSourceRoot(source) {
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
+ /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1688
+ function looksLikeFile(path) {
1689
+ const name = sourceName(path);
1690
+ return fileKind(name) !== "other" || previewKind(name) !== "none";
1691
+ }
1692
+ async function loadSourceRoot(source, expanded) {
734
1693
  const path = source.path;
735
- const password = source.password ?? "";
1694
+ const configured = source.password ?? "";
736
1695
  let item = {
737
1696
  name: sourceName(path),
738
1697
  size: 0,
739
- is_dir: true,
1698
+ is_dir: !looksLikeFile(path),
740
1699
  modified: ""
741
1700
  };
742
- if (demo.value) {
743
- if (/\/[^/]+\.[^/]+$/.test(path)) item = {
744
- ...item,
745
- is_dir: false,
746
- modified: "2026-09-01T08:00:00+08:00"
747
- };
748
- } else try {
749
- const data = await request("/api/fs/get", {
750
- path,
751
- password: password || activePassword.value
752
- });
753
- item = {
754
- ...item,
755
- ...data
756
- };
757
- } catch {}
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
+ }
1732
+ }
758
1733
  const name = source.title || item.name || sourceName(path);
759
1734
  return {
760
1735
  ...item,
761
1736
  name,
762
1737
  path,
763
1738
  depth: 0,
764
- expanded: source.expanded ?? props.expanded,
1739
+ expanded,
765
1740
  loaded: false,
766
1741
  loading: false,
767
1742
  children: [],
768
- password,
1743
+ password: configured,
1744
+ configuredPassword: configured,
769
1745
  isRoot: true,
1746
+ locked,
1747
+ unresolved: locked,
1748
+ unlockError,
1749
+ prompting: locked && expanded,
770
1750
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
771
1751
  };
772
1752
  }
1753
+ class RootLockedError extends Error {
1754
+ rememberedAt;
1755
+ constructor(rememberedAt) {
1756
+ super("root locked");
1757
+ this.rememberedAt = rememberedAt;
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
1762
+ * what was open); without it the configured `expanded` defaults apply.
1763
+ */
1764
+ async function buildTree(expandedPaths) {
1765
+ let nodes;
1766
+ if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1767
+ else {
1768
+ const { password, rememberedAt } = rootPasswordFor();
1769
+ let items;
1770
+ try {
1771
+ items = await listDir(props.path, password);
1772
+ } catch (error) {
1773
+ if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1774
+ throw error;
1775
+ }
1776
+ nodes = toNodes(items, props.path, 0, rootPassword.value);
1777
+ sortTree(nodes);
1778
+ }
1779
+ const open = async (list) => {
1780
+ for (const node of list) {
1781
+ if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
1782
+ node.expanded = await loadChildren(node, "show");
1783
+ if (node.expanded && expandedPaths) await open(node.children);
1784
+ }
1785
+ };
1786
+ await open(nodes);
1787
+ return nodes;
1788
+ }
1789
+ /** First load, or a retry after an error: shows the loading state. */
773
1790
  async function loadRoot() {
1791
+ refreshSeq += 1;
1792
+ refreshing.value = false;
774
1793
  loadingRoot.value = true;
775
1794
  errorMsg.value = "";
776
- needsPassword.value = false;
777
1795
  try {
778
- if (props.sources.length) {
779
- const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
780
- roots.value = nodes;
781
- for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
1796
+ roots.value = await buildTree();
1797
+ rootLock.active = false;
1798
+ selected.value = /* @__PURE__ */ new Set();
1799
+ downloadCache.clear();
1800
+ } 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);
1806
+ } finally {
1807
+ loadingRoot.value = false;
1808
+ }
1809
+ }
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
+ /**
1818
+ * Refresh keeps the old rows in place (dimmed) until the new tree is ready, then
1819
+ * swaps it in. Open folders stay open; selected files that still exist stay selected.
1820
+ */
1821
+ async function refresh() {
1822
+ if (refreshing.value || loadingRoot.value) return;
1823
+ if (!roots.value.length || errorMsg.value || rootLock.active) {
1824
+ await loadRoot();
1825
+ return;
1826
+ }
1827
+ const seq = ++refreshSeq;
1828
+ refreshing.value = true;
1829
+ try {
1830
+ const fresh = await buildTree(expandedPathsOf(roots.value));
1831
+ if (seq !== refreshSeq) return;
1832
+ roots.value = fresh;
1833
+ const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
1834
+ selected.value = new Set([...selected.value].filter((path) => existing.has(path)));
1835
+ downloadCache.clear();
1836
+ } catch (error) {
1837
+ if (seq !== refreshSeq) return;
1838
+ if (error instanceof RootLockedError) {
1839
+ roots.value = [];
1840
+ rootLock.active = true;
1841
+ rootLock.error = dropStale(error.rememberedAt) ? "stale" : "wrong";
782
1842
  selected.value = /* @__PURE__ */ new Set();
783
- downloadCache.clear();
784
- return;
1843
+ } else showToast(failureMessage("refreshFailed", error));
1844
+ } finally {
1845
+ if (seq === refreshSeq) refreshing.value = false;
1846
+ }
1847
+ }
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();
785
1859
  }
786
- const items = await listDir(props.path);
787
- roots.value = toNodes(items, props.path, 0, activePassword.value);
788
- sortTree(roots.value);
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
+ });
789
1869
  selected.value = /* @__PURE__ */ new Set();
790
1870
  downloadCache.clear();
791
1871
  } catch (error) {
792
- const message = errorText(error);
793
- if (isPasswordError(error)) needsPassword.value = true;
794
- else errorMsg.value = message;
1872
+ if (isPasswordError(error)) {
1873
+ rootLock.error = "wrong";
1874
+ rootLock.input = "";
1875
+ focusUnlock(props.path);
1876
+ } else showToast(failureMessage("loadFailed", error));
795
1877
  } finally {
796
- loadingRoot.value = false;
1878
+ rootLock.busy = false;
797
1879
  }
798
1880
  }
799
1881
  async function toggleExpand(node) {
1882
+ if (node.locked) {
1883
+ startPrompt(node, true);
1884
+ return;
1885
+ }
800
1886
  if (!node.is_dir) return;
801
1887
  if (node.expanded) {
802
1888
  node.expanded = false;
803
1889
  return;
804
1890
  }
805
- if (!await loadChildren(node)) return;
1891
+ if (!await loadChildren(node, "focus")) return;
806
1892
  node.expanded = true;
807
1893
  }
808
1894
  const visible = computed(() => {
@@ -832,18 +1918,33 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
832
1918
  const hit = files.filter((file) => selected.value.has(file.path)).length;
833
1919
  return hit > 0 && hit < files.length;
834
1920
  }
835
- /** Load a directory and all of its sub-directories. */
1921
+ /**
1922
+ * Load a directory and all of its sub-directories. Stops at the first folder
1923
+ * that needs a password and returns it, so nothing is silently left out.
1924
+ */
836
1925
  async function loadTree(node) {
837
- if (!node.is_dir) return true;
838
- if (!await loadChildren(node)) return false;
839
- for (const child of node.children) if (child.is_dir && !await loadTree(child)) return false;
840
- return true;
1926
+ if (!node.is_dir) return { ok: true };
1927
+ if (!await loadChildren(node)) return node.locked ? {
1928
+ ok: false,
1929
+ locked: node
1930
+ } : { ok: false };
1931
+ for (const child of node.children) {
1932
+ if (!child.is_dir) continue;
1933
+ const result = await loadTree(child);
1934
+ if (!result.ok) {
1935
+ node.expanded = true;
1936
+ return result;
1937
+ }
1938
+ }
1939
+ return { ok: true };
841
1940
  }
842
1941
  async function toggleCheck(node, event) {
843
1942
  const input = event?.target;
844
1943
  if (node.is_dir && !isChecked(node)) {
845
- if (await loadTree(node)) node.expanded = true;
1944
+ const result = await loadTree(node);
1945
+ if (result.ok) node.expanded = true;
846
1946
  else {
1947
+ if (result.locked) startPrompt(result.locked, true);
847
1948
  if (input) input.checked = isChecked(node);
848
1949
  return;
849
1950
  }
@@ -928,21 +2029,55 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
928
2029
  showToast(t("downloadFailed", { message: errorText(error) }));
929
2030
  }
930
2031
  }
931
- const previewable = /\.(?:pdf|png|jpe?g|gif|webp|svg|txt|md|json|mp3|wav|mp4|webm|mov)$/i;
2032
+ /** Only types the dialog can actually render get a preview button. */
932
2033
  function canPreview(node) {
933
- return !node.is_dir && previewable.test(node.name);
2034
+ return !node.is_dir && previewKind(node.name) !== "none";
934
2035
  }
2036
+ const previewNode = ref(null);
2037
+ const previewSrc = ref("");
2038
+ const previewViewer = ref("");
2039
+ const previewResolving = ref(false);
2040
+ const previewError = ref("");
2041
+ let previewSeq = 0;
2042
+ /** Previewable files in the order they are listed, for ← / →. */
2043
+ const previewList = computed(() => visible.value.filter(canPreview));
2044
+ const previewPosition = computed(() => previewNode.value ? previewList.value.indexOf(previewNode.value) + 1 : 0);
935
2045
  async function preview(node) {
936
- if (demo.value) {
937
- showToast(t("demoPreview", { name: node.name }));
938
- return;
939
- }
2046
+ const seq = ++previewSeq;
2047
+ previewNode.value = node;
2048
+ previewSrc.value = "";
2049
+ previewViewer.value = "";
2050
+ previewError.value = "";
2051
+ previewResolving.value = false;
2052
+ if (demo.value || !canPreview(node)) return;
2053
+ previewResolving.value = true;
940
2054
  try {
941
- window.open(await resolveUrl(node), "_blank", "noopener");
2055
+ const url = await resolveUrl(node);
2056
+ if (seq !== previewSeq) return;
2057
+ 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
+ }
942
2066
  } catch (error) {
943
- showToast(t("previewFailed", { message: errorText(error) }));
2067
+ if (seq === previewSeq) previewError.value = errorText(error);
2068
+ } finally {
2069
+ if (seq === previewSeq) previewResolving.value = false;
944
2070
  }
945
2071
  }
2072
+ function stepPreview(delta) {
2073
+ const list = previewList.value;
2074
+ if (!list.length) return;
2075
+ preview(list[((previewNode.value ? list.indexOf(previewNode.value) : -1) + delta + list.length) % list.length]);
2076
+ }
2077
+ function closePreview() {
2078
+ previewSeq += 1;
2079
+ previewNode.value = null;
2080
+ }
946
2081
  async function batchDownload() {
947
2082
  const files = allFiles.value.filter((file) => selected.value.has(file.path));
948
2083
  if (!files.length) return;
@@ -982,11 +2117,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
982
2117
  batchBusy.value = false;
983
2118
  }
984
2119
  }
985
- function submitPassword() {
986
- activePassword.value = passwordInput.value;
987
- downloadCache.clear();
988
- loadRoot();
989
- }
990
2120
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
991
2121
  const stickyTop = ref(0);
992
2122
  function measureStickyTop() {
@@ -1000,6 +2130,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1000
2130
  stickyTop.value = top;
1001
2131
  }
1002
2132
  onMounted(() => {
2133
+ syncRemembered();
1003
2134
  loadRoot();
1004
2135
  loadSiteTitle();
1005
2136
  measureStickyTop();
@@ -1011,11 +2142,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1011
2142
  });
1012
2143
  return (_ctx, _cache) => {
1013
2144
  return openBlock(), createElementBlock("div", {
1014
- class: normalizeClass(["fl-root", { "fl-selecting": selected.value.size > 0 }]),
1015
- style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` })
2145
+ ref_key: "rootEl",
2146
+ ref: rootEl,
2147
+ class: normalizeClass(["fl-root", {
2148
+ "fl-selecting": selected.value.size > 0,
2149
+ "fl-refreshing": refreshing.value
2150
+ }]),
2151
+ style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` }),
2152
+ "aria-busy": refreshing.value || loadingRoot.value
1016
2153
  }, [
1017
- createElementVNode("div", _hoisted_1, [
1018
- selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_2, [createElementVNode("span", _hoisted_3, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_4, toDisplayString(t("totalSize", { size: unref(formatSize)(selectedBytes.value) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_5, [createElementVNode("span", _hoisted_6, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
2154
+ 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)])),
1019
2156
  selected.value.size ? (openBlock(), createElementBlock("button", {
1020
2157
  key: 2,
1021
2158
  class: "fl-clear",
@@ -1023,7 +2160,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1023
2160
  "aria-label": t("clearSelection"),
1024
2161
  title: t("clearSelection"),
1025
2162
  onClick: clearSelection
1026
- }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
2163
+ }, [_cache[10] || (_cache[10] = createElementVNode("svg", {
1027
2164
  class: "fl-clear-icon",
1028
2165
  viewBox: "0 0 24 24",
1029
2166
  width: "18",
@@ -1033,8 +2170,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1033
2170
  "stroke-width": "2.2",
1034
2171
  "stroke-linecap": "round",
1035
2172
  "aria-hidden": "true"
1036
- }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)), createElementVNode("span", _hoisted_9, toDisplayString(t("clear")), 1)], 8, _hoisted_8)) : createCommentVNode("v-if", true),
1037
- createElementVNode("div", _hoisted_10, [createElementVNode("label", _hoisted_11, [_cache[7] || (_cache[7] = createElementVNode("svg", {
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", {
1038
2175
  viewBox: "0 0 24 24",
1039
2176
  width: "15",
1040
2177
  height: "15",
@@ -1052,13 +2189,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1052
2189
  type: "text",
1053
2190
  placeholder: t("filter"),
1054
2191
  "aria-label": t("filter")
1055
- }, null, 8, _hoisted_12), [[vModelText, keyword.value]])]), createElementVNode("button", {
1056
- class: "fl-icon-btn",
2192
+ }, null, 8, _hoisted_13), [[vModelText, keyword.value]])]), createElementVNode("button", {
2193
+ class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
1057
2194
  type: "button",
1058
- "aria-label": t("refresh"),
1059
- title: t("refresh"),
1060
- onClick: loadRoot
1061
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
2195
+ disabled: refreshing.value || loadingRoot.value,
2196
+ "aria-label": refreshing.value ? t("refreshing") : t("refresh"),
2197
+ title: refreshing.value ? t("refreshing") : t("refresh"),
2198
+ onClick: refresh
2199
+ }, [..._cache[12] || (_cache[12] = [createElementVNode("svg", {
1062
2200
  viewBox: "0 0 24 24",
1063
2201
  width: "15",
1064
2202
  height: "15",
@@ -1068,14 +2206,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1068
2206
  "stroke-linecap": "round",
1069
2207
  "stroke-linejoin": "round",
1070
2208
  "aria-hidden": "true"
1071
- }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 8, _hoisted_13)]),
2209
+ }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 10, _hoisted_14)]),
1072
2210
  selected.value.size ? (openBlock(), createElementBlock("button", {
1073
2211
  key: 3,
1074
2212
  class: "fl-btn fl-btn-primary",
1075
2213
  type: "button",
1076
2214
  disabled: batchBusy.value,
1077
2215
  onClick: batchDownload
1078
- }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
2216
+ }, [_cache[13] || (_cache[13] = createElementVNode("svg", {
1079
2217
  viewBox: "0 0 24 24",
1080
2218
  width: "15",
1081
2219
  height: "15",
@@ -1089,24 +2227,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1089
2227
  createElementVNode("path", { d: "M12 3v12" }),
1090
2228
  createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1091
2229
  createElementVNode("path", { d: "M5 21h14" })
1092
- ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_14)) : createCommentVNode("v-if", true)
2230
+ ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_15)) : createCommentVNode("v-if", true)
1093
2231
  ]),
1094
- needsPassword.value ? (openBlock(), createElementBlock("div", _hoisted_15, [
1095
- createElementVNode("span", null, toDisplayString(t("passwordPrompt")), 1),
1096
- withDirectives(createElementVNode("input", {
1097
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => passwordInput.value = $event),
1098
- type: "password",
1099
- placeholder: t("passwordPlaceholder"),
1100
- "aria-label": t("passwordPlaceholder"),
1101
- onKeyup: withKeys(submitPassword, ["enter"])
1102
- }, null, 40, _hoisted_16), [[vModelText, passwordInput.value]]),
1103
- createElementVNode("button", {
1104
- class: "fl-btn fl-btn-primary",
1105
- type: "button",
1106
- onClick: submitPassword
1107
- }, toDisplayString(t("open")), 1)
1108
- ])) : createCommentVNode("v-if", true),
1109
- createElementVNode("div", _hoisted_17, [
2232
+ createElementVNode("div", _hoisted_16, [
2233
+ refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_17)) : createCommentVNode("v-if", true),
1110
2234
  createElementVNode("label", _hoisted_18, [createElementVNode("input", {
1111
2235
  type: "checkbox",
1112
2236
  "aria-label": t("selectAll"),
@@ -1118,23 +2242,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1118
2242
  class: "fl-sort-btn",
1119
2243
  type: "button",
1120
2244
  "aria-sort": sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0,
1121
- onClick: _cache[2] || (_cache[2] = ($event) => setSort("name"))
2245
+ onClick: _cache[1] || (_cache[1] = ($event) => setSort("name"))
1122
2246
  }, [createTextVNode(toDisplayString(t("columnName")), 1), createElementVNode("span", _hoisted_22, toDisplayString(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_21)]),
1123
2247
  createElementVNode("span", _hoisted_23, [createElementVNode("button", {
1124
2248
  class: "fl-sort-btn",
1125
2249
  type: "button",
1126
- onClick: _cache[3] || (_cache[3] = ($event) => setSort("size"))
2250
+ onClick: _cache[2] || (_cache[2] = ($event) => setSort("size"))
1127
2251
  }, [createTextVNode(toDisplayString(t("columnSize")), 1), createElementVNode("span", _hoisted_24, toDisplayString(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1128
2252
  createElementVNode("span", _hoisted_25, [createElementVNode("button", {
1129
2253
  class: "fl-sort-btn",
1130
2254
  type: "button",
1131
- onClick: _cache[4] || (_cache[4] = ($event) => setSort("modified"))
2255
+ onClick: _cache[3] || (_cache[3] = ($event) => setSort("modified"))
1132
2256
  }, [createTextVNode(toDisplayString(t("columnDate")), 1), createElementVNode("span", _hoisted_26, toDisplayString(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1133
2257
  createElementVNode("span", _hoisted_27, [createElementVNode("label", _hoisted_28, [
1134
2258
  createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
1135
2259
  createElementVNode("span", _hoisted_29, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
1136
2260
  withDirectives(createElementVNode("select", {
1137
- "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => sortValue.value = $event),
2261
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => sortValue.value = $event),
1138
2262
  "aria-label": t("sortBy")
1139
2263
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
1140
2264
  return openBlock(), createElementBlock("option", {
@@ -1144,49 +2268,148 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1144
2268
  }), 128))], 8, _hoisted_30), [[vModelSelect, sortValue.value]])
1145
2269
  ])])
1146
2270
  ]),
1147
- loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_33, [
2271
+ loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : rootLock.active ? (openBlock(), createElementBlock("form", {
2272
+ key: 1,
2273
+ class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.error }]),
2274
+ onSubmit: withModifiers(submitRootUnlock, ["prevent"])
2275
+ }, [
2276
+ _cache[15] || (_cache[15] = createElementVNode("svg", {
2277
+ 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)),
2294
+ createElementVNode("label", {
2295
+ 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, [
2300
+ withDirectives(createElementVNode("input", {
2301
+ id: `fl-root-unlock-${__props.path}`,
2302
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => rootLock.input = $event),
2303
+ type: "password",
2304
+ autocomplete: "off",
2305
+ "data-unlock": __props.path,
2306
+ 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]]),
2310
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2311
+ key: 0,
2312
+ class: "fl-unlock-remember",
2313
+ type: "button",
2314
+ "aria-pressed": rootLock.remember,
2315
+ "aria-label": t("rememberPassword"),
2316
+ 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),
2337
+ createElementVNode("button", {
2338
+ class: "fl-unlock-submit",
2339
+ type: "submit",
2340
+ disabled: rootLock.busy || !rootLock.input
2341
+ }, toDisplayString(t("open")), 9, _hoisted_38)
2342
+ ])
2343
+ ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_39, [
1148
2344
  createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
1149
- /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_34, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
1150
- /failed to fetch|networkerror/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_35, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
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),
1151
2347
  createElementVNode("button", {
1152
2348
  class: "fl-btn",
1153
2349
  type: "button",
1154
2350
  onClick: loadRoot
1155
2351
  }, toDisplayString(t("retry")), 1)
1156
- ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_36, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2352
+ ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_42, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
1157
2353
  (openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (node) => {
1158
2354
  return openBlock(), createElementBlock("div", {
1159
2355
  key: node.path,
1160
2356
  class: normalizeClass(["fl-row", {
1161
2357
  "fl-row-dir": node.is_dir,
1162
2358
  "fl-row-root": node.isRoot,
1163
- "fl-row-selected": !node.is_dir && selected.value.has(node.path)
2359
+ "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
1164
2363
  }]),
1165
2364
  style: normalizeStyle({ "--fl-depth": node.depth })
1166
2365
  }, [
1167
- createElementVNode("label", _hoisted_37, [createElementVNode("input", {
2366
+ createElementVNode("label", _hoisted_43, [createElementVNode("input", {
1168
2367
  type: "checkbox",
2368
+ disabled: node.locked,
1169
2369
  "aria-label": t("selectItem", { name: node.name }),
1170
2370
  checked: isChecked(node),
1171
2371
  ".indeterminate": isIndeterminate(node),
1172
2372
  onChange: ($event) => toggleCheck(node, $event)
1173
- }, null, 40, _hoisted_38)]),
2373
+ }, null, 40, _hoisted_44)]),
1174
2374
  createElementVNode("span", {
1175
2375
  class: "fl-cell fl-cell-name",
1176
2376
  title: node.isRoot ? node.path : void 0
1177
2377
  }, [
1178
- _cache[12] || (_cache[12] = createElementVNode("span", {
2378
+ _cache[20] || (_cache[20] = createElementVNode("span", {
1179
2379
  class: "fl-indent",
1180
2380
  "aria-hidden": "true"
1181
2381
  }, null, -1)),
1182
- node.is_dir ? (openBlock(), createElementBlock("button", {
2382
+ node.locked ? (openBlock(), createElementBlock("button", {
1183
2383
  key: 0,
2384
+ class: "fl-arrow fl-lock",
2385
+ type: "button",
2386
+ "aria-label": t("unlockFolder", { name: node.name }),
2387
+ title: t("unlockFolder", { name: node.name }),
2388
+ 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", {
2406
+ key: 1,
1184
2407
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
1185
2408
  type: "button",
1186
2409
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1187
2410
  "aria-expanded": node.expanded,
1188
2411
  onClick: ($event) => toggleExpand(node)
1189
- }, [..._cache[10] || (_cache[10] = [createElementVNode("svg", {
2412
+ }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1190
2413
  viewBox: "0 0 24 24",
1191
2414
  width: "14",
1192
2415
  height: "14",
@@ -1196,86 +2419,251 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1196
2419
  "stroke-linecap": "round",
1197
2420
  "stroke-linejoin": "round",
1198
2421
  "aria-hidden": "true"
1199
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_40)) : (openBlock(), createElementBlock("span", _hoisted_41)),
1200
- node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_42, [..._cache[11] || (_cache[11] = [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", {
1201
- key: 3,
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", {
2424
+ key: 4,
1202
2425
  class: "fl-type",
1203
2426
  "data-kind": unref(fileKind)(node.name),
1204
2427
  "aria-hidden": "true"
1205
- }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_43)),
1206
- createElementVNode("span", _hoisted_44, [createElementVNode("span", {
2428
+ }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_50)),
2429
+ createElementVNode("span", _hoisted_51, [createElementVNode("span", {
1207
2430
  class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
1208
- onClick: ($event) => node.is_dir ? toggleExpand(node) : download(node)
1209
- }, toDisplayString(node.name), 11, _hoisted_45), createElementVNode("span", _hoisted_46, [node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("folderItems", { count: folderFiles(node).length })) + " · " + toDisplayString(unref(formatSize)(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))])]),
1210
- node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(t("folderItems", { count: folderFiles(node).length })), 1)) : createCommentVNode("v-if", true),
1211
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_48, "…")) : createCommentVNode("v-if", true)
1212
- ], 8, _hoisted_39),
1213
- createElementVNode("span", _hoisted_49, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
1214
- createElementVNode("span", _hoisted_50, toDisplayString(unref(formatDate)(node.modified)), 1),
1215
- createElementVNode("span", _hoisted_51, [node.is_dir ? (openBlock(), createElementBlock("button", {
1216
- key: 0,
1217
- class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
1218
- type: "button",
1219
- "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1220
- "aria-expanded": node.expanded,
1221
- onClick: ($event) => toggleExpand(node)
1222
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1223
- viewBox: "0 0 24 24",
1224
- width: "18",
1225
- height: "18",
1226
- fill: "none",
1227
- stroke: "currentColor",
1228
- "stroke-width": "2.2",
1229
- "stroke-linecap": "round",
1230
- "stroke-linejoin": "round",
1231
- "aria-hidden": "true"
1232
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_52)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
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", {
2435
+ key: 6,
2436
+ id: `fl-unlock-error-${node.path}`,
2437
+ class: "fl-unlock-message",
2438
+ 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", {
2441
+ key: 7,
2442
+ class: "fl-remembered",
2443
+ type: "button",
2444
+ "aria-label": `${t("forgetPassword")} ${node.name}`,
2445
+ title: t("forgetPassword"),
2446
+ 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", {
1233
2469
  key: 0,
1234
- class: "fl-action fl-download",
1235
- type: "button",
1236
- "aria-label": `${t("preview")} ${node.name}`,
1237
- title: t("preview"),
1238
- onClick: ($event) => preview(node)
1239
- }, [..._cache[14] || (_cache[14] = [createElementVNode("svg", {
1240
- viewBox: "0 0 24 24",
1241
- width: "16",
1242
- height: "16",
1243
- fill: "none",
1244
- stroke: "currentColor",
1245
- "stroke-width": "2",
1246
- "stroke-linecap": "round",
1247
- "stroke-linejoin": "round",
1248
- "aria-hidden": "true"
1249
- }, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
1250
- cx: "12",
1251
- cy: "12",
1252
- r: "3"
1253
- })], -1)])], 8, _hoisted_53)) : createCommentVNode("v-if", true), createElementVNode("button", {
1254
- class: "fl-action fl-download",
1255
- type: "button",
1256
- "aria-label": `${t("download")} ${node.name}`,
1257
- title: t("download"),
1258
- onClick: ($event) => download(node)
1259
- }, [..._cache[15] || (_cache[15] = [createElementVNode("svg", {
1260
- viewBox: "0 0 24 24",
1261
- width: "16",
1262
- height: "16",
1263
- fill: "none",
1264
- stroke: "currentColor",
1265
- "stroke-width": "2",
1266
- "stroke-linecap": "round",
1267
- "stroke-linejoin": "round",
1268
- "aria-hidden": "true"
2470
+ class: "fl-unlock fl-unlock-row",
2471
+ onSubmit: withModifiers(($event) => submitUnlock(node), ["prevent"]),
2472
+ onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node), ["prevent"]), ["esc"])
1269
2473
  }, [
1270
- createElementVNode("path", { d: "M12 3v12" }),
1271
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1272
- createElementVNode("path", { d: "M5 21h14" })
1273
- ], -1)])], 8, _hoisted_54)], 64))])
2474
+ _cache[23] || (_cache[23] = createElementVNode("svg", {
2475
+ 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)),
2492
+ withDirectives(createElementVNode("input", {
2493
+ "onUpdate:modelValue": ($event) => node.unlockInput = $event,
2494
+ type: "password",
2495
+ autocomplete: "off",
2496
+ "data-unlock": node.path,
2497
+ placeholder: node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"),
2498
+ "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]]),
2502
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2503
+ key: 0,
2504
+ class: "fl-unlock-remember",
2505
+ type: "button",
2506
+ "aria-pressed": !!node.unlockRemember,
2507
+ "aria-label": t("rememberPassword"),
2508
+ 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),
2529
+ createElementVNode("button", {
2530
+ class: "fl-unlock-submit",
2531
+ type: "submit",
2532
+ disabled: node.unlockBusy || !node.unlockInput
2533
+ }, toDisplayString(t("open")), 9, _hoisted_61),
2534
+ createElementVNode("button", {
2535
+ class: "fl-unlock-cancel",
2536
+ type: "button",
2537
+ "aria-label": t("cancel"),
2538
+ 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", {
2554
+ key: 0,
2555
+ class: "fl-action fl-arrow-end fl-lock",
2556
+ type: "button",
2557
+ "aria-label": t("unlockFolder", { name: node.name }),
2558
+ 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", {
2576
+ key: 1,
2577
+ class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
2578
+ type: "button",
2579
+ "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
2580
+ "aria-expanded": node.expanded,
2581
+ 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", {
2593
+ key: 0,
2594
+ class: "fl-action fl-download",
2595
+ type: "button",
2596
+ "aria-label": `${t("preview")} ${node.name}`,
2597
+ title: t("preview"),
2598
+ 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", {
2614
+ class: "fl-action fl-download",
2615
+ type: "button",
2616
+ "aria-label": `${t("download")} ${node.name}`,
2617
+ title: t("download"),
2618
+ 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)])
2634
+ ], 64))
1274
2635
  ], 6);
1275
2636
  }), 128)),
1276
- createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1)]),
1277
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_56, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
1278
- ], 6);
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),
2639
+ createVNode(_sfc_main$1, {
2640
+ open: !!previewNode.value,
2641
+ file: previewNode.value,
2642
+ labels: text.value,
2643
+ src: previewSrc.value,
2644
+ viewer: previewViewer.value,
2645
+ resolving: previewResolving.value,
2646
+ error: previewError.value,
2647
+ demo: demo.value,
2648
+ position: previewPosition.value,
2649
+ total: previewPosition.value ? previewList.value.length : 0,
2650
+ 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))
2654
+ }, null, 8, [
2655
+ "open",
2656
+ "file",
2657
+ "labels",
2658
+ "src",
2659
+ "viewer",
2660
+ "resolving",
2661
+ "error",
2662
+ "demo",
2663
+ "position",
2664
+ "total"
2665
+ ])
2666
+ ], 14, _hoisted_1);
1279
2667
  };
1280
2668
  }
1281
2669
  });
@@ -1300,4 +2688,4 @@ function enhanceAppWithFileList({ app }) {
1300
2688
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
1301
2689
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
1302
2690
  }
1303
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };
2691
+ export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, fillPreviewUrl, formatDate, formatLabel, formatSize, iframePreviewsFor, joinPath, previewKind, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };