vitepress-plugin-filelist 0.3.1 → 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 = {
@@ -455,6 +1062,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
455
1062
  const sortKey = ref("name");
456
1063
  const sortAsc = ref(true);
457
1064
  const siteTitle = ref("");
1065
+ /** OpenList's public `iframe_previews` setting (JSON text). */
1066
+ const iframePreviews = ref("");
458
1067
  const batchBusy = ref(false);
459
1068
  const batchProgress = ref(0);
460
1069
  const downloadCache = /* @__PURE__ */ new Map();
@@ -664,20 +1273,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
664
1273
  } catch {
665
1274
  url = "";
666
1275
  }
667
- if (!url) {
668
- const encoded = node.path.split("/").map(encodeURIComponent).join("/");
669
- url = `${baseUrl.value}/d${encoded}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
670
- }
1276
+ if (!url) url = directUrl(node);
671
1277
  downloadCache.set(node.path, url);
672
1278
  return url;
673
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
+ }
674
1284
  async function loadSiteTitle() {
675
1285
  if (demo.value) return;
676
1286
  try {
677
1287
  const data = await request("/api/public/settings");
678
1288
  siteTitle.value = data?.site_title || data?.title || "";
1289
+ iframePreviews.value = data?.iframe_previews || "";
679
1290
  } catch {
680
1291
  siteTitle.value = "";
1292
+ iframePreviews.value = "";
681
1293
  }
682
1294
  }
683
1295
  function sortTree(nodes) {
@@ -928,21 +1540,55 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
928
1540
  showToast(t("downloadFailed", { message: errorText(error) }));
929
1541
  }
930
1542
  }
931
- 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. */
932
1544
  function canPreview(node) {
933
- return !node.is_dir && previewable.test(node.name);
1545
+ return !node.is_dir && previewKind(node.name) !== "none";
934
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);
935
1556
  async function preview(node) {
936
- if (demo.value) {
937
- showToast(t("demoPreview", { name: node.name }));
938
- return;
939
- }
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;
940
1565
  try {
941
- 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
+ }
942
1577
  } catch (error) {
943
- showToast(t("previewFailed", { message: errorText(error) }));
1578
+ if (seq === previewSeq) previewError.value = errorText(error);
1579
+ } finally {
1580
+ if (seq === previewSeq) previewResolving.value = false;
944
1581
  }
945
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
+ }
946
1592
  async function batchDownload() {
947
1593
  const files = allFiles.value.filter((file) => selected.value.has(file.path));
948
1594
  if (!files.length) return;
@@ -1023,7 +1669,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1023
1669
  "aria-label": t("clearSelection"),
1024
1670
  title: t("clearSelection"),
1025
1671
  onClick: clearSelection
1026
- }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
1672
+ }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
1027
1673
  class: "fl-clear-icon",
1028
1674
  viewBox: "0 0 24 24",
1029
1675
  width: "18",
@@ -1034,7 +1680,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1034
1680
  "stroke-linecap": "round",
1035
1681
  "aria-hidden": "true"
1036
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),
1037
- 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", {
1038
1684
  viewBox: "0 0 24 24",
1039
1685
  width: "15",
1040
1686
  height: "15",
@@ -1058,7 +1704,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1058
1704
  "aria-label": t("refresh"),
1059
1705
  title: t("refresh"),
1060
1706
  onClick: loadRoot
1061
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
1707
+ }, [..._cache[11] || (_cache[11] = [createElementVNode("svg", {
1062
1708
  viewBox: "0 0 24 24",
1063
1709
  width: "15",
1064
1710
  height: "15",
@@ -1075,7 +1721,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1075
1721
  type: "button",
1076
1722
  disabled: batchBusy.value,
1077
1723
  onClick: batchDownload
1078
- }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
1724
+ }, [_cache[12] || (_cache[12] = createElementVNode("svg", {
1079
1725
  viewBox: "0 0 24 24",
1080
1726
  width: "15",
1081
1727
  height: "15",
@@ -1175,7 +1821,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1175
1821
  class: "fl-cell fl-cell-name",
1176
1822
  title: node.isRoot ? node.path : void 0
1177
1823
  }, [
1178
- _cache[12] || (_cache[12] = createElementVNode("span", {
1824
+ _cache[15] || (_cache[15] = createElementVNode("span", {
1179
1825
  class: "fl-indent",
1180
1826
  "aria-hidden": "true"
1181
1827
  }, null, -1)),
@@ -1186,7 +1832,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1186
1832
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1187
1833
  "aria-expanded": node.expanded,
1188
1834
  onClick: ($event) => toggleExpand(node)
1189
- }, [..._cache[10] || (_cache[10] = [createElementVNode("svg", {
1835
+ }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1190
1836
  viewBox: "0 0 24 24",
1191
1837
  width: "14",
1192
1838
  height: "14",
@@ -1197,7 +1843,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1197
1843
  "stroke-linejoin": "round",
1198
1844
  "aria-hidden": "true"
1199
1845
  }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_40)) : (openBlock(), createElementBlock("span", _hoisted_41)),
1200
- node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_42, [..._cache[11] || (_cache[11] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
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", {
1201
1847
  key: 3,
1202
1848
  class: "fl-type",
1203
1849
  "data-kind": unref(fileKind)(node.name),
@@ -1219,7 +1865,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1219
1865
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1220
1866
  "aria-expanded": node.expanded,
1221
1867
  onClick: ($event) => toggleExpand(node)
1222
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1868
+ }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
1223
1869
  viewBox: "0 0 24 24",
1224
1870
  width: "18",
1225
1871
  height: "18",
@@ -1236,7 +1882,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1236
1882
  "aria-label": `${t("preview")} ${node.name}`,
1237
1883
  title: t("preview"),
1238
1884
  onClick: ($event) => preview(node)
1239
- }, [..._cache[14] || (_cache[14] = [createElementVNode("svg", {
1885
+ }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1240
1886
  viewBox: "0 0 24 24",
1241
1887
  width: "16",
1242
1888
  height: "16",
@@ -1256,7 +1902,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1256
1902
  "aria-label": `${t("download")} ${node.name}`,
1257
1903
  title: t("download"),
1258
1904
  onClick: ($event) => download(node)
1259
- }, [..._cache[15] || (_cache[15] = [createElementVNode("svg", {
1905
+ }, [..._cache[18] || (_cache[18] = [createElementVNode("svg", {
1260
1906
  viewBox: "0 0 24 24",
1261
1907
  width: "16",
1262
1908
  height: "16",
@@ -1274,7 +1920,34 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1274
1920
  ], 6);
1275
1921
  }), 128)),
1276
1922
  createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1)]),
1277
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_56, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
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
+ ])
1278
1951
  ], 6);
1279
1952
  };
1280
1953
  }
@@ -1300,4 +1973,4 @@ function enhanceAppWithFileList({ app }) {
1300
1973
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
1301
1974
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
1302
1975
  }
1303
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };
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 };