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
- import { computed, defineComponent, mergeProps, onBeforeUnmount, onMounted, ref, unref, useSSRContext } from "vue";
2
- import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
1
+ import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, reactive, ref, unref, useSSRContext, watch } from "vue";
2
+ import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
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,6 +308,440 @@ 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 _sfc_main$1 = /*@__PURE__*/ defineComponent({
455
+ __name: "PreviewDialog",
456
+ __ssrInlineRender: true,
457
+ props: {
458
+ open: { type: Boolean },
459
+ file: {},
460
+ labels: {},
461
+ src: { default: "" },
462
+ viewer: { default: "" },
463
+ resolving: {
464
+ type: Boolean,
465
+ default: false
466
+ },
467
+ error: { default: "" },
468
+ demo: {
469
+ type: Boolean,
470
+ default: false
471
+ },
472
+ position: { default: 0 },
473
+ total: { default: 0 },
474
+ maxTextBytes: { default: 1048576 },
475
+ maxPdfBytes: { default: 52428800 }
476
+ },
477
+ emits: [
478
+ "close",
479
+ "prev",
480
+ "next",
481
+ "download"
482
+ ],
483
+ setup(__props, { emit: __emit }) {
484
+ /**
485
+ * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
486
+ *
487
+ * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
488
+ * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
489
+ * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
490
+ */
491
+ const props = __props;
492
+ function t(key, values) {
493
+ return formatLabel(props.labels[key], values);
494
+ }
495
+ const dialog = ref();
496
+ const kind = computed(() => props.file ? previewKind(props.file.name) : "none");
497
+ const loadingBody = ref(false);
498
+ const bodyFailed = ref(false);
499
+ const textContent = ref("");
500
+ const truncated = ref(false);
501
+ const blobUrl = ref("");
502
+ const rotation = ref(0);
503
+ const actualSize = ref(false);
504
+ let controller;
505
+ let loadSeq = 0;
506
+ const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
507
+ const mode = computed(() => {
508
+ if (props.demo) return "demo";
509
+ if (props.error || bodyFailed.value) return "failed";
510
+ if (kind.value === "none") return "unsupported";
511
+ if (tooLarge.value) return "tooLarge";
512
+ return kind.value;
513
+ });
514
+ const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
515
+ const infoMessage = computed(() => {
516
+ const file = props.file;
517
+ if (!file) return "";
518
+ if (mode.value === "demo") return t("demoPreview", { name: file.name });
519
+ if (mode.value === "tooLarge") return t("previewTooLarge", { size: formatSize(file.size) });
520
+ if (mode.value === "failed") return t("previewUnavailable");
521
+ return t("previewUnsupported");
522
+ });
523
+ function revokeBlob() {
524
+ if (blobUrl.value) URL.revokeObjectURL(blobUrl.value);
525
+ blobUrl.value = "";
526
+ }
527
+ function resetBody() {
528
+ controller?.abort();
529
+ controller = void 0;
530
+ loadSeq += 1;
531
+ revokeBlob();
532
+ loadingBody.value = false;
533
+ bodyFailed.value = false;
534
+ textContent.value = "";
535
+ truncated.value = false;
536
+ rotation.value = 0;
537
+ actualSize.value = false;
538
+ }
539
+ async function readPrefix(response, limit) {
540
+ const reader = response.body?.getReader();
541
+ if (!reader) {
542
+ const all = new Uint8Array(await response.arrayBuffer());
543
+ return {
544
+ bytes: all.slice(0, limit),
545
+ more: all.length > limit
546
+ };
547
+ }
548
+ const chunks = [];
549
+ let length = 0;
550
+ let more = false;
551
+ while (true) {
552
+ const { done, value } = await reader.read();
553
+ if (done) break;
554
+ chunks.push(value);
555
+ length += value.length;
556
+ if (length >= limit) {
557
+ more = length > limit;
558
+ await reader.cancel().catch(() => {});
559
+ break;
560
+ }
561
+ }
562
+ const bytes = new Uint8Array(Math.min(length, limit));
563
+ let offset = 0;
564
+ for (const chunk of chunks) {
565
+ const part = chunk.subarray(0, bytes.length - offset);
566
+ bytes.set(part, offset);
567
+ offset += part.length;
568
+ if (offset >= bytes.length) break;
569
+ }
570
+ return {
571
+ bytes,
572
+ more
573
+ };
574
+ }
575
+ async function loadBody() {
576
+ resetBody();
577
+ const file = props.file;
578
+ if (!props.open || !file || props.demo || !props.src || props.error) return;
579
+ if (kind.value !== "text" && !(kind.value === "pdf" && !props.viewer && !tooLarge.value)) return;
580
+ const seq = loadSeq;
581
+ controller = new AbortController();
582
+ loadingBody.value = true;
583
+ try {
584
+ if (kind.value === "text") {
585
+ const limit = props.maxTextBytes;
586
+ const response = await fetch(props.src, {
587
+ headers: { Range: `bytes=0-${limit - 1}` },
588
+ signal: controller.signal
589
+ });
590
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
591
+ const { bytes, more } = await readPrefix(response, limit);
592
+ if (seq !== loadSeq) return;
593
+ textContent.value = new TextDecoder("utf-8").decode(bytes);
594
+ truncated.value = more || file.size > limit;
595
+ } else {
596
+ const response = await fetch(props.src, { signal: controller.signal });
597
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
598
+ const buffer = await response.arrayBuffer();
599
+ if (seq !== loadSeq) return;
600
+ blobUrl.value = URL.createObjectURL(new Blob([buffer], { type: "application/pdf" }));
601
+ }
602
+ } catch {
603
+ if (seq === loadSeq && !controller?.signal.aborted) bodyFailed.value = true;
604
+ } finally {
605
+ if (seq === loadSeq) loadingBody.value = false;
606
+ }
607
+ }
608
+ watch(() => [
609
+ props.open,
610
+ props.file?.name,
611
+ props.src,
612
+ props.viewer,
613
+ props.error,
614
+ props.demo
615
+ ], () => void loadBody(), { immediate: true });
616
+ watch(() => props.open, (open) => {
617
+ const el = dialog.value;
618
+ if (!el) return;
619
+ if (open && !el.open) {
620
+ if (typeof el.showModal === "function") el.showModal();
621
+ else el.setAttribute("open", "");
622
+ } else if (!open && el.open) {
623
+ if (typeof el.close === "function") el.close();
624
+ else el.removeAttribute("open");
625
+ }
626
+ }, { flush: "post" });
627
+ onBeforeUnmount(resetBody);
628
+ return (_ctx, _push, _parent, _attrs) => {
629
+ _push(`<dialog${ssrRenderAttrs(mergeProps({
630
+ ref_key: "dialog",
631
+ ref: dialog,
632
+ class: "fl-preview",
633
+ "aria-label": __props.file?.name
634
+ }, _attrs))}>`);
635
+ if (__props.open && __props.file) {
636
+ _push(`<div class="fl-preview-panel"><header class="fl-preview-head"><span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><div class="fl-preview-title"><span class="fl-preview-name">${ssrInterpolate(__props.file.name)}</span><span class="fl-preview-meta">${ssrInterpolate(unref(formatSize)(__props.file.size))} · ${ssrInterpolate(unref(formatDate)(__props.file.modified))}</span></div>`);
637
+ if (__props.total > 1 || mode.value === "image" && !busy.value) {
638
+ _push(`<div class="fl-preview-tools fl-preview-nav">`);
639
+ if (__props.total > 1) _push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 6-6 6 6 6"></path></svg></button><span class="fl-preview-count">${ssrInterpolate(__props.position)} / ${ssrInterpolate(__props.total)}</span><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("next"))}${ssrRenderAttr("title", t("next"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button><!--]-->`);
640
+ else _push(`<!---->`);
641
+ if (mode.value === "image" && !busy.value) {
642
+ _push(`<!--[--><button class="fl-preview-tool fl-preview-rotate" type="button"${ssrRenderAttr("aria-label", t("rotate"))}${ssrRenderAttr("title", t("rotate"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button><button class="fl-preview-tool fl-preview-zoom" type="button"${ssrRenderAttr("aria-pressed", actualSize.value)}${ssrRenderAttr("aria-label", t("actualSize"))}${ssrRenderAttr("title", t("actualSize"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path>`);
643
+ if (!actualSize.value) _push(`<path d="M11 8v6M8 11h6"></path>`);
644
+ else _push(`<path d="M8 11h6"></path>`);
645
+ _push(`</svg></button><!--]-->`);
646
+ } else _push(`<!---->`);
647
+ _push(`</div>`);
648
+ } else _push(`<!---->`);
649
+ _push(`<div class="fl-preview-tools fl-preview-actions">`);
650
+ if (!__props.demo) _push(`<button class="fl-preview-tool fl-preview-download" type="button"${ssrRenderAttr("aria-label", t("download"))}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button>`);
651
+ else _push(`<!---->`);
652
+ _push(`<button class="fl-preview-tool fl-preview-close" type="button" autofocus${ssrRenderAttr("aria-label", t("close"))}${ssrRenderAttr("title", t("close"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg></button></div></header><div class="fl-preview-body"${ssrRenderAttr("data-mode", busy.value ? "loading" : mode.value)}>`);
653
+ if (busy.value) _push(`<div class="fl-preview-status" role="status">${ssrInterpolate(t("loading"))}</div>`);
654
+ else if (mode.value === "image") _push(`<img class="${ssrRenderClass([{ "fl-preview-actual": actualSize.value }, "fl-preview-image"])}"${ssrRenderAttr("src", __props.src)}${ssrRenderAttr("alt", __props.file.name)} style="${ssrRenderStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 })}">`);
655
+ else if (mode.value === "video") _push(`<video class="fl-preview-video"${ssrRenderAttr("src", __props.src)} controls playsinline preload="metadata"></video>`);
656
+ else if (mode.value === "audio") _push(`<audio class="fl-preview-audio"${ssrRenderAttr("src", __props.src)} controls preload="metadata"></audio>`);
657
+ else if (mode.value === "text") {
658
+ _push(`<div class="fl-preview-text">`);
659
+ if (truncated.value) _push(`<p class="fl-preview-note">${ssrInterpolate(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) }))}</p>`);
660
+ else _push(`<!---->`);
661
+ _push(`<pre>${ssrInterpolate(textContent.value)}</pre></div>`);
662
+ } else if (mode.value === "pdf") _push(`<iframe class="fl-preview-frame"${ssrRenderAttr("src", __props.viewer || blobUrl.value)}${ssrRenderAttr("title", __props.file.name)}></iframe>`);
663
+ else {
664
+ _push(`<div class="fl-preview-info"><span class="fl-type fl-preview-badge"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><p class="fl-preview-message">${ssrInterpolate(infoMessage.value)}</p>`);
665
+ if (!__props.demo) _push(`<button class="fl-btn fl-btn-primary" type="button"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(t("download"))}</button>`);
666
+ else _push(`<!---->`);
667
+ _push(`</div>`);
668
+ }
669
+ _push(`</div></div>`);
670
+ } else _push(`<!---->`);
671
+ _push(`</dialog>`);
672
+ };
673
+ }
674
+ });
675
+ const _sfc_setup$1 = _sfc_main$1.setup;
676
+ _sfc_main$1.setup = (props, ctx) => {
677
+ const ssrContext = useSSRContext();
678
+ (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/PreviewDialog.vue");
679
+ return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
680
+ };
681
+ // src/client/passwords.ts
682
+ /**
683
+ * Folder passwords remembered for the current browser tab.
684
+ *
685
+ * Stored in `sessionStorage`, so they live only in this tab and are gone when
686
+ * it closes. Keyed by OpenList base URL, then by folder path; a remembered
687
+ * folder also covers its sub-folders. Every access is guarded: private mode
688
+ * or blocked storage simply means nothing is remembered.
689
+ */
690
+ const PREFIX = "vitepress-plugin-filelist:passwords:";
691
+ function store() {
692
+ try {
693
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ function read(base) {
699
+ try {
700
+ const raw = store()?.getItem(PREFIX + base);
701
+ const parsed = raw ? JSON.parse(raw) : {};
702
+ return parsed && typeof parsed === "object" ? parsed : {};
703
+ } catch {
704
+ return {};
705
+ }
706
+ }
707
+ function write(base, map) {
708
+ try {
709
+ const target = store();
710
+ if (!target) return;
711
+ if (Object.keys(map).length) target.setItem(PREFIX + base, JSON.stringify(map));
712
+ else target.removeItem(PREFIX + base);
713
+ } catch {}
714
+ }
715
+ function normalise(path) {
716
+ return path.replace(/\/+$/, "") || "/";
717
+ }
718
+ /** Whether `path` is `folder` itself or inside it. */
719
+ function isWithin(path, folder) {
720
+ const p = normalise(path);
721
+ const f = normalise(folder);
722
+ return f === "/" || p === f || p.startsWith(`${f}/`);
723
+ }
724
+ /** The closest remembered folder at or above `path`, if any. */
725
+ function findRememberedPassword(base, path) {
726
+ let best = null;
727
+ for (const [folder, password] of Object.entries(read(base))) {
728
+ if (typeof password !== "string" || !isWithin(path, folder)) continue;
729
+ if (!best || folder.length > best.path.length) best = {
730
+ path: folder,
731
+ password
732
+ };
733
+ }
734
+ return best;
735
+ }
736
+ function forgetFolderPassword(base, path) {
737
+ const map = read(base);
738
+ delete map[normalise(path)];
739
+ write(base, map);
740
+ }
741
+ /** Folders with a remembered password, for showing the "remembered" tag. */
742
+ function rememberedFolders(base) {
743
+ return Object.keys(read(base));
744
+ }
271
745
  // src/client/FileList.vue
272
746
  const _sfc_main = /*@__PURE__*/ defineComponent({
273
747
  __name: "FileList",
@@ -283,6 +757,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
283
757
  token: { default: "" },
284
758
  password: { default: "" },
285
759
  title: { default: "" },
760
+ rememberPassword: {
761
+ type: Boolean,
762
+ default: true
763
+ },
286
764
  locale: { default: "zh-CN" },
287
765
  labels: {}
288
766
  },
@@ -301,18 +779,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
301
779
  }
302
780
  const demo = computed(() => !props.base);
303
781
  const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
304
- const activePassword = ref(props.password);
782
+ const rootEl = ref();
305
783
  const roots = ref([]);
306
784
  const loadingRoot = ref(false);
307
785
  const errorMsg = ref("");
308
- const needsPassword = ref(false);
309
- const passwordInput = ref("");
786
+ const refreshing = ref(false);
787
+ let refreshSeq = 0;
788
+ /** Single-path mode: the listed folder itself needs a password. */
789
+ const rootLock = reactive({
790
+ active: false,
791
+ input: "",
792
+ error: "",
793
+ remember: false,
794
+ busy: false
795
+ });
796
+ /** Password the visitor typed for the listed folder (single-path mode). */
797
+ const rootPassword = ref("");
310
798
  const keyword = ref("");
311
799
  const selected = ref(/* @__PURE__ */ new Set());
312
800
  const toast = ref("");
313
801
  const sortKey = ref("name");
314
802
  const sortAsc = ref(true);
315
803
  const siteTitle = ref("");
804
+ /** OpenList's public `iframe_previews` setting (JSON text). */
805
+ const iframePreviews = ref("");
316
806
  const batchBusy = ref(false);
317
807
  const batchProgress = ref(0);
318
808
  const downloadCache = /* @__PURE__ */ new Map();
@@ -328,6 +818,85 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
328
818
  function isPasswordError(error) {
329
819
  return /password|密码/i.test(errorText(error));
330
820
  }
821
+ function isNetworkError(error) {
822
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
823
+ }
824
+ /** A failure toast; browser network errors are usually CORS, so say where to look. */
825
+ function failureMessage(key, error) {
826
+ const message = t(key, { message: errorText(error) });
827
+ return isNetworkError(error) ? `${message} ${t("networkHint")}` : message;
828
+ }
829
+ /** Total size for display: an empty total reads `0 B`, not `-`. */
830
+ function formatTotal(bytes) {
831
+ return bytes > 0 ? formatSize(bytes) : "0 B";
832
+ }
833
+ const canRemember = computed(() => props.rememberPassword !== false);
834
+ const passwordScope = computed(() => baseUrl.value || "demo");
835
+ /** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
836
+ const typedPasswords = /* @__PURE__ */ new Map();
837
+ function typedPasswordFor(path) {
838
+ let best = "";
839
+ let bestPath = "";
840
+ for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
841
+ best = password;
842
+ bestPath = folder;
843
+ }
844
+ return best;
845
+ }
846
+ /** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
847
+ const rememberedPaths = ref(/* @__PURE__ */ new Set());
848
+ function syncRemembered() {
849
+ rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
850
+ }
851
+ function storedPassword(path) {
852
+ return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
853
+ }
854
+ /**
855
+ * Password for an item: its own or inherited one first (site-configured or
856
+ * typed), then one remembered in this tab, then the component's `password`.
857
+ */
858
+ function passwordFor(node) {
859
+ if (node.password) return { password: node.password };
860
+ const typed = typedPasswordFor(node.path);
861
+ if (typed) return { password: typed };
862
+ const stored = storedPassword(node.path);
863
+ if (stored) return {
864
+ password: stored.password,
865
+ rememberedAt: stored.path
866
+ };
867
+ return { password: props.password };
868
+ }
869
+ function rootPasswordFor() {
870
+ if (rootPassword.value) return { password: rootPassword.value };
871
+ const stored = storedPassword(props.path);
872
+ if (stored) return {
873
+ password: stored.password,
874
+ rememberedAt: stored.path
875
+ };
876
+ return { password: props.password };
877
+ }
878
+ /** A remembered password stopped working: drop it so we do not keep retrying it. */
879
+ function dropStale(path) {
880
+ if (!path) return false;
881
+ forgetFolderPassword(passwordScope.value, path);
882
+ syncRemembered();
883
+ return true;
884
+ }
885
+ function focusUnlock(path) {
886
+ nextTick(() => {
887
+ (rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
888
+ });
889
+ }
890
+ /** Show the same-row password input; `focus` only for user actions, never on page load. */
891
+ function startPrompt(node, focus) {
892
+ node.prompting = true;
893
+ if (focus) focusUnlock(node.path);
894
+ }
895
+ function unlockErrorText(error) {
896
+ if (error === "wrong") return t("passwordWrong");
897
+ if (error === "stale") return t("passwordStale");
898
+ return "";
899
+ }
331
900
  const DEMO_TREE = {
332
901
  "/": [
333
902
  {
@@ -359,6 +928,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
359
928
  size: 0,
360
929
  is_dir: true,
361
930
  modified: "2026-09-01T08:00:00+08:00"
931
+ },
932
+ {
933
+ name: "加密示例(密码 1234)",
934
+ size: 0,
935
+ is_dir: true,
936
+ modified: "2026-10-20T18:00:00+08:00"
362
937
  }
363
938
  ],
364
939
  "/assignments": [{
@@ -449,6 +1024,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
449
1024
  is_dir: false,
450
1025
  modified: "2026-01-05T19:02:00+08:00"
451
1026
  }],
1027
+ "/加密示例(密码 1234)": [{
1028
+ name: "期末答案(教师版).pdf",
1029
+ size: 2621440,
1030
+ is_dir: false,
1031
+ modified: "2026-10-20T18:00:00+08:00"
1032
+ }, {
1033
+ name: "成绩登记表.xlsx",
1034
+ size: 48128,
1035
+ is_dir: false,
1036
+ modified: "2026-10-21T09:12:00+08:00"
1037
+ }],
452
1038
  "/slides": [{
453
1039
  name: "第一章 行列式.pptx",
454
1040
  size: 3145728,
@@ -461,6 +1047,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
461
1047
  modified: "2026-09-15T13:30:00+08:00"
462
1048
  }]
463
1049
  };
1050
+ /** Demo-only folder passwords, so the demo can show the password flow. */
1051
+ const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
464
1052
  class ApiError extends Error {
465
1053
  code;
466
1054
  constructor(message, code) {
@@ -468,16 +1056,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
468
1056
  this.code = code;
469
1057
  }
470
1058
  }
471
- function authHeaders() {
1059
+ /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1060
+ function authHeaders(json) {
472
1061
  return {
473
- "Content-Type": "application/json",
1062
+ ...json ? { "Content-Type": "application/json" } : {},
474
1063
  ...props.token ? { Authorization: props.token } : {}
475
1064
  };
476
1065
  }
477
1066
  async function request(apiPath, body) {
478
1067
  const response = await fetch(`${baseUrl.value}${apiPath}`, {
479
1068
  method: body === void 0 ? "GET" : "POST",
480
- headers: authHeaders(),
1069
+ headers: authHeaders(body !== void 0),
481
1070
  body: body === void 0 ? void 0 : JSON.stringify(body)
482
1071
  });
483
1072
  const payload = await response.json().catch(() => null);
@@ -485,9 +1074,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
485
1074
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
486
1075
  return payload.data;
487
1076
  }
488
- async function listDir(path, password = activePassword.value) {
1077
+ async function listDir(path, password) {
489
1078
  if (demo.value) {
490
1079
  await new Promise((resolve) => setTimeout(resolve, 220));
1080
+ const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
1081
+ if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
491
1082
  return DEMO_TREE[path] ?? [];
492
1083
  }
493
1084
  return (await request("/api/fs/list", {
@@ -498,13 +1089,47 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
498
1089
  refresh: false
499
1090
  }))?.content ?? [];
500
1091
  }
1092
+ function absoluteUrl(raw) {
1093
+ if (!raw) return "";
1094
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
1095
+ const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
1096
+ try {
1097
+ return new URL(raw, `${root}/`).href;
1098
+ } catch {
1099
+ return raw;
1100
+ }
1101
+ }
1102
+ async function resolveUrl(node) {
1103
+ if (demo.value) return "";
1104
+ if (node.raw_url) return absoluteUrl(node.raw_url);
1105
+ const cached = downloadCache.get(node.path);
1106
+ if (cached) return cached;
1107
+ let url = "";
1108
+ try {
1109
+ url = absoluteUrl((await request("/api/fs/get", {
1110
+ path: node.path,
1111
+ password: passwordFor(node).password
1112
+ }))?.raw_url ?? "");
1113
+ } catch {
1114
+ url = "";
1115
+ }
1116
+ if (!url) url = directUrl(node);
1117
+ downloadCache.set(node.path, url);
1118
+ return url;
1119
+ }
1120
+ /** OpenList's signed `/d/` download link for a file. */
1121
+ function directUrl(node) {
1122
+ return `${baseUrl.value}/d${encodePath(node.path)}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
1123
+ }
501
1124
  async function loadSiteTitle() {
502
1125
  if (demo.value) return;
503
1126
  try {
504
1127
  const data = await request("/api/public/settings");
505
1128
  siteTitle.value = data?.site_title || data?.title || "";
1129
+ iframePreviews.value = data?.iframe_previews || "";
506
1130
  } catch {
507
1131
  siteTitle.value = "";
1132
+ iframePreviews.value = "";
508
1133
  }
509
1134
  }
510
1135
  function sortTree(nodes) {
@@ -533,84 +1158,154 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
533
1158
  const parts = path.split("/").filter(Boolean);
534
1159
  return parts[parts.length - 1] || path;
535
1160
  }
536
- async function loadChildren(node) {
1161
+ /**
1162
+ * Load a folder's children. A password error marks the folder locked instead of
1163
+ * toasting; `prompt` also shows its same-row input (and focuses it).
1164
+ */
1165
+ async function loadChildren(node, prompt = false) {
537
1166
  if (node.loaded) return true;
1167
+ if (node.locked) {
1168
+ if (prompt) startPrompt(node, prompt === "focus");
1169
+ return false;
1170
+ }
538
1171
  node.loading = true;
1172
+ const { password, rememberedAt } = passwordFor(node);
539
1173
  try {
540
- node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
1174
+ node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
541
1175
  sortTree(node.children);
542
1176
  node.loaded = true;
543
1177
  return true;
544
1178
  } catch (error) {
545
- if (isPasswordError(error)) needsPassword.value = true;
546
- showToast(t("folderFailed", { message: errorText(error) }));
1179
+ if (isPasswordError(error)) {
1180
+ node.locked = true;
1181
+ node.expanded = false;
1182
+ node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1183
+ if (prompt) startPrompt(node, prompt === "focus");
1184
+ } else showToast(failureMessage("folderFailed", error));
547
1185
  return false;
548
1186
  } finally {
549
1187
  node.loading = false;
550
1188
  }
551
1189
  }
552
- async function loadSourceRoot(source) {
1190
+ /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1191
+ function looksLikeFile(path) {
1192
+ const name = sourceName(path);
1193
+ return fileKind(name) !== "other" || previewKind(name) !== "none";
1194
+ }
1195
+ async function loadSourceRoot(source, expanded) {
553
1196
  const path = source.path;
554
- const password = source.password ?? "";
1197
+ const configured = source.password ?? "";
555
1198
  let item = {
556
1199
  name: sourceName(path),
557
1200
  size: 0,
558
- is_dir: true,
1201
+ is_dir: !looksLikeFile(path),
559
1202
  modified: ""
560
1203
  };
561
- if (demo.value) {
562
- if (/\/[^/]+\.[^/]+$/.test(path)) item = {
563
- ...item,
564
- is_dir: false,
565
- modified: "2026-09-01T08:00:00+08:00"
566
- };
567
- } else try {
568
- const data = await request("/api/fs/get", {
569
- path,
570
- password: password || activePassword.value
571
- });
572
- item = {
573
- ...item,
574
- ...data
575
- };
576
- } catch {}
1204
+ let locked = false;
1205
+ let unlockError = "";
1206
+ if (!demo.value) {
1207
+ const { password, rememberedAt } = configured ? {
1208
+ password: configured,
1209
+ rememberedAt: void 0
1210
+ } : (() => {
1211
+ const stored = storedPassword(path);
1212
+ return stored ? {
1213
+ password: stored.password,
1214
+ rememberedAt: stored.path
1215
+ } : {
1216
+ password: props.password,
1217
+ rememberedAt: void 0
1218
+ };
1219
+ })();
1220
+ try {
1221
+ const data = await request("/api/fs/get", {
1222
+ path,
1223
+ password
1224
+ });
1225
+ item = {
1226
+ ...item,
1227
+ ...data
1228
+ };
1229
+ } catch (error) {
1230
+ if (isPasswordError(error)) {
1231
+ locked = true;
1232
+ unlockError = dropStale(rememberedAt) ? "stale" : "";
1233
+ }
1234
+ }
1235
+ }
577
1236
  const name = source.title || item.name || sourceName(path);
578
1237
  return {
579
1238
  ...item,
580
1239
  name,
581
1240
  path,
582
1241
  depth: 0,
583
- expanded: source.expanded ?? props.expanded,
1242
+ expanded,
584
1243
  loaded: false,
585
1244
  loading: false,
586
1245
  children: [],
587
- password,
1246
+ password: configured,
1247
+ configuredPassword: configured,
588
1248
  isRoot: true,
1249
+ locked,
1250
+ unresolved: locked,
1251
+ unlockError,
1252
+ prompting: locked && expanded,
589
1253
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
590
1254
  };
591
1255
  }
1256
+ class RootLockedError extends Error {
1257
+ rememberedAt;
1258
+ constructor(rememberedAt) {
1259
+ super("root locked");
1260
+ this.rememberedAt = rememberedAt;
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
1265
+ * what was open); without it the configured `expanded` defaults apply.
1266
+ */
1267
+ async function buildTree(expandedPaths) {
1268
+ let nodes;
1269
+ if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1270
+ else {
1271
+ const { password, rememberedAt } = rootPasswordFor();
1272
+ let items;
1273
+ try {
1274
+ items = await listDir(props.path, password);
1275
+ } catch (error) {
1276
+ if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1277
+ throw error;
1278
+ }
1279
+ nodes = toNodes(items, props.path, 0, rootPassword.value);
1280
+ sortTree(nodes);
1281
+ }
1282
+ const open = async (list) => {
1283
+ for (const node of list) {
1284
+ if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
1285
+ node.expanded = await loadChildren(node, "show");
1286
+ if (node.expanded && expandedPaths) await open(node.children);
1287
+ }
1288
+ };
1289
+ await open(nodes);
1290
+ return nodes;
1291
+ }
1292
+ /** First load, or a retry after an error: shows the loading state. */
592
1293
  async function loadRoot() {
1294
+ refreshSeq += 1;
1295
+ refreshing.value = false;
593
1296
  loadingRoot.value = true;
594
1297
  errorMsg.value = "";
595
- needsPassword.value = false;
596
1298
  try {
597
- if (props.sources.length) {
598
- const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
599
- roots.value = nodes;
600
- for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
601
- selected.value = /* @__PURE__ */ new Set();
602
- downloadCache.clear();
603
- return;
604
- }
605
- const items = await listDir(props.path);
606
- roots.value = toNodes(items, props.path, 0, activePassword.value);
607
- sortTree(roots.value);
1299
+ roots.value = await buildTree();
1300
+ rootLock.active = false;
608
1301
  selected.value = /* @__PURE__ */ new Set();
609
1302
  downloadCache.clear();
610
1303
  } catch (error) {
611
- const message = errorText(error);
612
- if (isPasswordError(error)) needsPassword.value = true;
613
- else errorMsg.value = message;
1304
+ if (error instanceof RootLockedError) {
1305
+ roots.value = [];
1306
+ rootLock.active = true;
1307
+ rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
1308
+ } else errorMsg.value = errorText(error);
614
1309
  } finally {
615
1310
  loadingRoot.value = false;
616
1311
  }
@@ -675,9 +1370,75 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
675
1370
  resortAll();
676
1371
  }
677
1372
  });
678
- const previewable = /\.(?:pdf|png|jpe?g|gif|webp|svg|txt|md|json|mp3|wav|mp4|webm|mov)$/i;
1373
+ function triggerUrl(url) {
1374
+ const anchor = document.createElement("a");
1375
+ anchor.href = url;
1376
+ anchor.target = "_blank";
1377
+ anchor.rel = "noopener";
1378
+ document.body.appendChild(anchor);
1379
+ anchor.click();
1380
+ anchor.remove();
1381
+ }
1382
+ async function download(node) {
1383
+ if (node.is_dir) return;
1384
+ if (demo.value) {
1385
+ showToast(t("demoDownload", { name: node.name }));
1386
+ return;
1387
+ }
1388
+ try {
1389
+ triggerUrl(await resolveUrl(node));
1390
+ } catch (error) {
1391
+ showToast(t("downloadFailed", { message: errorText(error) }));
1392
+ }
1393
+ }
1394
+ /** Only types the dialog can actually render get a preview button. */
679
1395
  function canPreview(node) {
680
- return !node.is_dir && previewable.test(node.name);
1396
+ return !node.is_dir && previewKind(node.name) !== "none";
1397
+ }
1398
+ const previewNode = ref(null);
1399
+ const previewSrc = ref("");
1400
+ const previewViewer = ref("");
1401
+ const previewResolving = ref(false);
1402
+ const previewError = ref("");
1403
+ let previewSeq = 0;
1404
+ /** Previewable files in the order they are listed, for ← / →. */
1405
+ const previewList = computed(() => visible.value.filter(canPreview));
1406
+ const previewPosition = computed(() => previewNode.value ? previewList.value.indexOf(previewNode.value) + 1 : 0);
1407
+ async function preview(node) {
1408
+ const seq = ++previewSeq;
1409
+ previewNode.value = node;
1410
+ previewSrc.value = "";
1411
+ previewViewer.value = "";
1412
+ previewError.value = "";
1413
+ previewResolving.value = false;
1414
+ if (demo.value || !canPreview(node)) return;
1415
+ previewResolving.value = true;
1416
+ try {
1417
+ const url = await resolveUrl(node);
1418
+ if (seq !== previewSeq) return;
1419
+ previewSrc.value = url;
1420
+ if (previewKind(node.name) === "pdf") {
1421
+ const viewer = iframePreviewsFor(node.name, iframePreviews.value)[0];
1422
+ if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
1423
+ name: node.name,
1424
+ rawUrl: url,
1425
+ downloadUrl: directUrl(node)
1426
+ });
1427
+ }
1428
+ } catch (error) {
1429
+ if (seq === previewSeq) previewError.value = errorText(error);
1430
+ } finally {
1431
+ if (seq === previewSeq) previewResolving.value = false;
1432
+ }
1433
+ }
1434
+ function stepPreview(delta) {
1435
+ const list = previewList.value;
1436
+ if (!list.length) return;
1437
+ preview(list[((previewNode.value ? list.indexOf(previewNode.value) : -1) + delta + list.length) % list.length]);
1438
+ }
1439
+ function closePreview() {
1440
+ previewSeq += 1;
1441
+ previewNode.value = null;
681
1442
  }
682
1443
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
683
1444
  const stickyTop = ref(0);
@@ -692,6 +1453,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
692
1453
  stickyTop.value = top;
693
1454
  }
694
1455
  onMounted(() => {
1456
+ syncRemembered();
695
1457
  loadRoot();
696
1458
  loadSiteTitle();
697
1459
  measureStickyTop();
@@ -703,10 +1465,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
703
1465
  });
704
1466
  return (_ctx, _push, _parent, _attrs) => {
705
1467
  _push(`<div${ssrRenderAttrs(mergeProps({
706
- class: ["fl-root", { "fl-selecting": selected.value.size > 0 }],
707
- style: { "--fl-sticky-top-auto": `${stickyTop.value}px` }
1468
+ ref_key: "rootEl",
1469
+ ref: rootEl,
1470
+ class: ["fl-root", {
1471
+ "fl-selecting": selected.value.size > 0,
1472
+ "fl-refreshing": refreshing.value
1473
+ }],
1474
+ style: { "--fl-sticky-top-auto": `${stickyTop.value}px` },
1475
+ "aria-busy": refreshing.value || loadingRoot.value
708
1476
  }, _attrs))}><div class="fl-bar">`);
709
- if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: unref(formatSize)(selectedBytes.value) }))}</span></div>`);
1477
+ if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: formatTotal(selectedBytes.value) }))}</span></div>`);
710
1478
  else {
711
1479
  _push(`<div class="fl-title"><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
712
1480
  if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
@@ -715,19 +1483,27 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
715
1483
  }
716
1484
  if (selected.value.size) _push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}><svg class="fl-clear-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg><span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
717
1485
  else _push(`<!---->`);
718
- _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="fl-icon-btn" type="button"${ssrRenderAttr("aria-label", t("refresh"))}${ssrRenderAttr("title", t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
1486
+ _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="${ssrRenderClass([{ "fl-spinning": refreshing.value }, "fl-icon-btn fl-refresh"])}" type="button"${ssrIncludeBooleanAttr(refreshing.value || loadingRoot.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", refreshing.value ? t("refreshing") : t("refresh"))}${ssrRenderAttr("title", refreshing.value ? t("refreshing") : t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
719
1487
  if (selected.value.size) _push(`<button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch"))}</button>`);
720
1488
  else _push(`<!---->`);
721
- _push(`</div>`);
722
- if (needsPassword.value) _push(`<div class="fl-password"><span>${ssrInterpolate(t("passwordPrompt"))}</span><input${ssrRenderAttr("value", passwordInput.value)} type="password"${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordPlaceholder"))}><button class="fl-btn fl-btn-primary" type="button">${ssrInterpolate(t("open"))}</button></div>`);
1489
+ _push(`</div><div class="fl-row fl-head">`);
1490
+ if (refreshing.value) _push(`<span class="fl-progress" aria-hidden="true"></span>`);
723
1491
  else _push(`<!---->`);
724
- _push(`<div class="fl-row fl-head"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
1492
+ _push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
725
1493
  ssrRenderList(sortOptions.value, (option) => {
726
1494
  _push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
727
1495
  });
728
1496
  _push(`<!--]--></select></label></span></div>`);
729
1497
  if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
730
- else if (errorMsg.value) {
1498
+ else if (rootLock.active) {
1499
+ _push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.error }, "fl-unlock-root"])}"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><label class="fl-unlock-label"${ssrRenderAttr("for", `fl-root-unlock-${__props.path}`)}>${ssrInterpolate(t("passwordPrompt"))}</label>`);
1500
+ if (rootLock.error) _push(`<span id="fl-root-unlock-error" class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(rootLock.error))}</span>`);
1501
+ else _push(`<!---->`);
1502
+ _push(`<span class="fl-unlock"><input${ssrRenderAttr("id", `fl-root-unlock-${__props.path}`)}${ssrRenderAttr("value", rootLock.input)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", __props.path)}${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-invalid", !!rootLock.error)}${ssrRenderAttr("aria-describedby", rootLock.error ? "fl-root-unlock-error" : void 0)}>`);
1503
+ if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", rootLock.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
1504
+ else _push(`<!---->`);
1505
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(rootLock.busy || !rootLock.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button></span></form>`);
1506
+ } else if (errorMsg.value) {
731
1507
  _push(`<div class="fl-status fl-error">${ssrInterpolate(t("loadFailed", { message: errorMsg.value }))} `);
732
1508
  if (/guest/i.test(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("guestHint"))}</p>`);
733
1509
  else _push(`<!---->`);
@@ -741,37 +1517,71 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
741
1517
  _push(`<div class="${ssrRenderClass([{
742
1518
  "fl-row-dir": node.is_dir,
743
1519
  "fl-row-root": node.isRoot,
744
- "fl-row-selected": !node.is_dir && selected.value.has(node.path)
745
- }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
746
- if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1520
+ "fl-row-selected": !node.is_dir && selected.value.has(node.path),
1521
+ "fl-row-locked": node.locked,
1522
+ "fl-row-prompting": node.locked && node.prompting,
1523
+ "fl-row-error": node.locked && node.prompting && !!node.unlockError
1524
+ }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(node.locked) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
1525
+ if (node.locked) _push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1526
+ else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
747
1527
  else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
748
1528
  if (node.is_dir) _push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><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"></path></svg>`);
749
1529
  else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
750
1530
  _push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}">${ssrInterpolate(node.name)}</span><span class="fl-meta">`);
751
1531
  if (node.is_dir) {
752
1532
  _push(`<!--[-->`);
753
- if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(unref(formatSize)(unref(totalSize)(folderFiles(node))))}<!--]-->`);
1533
+ if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(formatTotal(unref(totalSize)(folderFiles(node))))}<!--]-->`);
754
1534
  else _push(`<!---->`);
755
1535
  _push(`<!--]-->`);
756
1536
  } else _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
757
1537
  _push(`</span></span>`);
758
1538
  if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))}</span>`);
759
1539
  else _push(`<!---->`);
1540
+ if (node.locked && node.prompting && node.unlockError) _push(`<span${ssrRenderAttr("id", `fl-unlock-error-${node.path}`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(node.unlockError))}</span>`);
1541
+ else _push(`<!---->`);
1542
+ if (!node.locked && rememberedPaths.value.has(node.path)) _push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg> ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
1543
+ else _push(`<!---->`);
760
1544
  if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
761
1545
  else _push(`<!---->`);
762
- _push(`</span><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
763
- if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
764
- else {
765
- _push(`<!--[-->`);
766
- if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
1546
+ _push(`</span>`);
1547
+ if (node.locked && node.prompting) {
1548
+ _push(`<form class="fl-unlock fl-unlock-row"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><input${ssrRenderAttr("value", node.unlockInput)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", node.path)}${ssrRenderAttr("placeholder", node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordFor", { name: node.name }))}${ssrRenderAttr("aria-invalid", !!node.unlockError)}${ssrRenderAttr("aria-describedby", node.unlockError ? `fl-unlock-error-${node.path}` : void 0)}>`);
1549
+ if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", !!node.unlockRemember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
767
1550
  else _push(`<!---->`);
768
- _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
1551
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(node.unlockBusy || !node.unlockInput) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button><button class="fl-unlock-cancel" type="button"${ssrRenderAttr("aria-label", t("cancel"))}${ssrRenderAttr("title", t("cancel"))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg></button></form>`);
1552
+ } else {
1553
+ _push(`<!--[--><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
1554
+ if (node.locked) _push(`<button class="fl-action fl-arrow-end fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1555
+ else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1556
+ else if (!node.locked) {
1557
+ _push(`<!--[-->`);
1558
+ if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
1559
+ else _push(`<!---->`);
1560
+ _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
1561
+ } else _push(`<!---->`);
1562
+ _push(`</span><!--]-->`);
769
1563
  }
770
- _push(`</span></div>`);
1564
+ _push(`</div>`);
771
1565
  });
772
- _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(loadedBytes.value) }))}</span></div>`);
1566
+ _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: formatTotal(loadedBytes.value) }))}</span></div>`);
773
1567
  if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
774
1568
  else _push(`<!---->`);
1569
+ _push(ssrRenderComponent(_sfc_main$1, {
1570
+ open: !!previewNode.value,
1571
+ file: previewNode.value,
1572
+ labels: text.value,
1573
+ src: previewSrc.value,
1574
+ viewer: previewViewer.value,
1575
+ resolving: previewResolving.value,
1576
+ error: previewError.value,
1577
+ demo: demo.value,
1578
+ position: previewPosition.value,
1579
+ total: previewPosition.value ? previewList.value.length : 0,
1580
+ onClose: closePreview,
1581
+ onPrev: ($event) => stepPreview(-1),
1582
+ onNext: ($event) => stepPreview(1),
1583
+ onDownload: ($event) => previewNode.value && download(previewNode.value)
1584
+ }, null, _parent));
775
1585
  _push(`</div>`);
776
1586
  };
777
1587
  }
@@ -803,4 +1613,4 @@ function enhanceAppWithFileList({ app }) {
803
1613
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
804
1614
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
805
1615
  }
806
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };
1616
+ 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 };