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.
- package/README.md +33 -5
- package/dist/client/browser/index.d.ts +70 -0
- package/dist/client/browser/index.js +1631 -243
- package/dist/client/ssr/index.d.ts +70 -0
- package/dist/client/ssr/index.js +887 -77
- package/dist/client/style.css +563 -25
- package/package.json +1 -1
package/dist/client/ssr/index.js
CHANGED
|
@@ -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: "
|
|
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
|
|
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
|
|
782
|
+
const rootEl = ref();
|
|
305
783
|
const roots = ref([]);
|
|
306
784
|
const loadingRoot = ref(false);
|
|
307
785
|
const errorMsg = ref("");
|
|
308
|
-
const
|
|
309
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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))
|
|
546
|
-
|
|
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
|
-
|
|
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
|
|
1197
|
+
const configured = source.password ?? "";
|
|
555
1198
|
let item = {
|
|
556
1199
|
name: sourceName(path),
|
|
557
1200
|
size: 0,
|
|
558
|
-
is_dir:
|
|
1201
|
+
is_dir: !looksLikeFile(path),
|
|
559
1202
|
modified: ""
|
|
560
1203
|
};
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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
|
-
|
|
598
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
707
|
-
|
|
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:
|
|
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 (
|
|
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(`<
|
|
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 (
|
|
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
|
-
|
|
746
|
-
|
|
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(
|
|
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
|
|
763
|
-
if (node.
|
|
764
|
-
|
|
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-
|
|
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(`</
|
|
1564
|
+
_push(`</div>`);
|
|
771
1565
|
});
|
|
772
|
-
_push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size:
|
|
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 };
|