vitepress-plugin-filelist 0.3.0 → 0.4.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, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys } from "vue";
3
3
  // src/client/locales.ts
4
4
  const fileListLocales = {
5
5
  "zh-CN": {
@@ -43,7 +43,16 @@ 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},完整内容请下载"
47
56
  },
48
57
  "en-US": {
49
58
  allFiles: "All files",
@@ -86,7 +95,16 @@ const fileListLocales = {
86
95
  selectAll: "Select all",
87
96
  selectItem: "Select {name}",
88
97
  expand: "Expand {name}",
89
- collapse: "Collapse {name}"
98
+ collapse: "Collapse {name}",
99
+ close: "Close",
100
+ previous: "Previous",
101
+ next: "Next",
102
+ rotate: "Rotate",
103
+ actualSize: "Actual size",
104
+ previewUnsupported: "This file type cannot be previewed here. Download it to view.",
105
+ previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
106
+ previewTooLarge: "This file is large ({size}). Download it to view.",
107
+ textTruncated: "Showing the first {size} only. Download for the full file."
90
108
  }
91
109
  };
92
110
  function formatLabel(template, values = {}) {
@@ -268,6 +286,595 @@ function compareNodes(a, b, key, ascending = true) {
268
286
  else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
269
287
  return ascending ? result : -result;
270
288
  }
289
+ const PREVIEW_EXTS = {
290
+ image: [
291
+ "png",
292
+ "jpg",
293
+ "jpeg",
294
+ "gif",
295
+ "webp",
296
+ "svg",
297
+ "bmp",
298
+ "ico",
299
+ "avif"
300
+ ],
301
+ video: [
302
+ "mp4",
303
+ "webm",
304
+ "m4v",
305
+ "mov",
306
+ "ogv"
307
+ ],
308
+ audio: [
309
+ "mp3",
310
+ "wav",
311
+ "ogg",
312
+ "oga",
313
+ "m4a",
314
+ "aac",
315
+ "flac",
316
+ "opus"
317
+ ],
318
+ text: [
319
+ "txt",
320
+ "md",
321
+ "markdown",
322
+ "log",
323
+ "json",
324
+ "csv",
325
+ "tsv",
326
+ "xml",
327
+ "yml",
328
+ "yaml",
329
+ "toml",
330
+ "ini",
331
+ "conf",
332
+ "properties",
333
+ "js",
334
+ "mjs",
335
+ "cjs",
336
+ "ts",
337
+ "tsx",
338
+ "jsx",
339
+ "vue",
340
+ "css",
341
+ "scss",
342
+ "less",
343
+ "html",
344
+ "htm",
345
+ "py",
346
+ "java",
347
+ "kt",
348
+ "c",
349
+ "h",
350
+ "cpp",
351
+ "hpp",
352
+ "cs",
353
+ "go",
354
+ "rs",
355
+ "rb",
356
+ "php",
357
+ "sh",
358
+ "bat",
359
+ "ps1",
360
+ "sql",
361
+ "srt",
362
+ "vtt",
363
+ "ass",
364
+ "lrc",
365
+ "gitignore",
366
+ "env"
367
+ ],
368
+ pdf: ["pdf"]
369
+ };
370
+ const PREVIEW_KINDS = {};
371
+ for (const [kind, exts] of Object.entries(PREVIEW_EXTS)) for (const ext of exts) PREVIEW_KINDS[ext] = kind;
372
+ function lowerExt(name) {
373
+ const dot = name.lastIndexOf(".");
374
+ return dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
375
+ }
376
+ /**
377
+ * How a file is shown in the preview dialog. Office documents and anything
378
+ * else the browser cannot render are `none` (info + download).
379
+ */
380
+ function previewKind(name) {
381
+ return PREVIEW_KINDS[lowerExt(name)] ?? "none";
382
+ }
383
+ /**
384
+ * Viewers from OpenList's public `iframe_previews` setting that match a file,
385
+ * in the order OpenList lists them. Keys are comma-separated extensions or a
386
+ * `/regex/flags` tested against the extension, as in OpenList's frontend.
387
+ */
388
+ function iframePreviewsFor(name, setting) {
389
+ if (!setting) return [];
390
+ let parsed;
391
+ try {
392
+ parsed = JSON.parse(setting);
393
+ } catch {
394
+ return [];
395
+ }
396
+ if (!parsed || typeof parsed !== "object") return [];
397
+ const ext = lowerExt(name);
398
+ const out = [];
399
+ for (const [key, viewers] of Object.entries(parsed)) {
400
+ let hit = false;
401
+ if (key.startsWith("/")) {
402
+ const end = key.lastIndexOf("/");
403
+ try {
404
+ hit = end > 0 && new RegExp(key.slice(1, end), key.slice(end + 1)).test(ext);
405
+ } catch {
406
+ hit = false;
407
+ }
408
+ } else hit = key.split(",").map((item) => item.trim().toLowerCase()).includes(ext);
409
+ if (!hit || !viewers || typeof viewers !== "object") continue;
410
+ for (const [label, scheme] of Object.entries(viewers)) if (typeof scheme === "string" && /^https?:\/\//i.test(scheme)) out.push({
411
+ label,
412
+ scheme
413
+ });
414
+ }
415
+ return out;
416
+ }
417
+ function applyUrlOps(placeholder, url) {
418
+ const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
419
+ let value = url;
420
+ for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
421
+ return value;
422
+ }
423
+ /**
424
+ * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
425
+ * (signed download URL), each optionally prefixed with `e` (URI-encode) or
426
+ * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
427
+ */
428
+ function fillPreviewUrl(scheme, args) {
429
+ 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));
430
+ }
431
+ // src/client/PreviewDialog.vue
432
+ const _hoisted_1$1 = ["aria-label"];
433
+ const _hoisted_2$1 = {
434
+ key: 0,
435
+ class: "fl-preview-panel"
436
+ };
437
+ const _hoisted_3$1 = { class: "fl-preview-head" };
438
+ const _hoisted_4$1 = ["data-kind"];
439
+ const _hoisted_5$1 = { class: "fl-preview-title" };
440
+ const _hoisted_6$1 = { class: "fl-preview-name" };
441
+ const _hoisted_7$1 = { class: "fl-preview-meta" };
442
+ const _hoisted_8$1 = {
443
+ key: 0,
444
+ class: "fl-preview-tools fl-preview-nav"
445
+ };
446
+ const _hoisted_9$1 = ["aria-label", "title"];
447
+ const _hoisted_10$1 = { class: "fl-preview-count" };
448
+ const _hoisted_11$1 = ["aria-label", "title"];
449
+ const _hoisted_12$1 = ["aria-label", "title"];
450
+ const _hoisted_13$1 = [
451
+ "aria-pressed",
452
+ "aria-label",
453
+ "title"
454
+ ];
455
+ const _hoisted_14$1 = {
456
+ viewBox: "0 0 24 24",
457
+ width: "17",
458
+ height: "17",
459
+ fill: "none",
460
+ stroke: "currentColor",
461
+ "stroke-width": "2",
462
+ "stroke-linecap": "round",
463
+ "aria-hidden": "true"
464
+ };
465
+ const _hoisted_15$1 = {
466
+ key: 0,
467
+ d: "M11 8v6M8 11h6"
468
+ };
469
+ const _hoisted_16$1 = {
470
+ key: 1,
471
+ d: "M8 11h6"
472
+ };
473
+ const _hoisted_17$1 = { class: "fl-preview-tools fl-preview-actions" };
474
+ const _hoisted_18$1 = ["aria-label", "title"];
475
+ const _hoisted_19$1 = ["aria-label", "title"];
476
+ const _hoisted_20$1 = ["data-mode"];
477
+ const _hoisted_21$1 = {
478
+ key: 0,
479
+ class: "fl-preview-status",
480
+ role: "status"
481
+ };
482
+ const _hoisted_22$1 = ["src", "alt"];
483
+ const _hoisted_23$1 = ["src"];
484
+ const _hoisted_24$1 = ["src"];
485
+ const _hoisted_25$1 = {
486
+ key: 4,
487
+ class: "fl-preview-text"
488
+ };
489
+ const _hoisted_26$1 = {
490
+ key: 0,
491
+ class: "fl-preview-note"
492
+ };
493
+ const _hoisted_27$1 = ["src", "title"];
494
+ const _hoisted_28$1 = {
495
+ key: 6,
496
+ class: "fl-preview-info"
497
+ };
498
+ const _hoisted_29$1 = ["data-kind"];
499
+ const _hoisted_30$1 = { class: "fl-preview-message" };
500
+ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
501
+ __name: "PreviewDialog",
502
+ props: {
503
+ open: { type: Boolean },
504
+ file: {},
505
+ labels: {},
506
+ src: { default: "" },
507
+ viewer: { default: "" },
508
+ resolving: {
509
+ type: Boolean,
510
+ default: false
511
+ },
512
+ error: { default: "" },
513
+ demo: {
514
+ type: Boolean,
515
+ default: false
516
+ },
517
+ position: { default: 0 },
518
+ total: { default: 0 },
519
+ maxTextBytes: { default: 1048576 },
520
+ maxPdfBytes: { default: 52428800 }
521
+ },
522
+ emits: [
523
+ "close",
524
+ "prev",
525
+ "next",
526
+ "download"
527
+ ],
528
+ setup(__props, { emit: __emit }) {
529
+ /**
530
+ * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
531
+ *
532
+ * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
533
+ * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
534
+ * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
535
+ */
536
+ const props = __props;
537
+ const emit = __emit;
538
+ function t(key, values) {
539
+ return formatLabel(props.labels[key], values);
540
+ }
541
+ const dialog = ref();
542
+ const kind = computed(() => props.file ? previewKind(props.file.name) : "none");
543
+ const loadingBody = ref(false);
544
+ const bodyFailed = ref(false);
545
+ const textContent = ref("");
546
+ const truncated = ref(false);
547
+ const blobUrl = ref("");
548
+ const rotation = ref(0);
549
+ const actualSize = ref(false);
550
+ let controller;
551
+ let loadSeq = 0;
552
+ const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
553
+ const mode = computed(() => {
554
+ if (props.demo) return "demo";
555
+ if (props.error || bodyFailed.value) return "failed";
556
+ if (kind.value === "none") return "unsupported";
557
+ if (tooLarge.value) return "tooLarge";
558
+ return kind.value;
559
+ });
560
+ const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
561
+ const infoMessage = computed(() => {
562
+ const file = props.file;
563
+ if (!file) return "";
564
+ if (mode.value === "demo") return t("demoPreview", { name: file.name });
565
+ if (mode.value === "tooLarge") return t("previewTooLarge", { size: formatSize(file.size) });
566
+ if (mode.value === "failed") return t("previewUnavailable");
567
+ return t("previewUnsupported");
568
+ });
569
+ function revokeBlob() {
570
+ if (blobUrl.value) URL.revokeObjectURL(blobUrl.value);
571
+ blobUrl.value = "";
572
+ }
573
+ function resetBody() {
574
+ controller?.abort();
575
+ controller = void 0;
576
+ loadSeq += 1;
577
+ revokeBlob();
578
+ loadingBody.value = false;
579
+ bodyFailed.value = false;
580
+ textContent.value = "";
581
+ truncated.value = false;
582
+ rotation.value = 0;
583
+ actualSize.value = false;
584
+ }
585
+ async function readPrefix(response, limit) {
586
+ const reader = response.body?.getReader();
587
+ if (!reader) {
588
+ const all = new Uint8Array(await response.arrayBuffer());
589
+ return {
590
+ bytes: all.slice(0, limit),
591
+ more: all.length > limit
592
+ };
593
+ }
594
+ const chunks = [];
595
+ let length = 0;
596
+ let more = false;
597
+ while (true) {
598
+ const { done, value } = await reader.read();
599
+ if (done) break;
600
+ chunks.push(value);
601
+ length += value.length;
602
+ if (length >= limit) {
603
+ more = length > limit;
604
+ await reader.cancel().catch(() => {});
605
+ break;
606
+ }
607
+ }
608
+ const bytes = new Uint8Array(Math.min(length, limit));
609
+ let offset = 0;
610
+ for (const chunk of chunks) {
611
+ const part = chunk.subarray(0, bytes.length - offset);
612
+ bytes.set(part, offset);
613
+ offset += part.length;
614
+ if (offset >= bytes.length) break;
615
+ }
616
+ return {
617
+ bytes,
618
+ more
619
+ };
620
+ }
621
+ async function loadBody() {
622
+ resetBody();
623
+ const file = props.file;
624
+ if (!props.open || !file || props.demo || !props.src || props.error) return;
625
+ if (kind.value !== "text" && !(kind.value === "pdf" && !props.viewer && !tooLarge.value)) return;
626
+ const seq = loadSeq;
627
+ controller = new AbortController();
628
+ loadingBody.value = true;
629
+ try {
630
+ if (kind.value === "text") {
631
+ const limit = props.maxTextBytes;
632
+ const response = await fetch(props.src, {
633
+ headers: { Range: `bytes=0-${limit - 1}` },
634
+ signal: controller.signal
635
+ });
636
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
637
+ const { bytes, more } = await readPrefix(response, limit);
638
+ if (seq !== loadSeq) return;
639
+ textContent.value = new TextDecoder("utf-8").decode(bytes);
640
+ truncated.value = more || file.size > limit;
641
+ } else {
642
+ const response = await fetch(props.src, { signal: controller.signal });
643
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
644
+ const buffer = await response.arrayBuffer();
645
+ if (seq !== loadSeq) return;
646
+ blobUrl.value = URL.createObjectURL(new Blob([buffer], { type: "application/pdf" }));
647
+ }
648
+ } catch {
649
+ if (seq === loadSeq && !controller?.signal.aborted) bodyFailed.value = true;
650
+ } finally {
651
+ if (seq === loadSeq) loadingBody.value = false;
652
+ }
653
+ }
654
+ watch(() => [
655
+ props.open,
656
+ props.file?.name,
657
+ props.src,
658
+ props.viewer,
659
+ props.error,
660
+ props.demo
661
+ ], () => void loadBody(), { immediate: true });
662
+ watch(() => props.open, (open) => {
663
+ const el = dialog.value;
664
+ if (!el) return;
665
+ if (open && !el.open) {
666
+ if (typeof el.showModal === "function") el.showModal();
667
+ else el.setAttribute("open", "");
668
+ } else if (!open && el.open) {
669
+ if (typeof el.close === "function") el.close();
670
+ else el.removeAttribute("open");
671
+ }
672
+ }, { flush: "post" });
673
+ onBeforeUnmount(resetBody);
674
+ function onNativeClose() {
675
+ if (props.open) emit("close");
676
+ }
677
+ function onBackdrop(event) {
678
+ if (event.target === dialog.value) emit("close");
679
+ }
680
+ function onKeydown(event) {
681
+ if (props.total < 2) return;
682
+ const target = event.target;
683
+ if (target && /^(INPUT|TEXTAREA|SELECT|VIDEO|AUDIO)$/.test(target.tagName)) return;
684
+ if (event.key === "ArrowLeft") {
685
+ event.preventDefault();
686
+ emit("prev");
687
+ } else if (event.key === "ArrowRight") {
688
+ event.preventDefault();
689
+ emit("next");
690
+ }
691
+ }
692
+ function markFailed() {
693
+ bodyFailed.value = true;
694
+ }
695
+ return (_ctx, _cache) => {
696
+ return openBlock(), createElementBlock("dialog", {
697
+ ref_key: "dialog",
698
+ ref: dialog,
699
+ class: "fl-preview",
700
+ "aria-label": __props.file?.name,
701
+ onClose: onNativeClose,
702
+ onClick: onBackdrop,
703
+ onKeydown
704
+ }, [__props.open && __props.file ? (openBlock(), createElementBlock("div", _hoisted_2$1, [createElementVNode("header", _hoisted_3$1, [
705
+ createElementVNode("span", {
706
+ class: "fl-type",
707
+ "data-kind": unref(fileKind)(__props.file.name),
708
+ "aria-hidden": "true"
709
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_4$1),
710
+ 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)]),
711
+ __props.total > 1 || mode.value === "image" && !busy.value ? (openBlock(), createElementBlock("div", _hoisted_8$1, [__props.total > 1 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
712
+ createElementVNode("button", {
713
+ class: "fl-preview-tool",
714
+ type: "button",
715
+ "aria-label": t("previous"),
716
+ title: t("previous"),
717
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("prev"))
718
+ }, [..._cache[7] || (_cache[7] = [createElementVNode("svg", {
719
+ viewBox: "0 0 24 24",
720
+ width: "18",
721
+ height: "18",
722
+ fill: "none",
723
+ stroke: "currentColor",
724
+ "stroke-width": "2.2",
725
+ "stroke-linecap": "round",
726
+ "stroke-linejoin": "round",
727
+ "aria-hidden": "true"
728
+ }, [createElementVNode("path", { d: "m15 6-6 6 6 6" })], -1)])], 8, _hoisted_9$1),
729
+ createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
730
+ createElementVNode("button", {
731
+ class: "fl-preview-tool",
732
+ type: "button",
733
+ "aria-label": t("next"),
734
+ title: t("next"),
735
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("next"))
736
+ }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
737
+ viewBox: "0 0 24 24",
738
+ width: "18",
739
+ height: "18",
740
+ fill: "none",
741
+ stroke: "currentColor",
742
+ "stroke-width": "2.2",
743
+ "stroke-linecap": "round",
744
+ "stroke-linejoin": "round",
745
+ "aria-hidden": "true"
746
+ }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 8, _hoisted_11$1)
747
+ ], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
748
+ class: "fl-preview-tool fl-preview-rotate",
749
+ type: "button",
750
+ "aria-label": t("rotate"),
751
+ title: t("rotate"),
752
+ onClick: _cache[2] || (_cache[2] = ($event) => rotation.value = (rotation.value + 90) % 360)
753
+ }, [..._cache[9] || (_cache[9] = [createElementVNode("svg", {
754
+ viewBox: "0 0 24 24",
755
+ width: "17",
756
+ height: "17",
757
+ fill: "none",
758
+ stroke: "currentColor",
759
+ "stroke-width": "2",
760
+ "stroke-linecap": "round",
761
+ "stroke-linejoin": "round",
762
+ "aria-hidden": "true"
763
+ }, [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", {
764
+ class: "fl-preview-tool fl-preview-zoom",
765
+ type: "button",
766
+ "aria-pressed": actualSize.value,
767
+ "aria-label": t("actualSize"),
768
+ title: t("actualSize"),
769
+ onClick: _cache[3] || (_cache[3] = ($event) => actualSize.value = !actualSize.value)
770
+ }, [(openBlock(), createElementBlock("svg", _hoisted_14$1, [
771
+ _cache[10] || (_cache[10] = createElementVNode("circle", {
772
+ cx: "11",
773
+ cy: "11",
774
+ r: "7"
775
+ }, null, -1)),
776
+ _cache[11] || (_cache[11] = createElementVNode("path", { d: "m20 20-3.5-3.5" }, null, -1)),
777
+ !actualSize.value ? (openBlock(), createElementBlock("path", _hoisted_15$1)) : (openBlock(), createElementBlock("path", _hoisted_16$1))
778
+ ]))], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
779
+ createElementVNode("div", _hoisted_17$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
780
+ key: 0,
781
+ class: "fl-preview-tool fl-preview-download",
782
+ type: "button",
783
+ "aria-label": t("download"),
784
+ title: t("download"),
785
+ onClick: _cache[4] || (_cache[4] = ($event) => emit("download"))
786
+ }, [..._cache[12] || (_cache[12] = [createElementVNode("svg", {
787
+ viewBox: "0 0 24 24",
788
+ width: "17",
789
+ height: "17",
790
+ fill: "none",
791
+ stroke: "currentColor",
792
+ "stroke-width": "2",
793
+ "stroke-linecap": "round",
794
+ "stroke-linejoin": "round",
795
+ "aria-hidden": "true"
796
+ }, [
797
+ createElementVNode("path", { d: "M12 3v12" }),
798
+ createElementVNode("path", { d: "m7 10 5 5 5-5" }),
799
+ createElementVNode("path", { d: "M5 21h14" })
800
+ ], -1)])], 8, _hoisted_18$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
801
+ class: "fl-preview-tool fl-preview-close",
802
+ type: "button",
803
+ autofocus: "",
804
+ "aria-label": t("close"),
805
+ title: t("close"),
806
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("close"))
807
+ }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
808
+ viewBox: "0 0 24 24",
809
+ width: "18",
810
+ height: "18",
811
+ fill: "none",
812
+ stroke: "currentColor",
813
+ "stroke-width": "2.2",
814
+ "stroke-linecap": "round",
815
+ "aria-hidden": "true"
816
+ }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_19$1)])
817
+ ]), createElementVNode("div", {
818
+ class: "fl-preview-body",
819
+ "data-mode": busy.value ? "loading" : mode.value
820
+ }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_21$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
821
+ key: 1,
822
+ class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
823
+ src: __props.src,
824
+ alt: __props.file.name,
825
+ style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
826
+ onError: markFailed
827
+ }, null, 46, _hoisted_22$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
828
+ key: 2,
829
+ class: "fl-preview-video",
830
+ src: __props.src,
831
+ controls: "",
832
+ playsinline: "",
833
+ preload: "metadata",
834
+ onError: markFailed
835
+ }, null, 40, _hoisted_23$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
836
+ key: 3,
837
+ class: "fl-preview-audio",
838
+ src: __props.src,
839
+ controls: "",
840
+ preload: "metadata",
841
+ onError: markFailed
842
+ }, 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", {
843
+ key: 5,
844
+ class: "fl-preview-frame",
845
+ src: __props.viewer || blobUrl.value,
846
+ title: __props.file.name
847
+ }, null, 8, _hoisted_27$1)) : (openBlock(), createElementBlock("div", _hoisted_28$1, [
848
+ createElementVNode("span", {
849
+ class: "fl-type fl-preview-badge",
850
+ "data-kind": unref(fileKind)(__props.file.name),
851
+ "aria-hidden": "true"
852
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_29$1),
853
+ createElementVNode("p", _hoisted_30$1, toDisplayString(infoMessage.value), 1),
854
+ !__props.demo ? (openBlock(), createElementBlock("button", {
855
+ key: 0,
856
+ class: "fl-btn fl-btn-primary",
857
+ type: "button",
858
+ onClick: _cache[6] || (_cache[6] = ($event) => emit("download"))
859
+ }, [_cache[14] || (_cache[14] = createElementVNode("svg", {
860
+ viewBox: "0 0 24 24",
861
+ width: "15",
862
+ height: "15",
863
+ fill: "none",
864
+ stroke: "currentColor",
865
+ "stroke-width": "2.2",
866
+ "stroke-linecap": "round",
867
+ "stroke-linejoin": "round",
868
+ "aria-hidden": "true"
869
+ }, [
870
+ createElementVNode("path", { d: "M12 3v12" }),
871
+ createElementVNode("path", { d: "m7 10 5 5 5-5" }),
872
+ createElementVNode("path", { d: "M5 21h14" })
873
+ ], -1)), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
874
+ ]))], 8, _hoisted_20$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
875
+ };
876
+ }
877
+ });
271
878
  // src/client/FileList.vue
272
879
  const _hoisted_1 = { class: "fl-bar" };
273
880
  const _hoisted_2 = {
@@ -408,10 +1015,6 @@ const _hoisted_54 = [
408
1015
  ];
409
1016
  const _hoisted_55 = { class: "fl-foot" };
410
1017
  const _hoisted_56 = {
411
- key: 0,
412
- class: "fl-foot-hint"
413
- };
414
- const _hoisted_57 = {
415
1018
  key: 4,
416
1019
  class: "fl-toast",
417
1020
  role: "status"
@@ -459,6 +1062,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
459
1062
  const sortKey = ref("name");
460
1063
  const sortAsc = ref(true);
461
1064
  const siteTitle = ref("");
1065
+ /** OpenList's public `iframe_previews` setting (JSON text). */
1066
+ const iframePreviews = ref("");
462
1067
  const batchBusy = ref(false);
463
1068
  const batchProgress = ref(0);
464
1069
  const downloadCache = /* @__PURE__ */ new Map();
@@ -668,20 +1273,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
668
1273
  } catch {
669
1274
  url = "";
670
1275
  }
671
- if (!url) {
672
- const encoded = node.path.split("/").map(encodeURIComponent).join("/");
673
- url = `${baseUrl.value}/d${encoded}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
674
- }
1276
+ if (!url) url = directUrl(node);
675
1277
  downloadCache.set(node.path, url);
676
1278
  return url;
677
1279
  }
1280
+ /** OpenList's signed `/d/` download link for a file. */
1281
+ function directUrl(node) {
1282
+ return `${baseUrl.value}/d${encodePath(node.path)}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
1283
+ }
678
1284
  async function loadSiteTitle() {
679
1285
  if (demo.value) return;
680
1286
  try {
681
1287
  const data = await request("/api/public/settings");
682
1288
  siteTitle.value = data?.site_title || data?.title || "";
1289
+ iframePreviews.value = data?.iframe_previews || "";
683
1290
  } catch {
684
1291
  siteTitle.value = "";
1292
+ iframePreviews.value = "";
685
1293
  }
686
1294
  }
687
1295
  function sortTree(nodes) {
@@ -901,17 +1509,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
901
1509
  function toggleCheckAll() {
902
1510
  selected.value = allChecked.value ? /* @__PURE__ */ new Set() : new Set(allFiles.value.map((file) => file.path));
903
1511
  }
904
- const unloadedFolders = computed(() => {
905
- let count = 0;
906
- const walk = (nodes) => {
907
- for (const node of nodes) {
908
- if (node.is_dir && !node.loaded) count += 1;
909
- if (node.children.length) walk(node.children);
910
- }
911
- };
912
- walk(roots.value);
913
- return count;
914
- });
915
1512
  function triggerUrl(url) {
916
1513
  const anchor = document.createElement("a");
917
1514
  anchor.href = url;
@@ -943,21 +1540,55 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
943
1540
  showToast(t("downloadFailed", { message: errorText(error) }));
944
1541
  }
945
1542
  }
946
- const previewable = /\.(?:pdf|png|jpe?g|gif|webp|svg|txt|md|json|mp3|wav|mp4|webm|mov)$/i;
1543
+ /** Only types the dialog can actually render get a preview button. */
947
1544
  function canPreview(node) {
948
- return !node.is_dir && previewable.test(node.name);
1545
+ return !node.is_dir && previewKind(node.name) !== "none";
949
1546
  }
1547
+ const previewNode = ref(null);
1548
+ const previewSrc = ref("");
1549
+ const previewViewer = ref("");
1550
+ const previewResolving = ref(false);
1551
+ const previewError = ref("");
1552
+ let previewSeq = 0;
1553
+ /** Previewable files in the order they are listed, for ← / →. */
1554
+ const previewList = computed(() => visible.value.filter(canPreview));
1555
+ const previewPosition = computed(() => previewNode.value ? previewList.value.indexOf(previewNode.value) + 1 : 0);
950
1556
  async function preview(node) {
951
- if (demo.value) {
952
- showToast(t("demoPreview", { name: node.name }));
953
- return;
954
- }
1557
+ const seq = ++previewSeq;
1558
+ previewNode.value = node;
1559
+ previewSrc.value = "";
1560
+ previewViewer.value = "";
1561
+ previewError.value = "";
1562
+ previewResolving.value = false;
1563
+ if (demo.value || !canPreview(node)) return;
1564
+ previewResolving.value = true;
955
1565
  try {
956
- window.open(await resolveUrl(node), "_blank", "noopener");
1566
+ const url = await resolveUrl(node);
1567
+ if (seq !== previewSeq) return;
1568
+ previewSrc.value = url;
1569
+ if (previewKind(node.name) === "pdf") {
1570
+ const viewer = iframePreviewsFor(node.name, iframePreviews.value)[0];
1571
+ if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
1572
+ name: node.name,
1573
+ rawUrl: url,
1574
+ downloadUrl: directUrl(node)
1575
+ });
1576
+ }
957
1577
  } catch (error) {
958
- showToast(t("previewFailed", { message: errorText(error) }));
1578
+ if (seq === previewSeq) previewError.value = errorText(error);
1579
+ } finally {
1580
+ if (seq === previewSeq) previewResolving.value = false;
959
1581
  }
960
1582
  }
1583
+ function stepPreview(delta) {
1584
+ const list = previewList.value;
1585
+ if (!list.length) return;
1586
+ preview(list[((previewNode.value ? list.indexOf(previewNode.value) : -1) + delta + list.length) % list.length]);
1587
+ }
1588
+ function closePreview() {
1589
+ previewSeq += 1;
1590
+ previewNode.value = null;
1591
+ }
961
1592
  async function batchDownload() {
962
1593
  const files = allFiles.value.filter((file) => selected.value.has(file.path));
963
1594
  if (!files.length) return;
@@ -1038,7 +1669,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1038
1669
  "aria-label": t("clearSelection"),
1039
1670
  title: t("clearSelection"),
1040
1671
  onClick: clearSelection
1041
- }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
1672
+ }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
1042
1673
  class: "fl-clear-icon",
1043
1674
  viewBox: "0 0 24 24",
1044
1675
  width: "18",
@@ -1049,7 +1680,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1049
1680
  "stroke-linecap": "round",
1050
1681
  "aria-hidden": "true"
1051
1682
  }, [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),
1052
- createElementVNode("div", _hoisted_10, [createElementVNode("label", _hoisted_11, [_cache[7] || (_cache[7] = createElementVNode("svg", {
1683
+ createElementVNode("div", _hoisted_10, [createElementVNode("label", _hoisted_11, [_cache[10] || (_cache[10] = createElementVNode("svg", {
1053
1684
  viewBox: "0 0 24 24",
1054
1685
  width: "15",
1055
1686
  height: "15",
@@ -1073,7 +1704,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1073
1704
  "aria-label": t("refresh"),
1074
1705
  title: t("refresh"),
1075
1706
  onClick: loadRoot
1076
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
1707
+ }, [..._cache[11] || (_cache[11] = [createElementVNode("svg", {
1077
1708
  viewBox: "0 0 24 24",
1078
1709
  width: "15",
1079
1710
  height: "15",
@@ -1090,7 +1721,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1090
1721
  type: "button",
1091
1722
  disabled: batchBusy.value,
1092
1723
  onClick: batchDownload
1093
- }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
1724
+ }, [_cache[12] || (_cache[12] = createElementVNode("svg", {
1094
1725
  viewBox: "0 0 24 24",
1095
1726
  width: "15",
1096
1727
  height: "15",
@@ -1190,7 +1821,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1190
1821
  class: "fl-cell fl-cell-name",
1191
1822
  title: node.isRoot ? node.path : void 0
1192
1823
  }, [
1193
- _cache[12] || (_cache[12] = createElementVNode("span", {
1824
+ _cache[15] || (_cache[15] = createElementVNode("span", {
1194
1825
  class: "fl-indent",
1195
1826
  "aria-hidden": "true"
1196
1827
  }, null, -1)),
@@ -1201,7 +1832,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1201
1832
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1202
1833
  "aria-expanded": node.expanded,
1203
1834
  onClick: ($event) => toggleExpand(node)
1204
- }, [..._cache[10] || (_cache[10] = [createElementVNode("svg", {
1835
+ }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1205
1836
  viewBox: "0 0 24 24",
1206
1837
  width: "14",
1207
1838
  height: "14",
@@ -1212,7 +1843,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1212
1843
  "stroke-linejoin": "round",
1213
1844
  "aria-hidden": "true"
1214
1845
  }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_40)) : (openBlock(), createElementBlock("span", _hoisted_41)),
1215
- 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", {
1846
+ node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_42, [..._cache[14] || (_cache[14] = [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", {
1216
1847
  key: 3,
1217
1848
  class: "fl-type",
1218
1849
  "data-kind": unref(fileKind)(node.name),
@@ -1234,7 +1865,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1234
1865
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1235
1866
  "aria-expanded": node.expanded,
1236
1867
  onClick: ($event) => toggleExpand(node)
1237
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1868
+ }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
1238
1869
  viewBox: "0 0 24 24",
1239
1870
  width: "18",
1240
1871
  height: "18",
@@ -1251,7 +1882,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1251
1882
  "aria-label": `${t("preview")} ${node.name}`,
1252
1883
  title: t("preview"),
1253
1884
  onClick: ($event) => preview(node)
1254
- }, [..._cache[14] || (_cache[14] = [createElementVNode("svg", {
1885
+ }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1255
1886
  viewBox: "0 0 24 24",
1256
1887
  width: "16",
1257
1888
  height: "16",
@@ -1271,7 +1902,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1271
1902
  "aria-label": `${t("download")} ${node.name}`,
1272
1903
  title: t("download"),
1273
1904
  onClick: ($event) => download(node)
1274
- }, [..._cache[15] || (_cache[15] = [createElementVNode("svg", {
1905
+ }, [..._cache[18] || (_cache[18] = [createElementVNode("svg", {
1275
1906
  viewBox: "0 0 24 24",
1276
1907
  width: "16",
1277
1908
  height: "16",
@@ -1288,8 +1919,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1288
1919
  ], -1)])], 8, _hoisted_54)], 64))])
1289
1920
  ], 6);
1290
1921
  }), 128)),
1291
- createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1), unloadedFolders.value ? (openBlock(), createElementBlock("span", _hoisted_56, toDisplayString(t("unloaded", { count: unloadedFolders.value })), 1)) : createCommentVNode("v-if", true)]),
1292
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_57, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
1922
+ createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1)]),
1923
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_56, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
1924
+ createVNode(_sfc_main$1, {
1925
+ open: !!previewNode.value,
1926
+ file: previewNode.value,
1927
+ labels: text.value,
1928
+ src: previewSrc.value,
1929
+ viewer: previewViewer.value,
1930
+ resolving: previewResolving.value,
1931
+ error: previewError.value,
1932
+ demo: demo.value,
1933
+ position: previewPosition.value,
1934
+ total: previewPosition.value ? previewList.value.length : 0,
1935
+ onClose: closePreview,
1936
+ onPrev: _cache[6] || (_cache[6] = ($event) => stepPreview(-1)),
1937
+ onNext: _cache[7] || (_cache[7] = ($event) => stepPreview(1)),
1938
+ onDownload: _cache[8] || (_cache[8] = ($event) => previewNode.value && download(previewNode.value))
1939
+ }, null, 8, [
1940
+ "open",
1941
+ "file",
1942
+ "labels",
1943
+ "src",
1944
+ "viewer",
1945
+ "resolving",
1946
+ "error",
1947
+ "demo",
1948
+ "position",
1949
+ "total"
1950
+ ])
1293
1951
  ], 6);
1294
1952
  };
1295
1953
  }
@@ -1315,4 +1973,4 @@ function enhanceAppWithFileList({ app }) {
1315
1973
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
1316
1974
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
1317
1975
  }
1318
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };
1976
+ 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 };