vitepress-plugin-filelist 0.4.0 → 0.5.1

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,125 +1,15 @@
1
1
  import "../style.css";
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
- // src/client/locales.ts
4
- const fileListLocales = {
5
- "zh-CN": {
6
- allFiles: "全部文件",
7
- demoBadge: "演示数据",
8
- filter: "筛选文件名",
9
- refresh: "刷新",
10
- batch: "下载 ZIP",
11
- batchCount: "批量下载({count})",
12
- batching: "打包中 {percent}%",
13
- passwordPrompt: "该目录受密码保护:",
14
- passwordPlaceholder: "输入目录密码",
15
- open: "打开",
16
- columnName: "文件名",
17
- columnSize: "文件大小",
18
- columnDate: "最后修改日期",
19
- loading: "加载中…",
20
- loadFailed: "加载失败:{message}",
21
- guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
22
- corsHint: "可能是跨域或混合内容被拦截,检查 OpenList 的 cors 配置与页面协议。",
23
- retry: "重试",
24
- empty: "没有文件",
25
- preview: "预览",
26
- download: "下载",
27
- loaded: "{count} 个已加载文件",
28
- selected: "已选 {count} 项",
29
- unloaded: "{count} 个目录未展开,未计入全选",
30
- folderFailed: "打开文件夹失败:{message}",
31
- demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
32
- demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
33
- demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
34
- downloadFailed: "下载失败:{message}",
35
- previewFailed: "预览失败:{message}",
36
- batchFailed: "打包失败:{message}",
37
- defaultZip: "资源",
38
- clear: "清除",
39
- clearSelection: "取消选择",
40
- totalSize: "共 {size}",
41
- folderItems: "{count} 项",
42
- sortBy: "排序",
43
- selectAll: "全选",
44
- selectItem: "选择 {name}",
45
- expand: "展开 {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
- },
57
- "en-US": {
58
- allFiles: "All files",
59
- demoBadge: "Demo data",
60
- filter: "Filter files",
61
- refresh: "Refresh",
62
- batch: "Download ZIP",
63
- batchCount: "Download ZIP ({count})",
64
- batching: "Zipping {percent}%",
65
- passwordPrompt: "This folder is password protected:",
66
- passwordPlaceholder: "Folder password",
67
- open: "Open",
68
- columnName: "Name",
69
- columnSize: "Size",
70
- columnDate: "Modified",
71
- loading: "Loading…",
72
- loadFailed: "Failed to load: {message}",
73
- guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
74
- corsHint: "Possibly blocked by CORS or mixed content. Check OpenList cors settings and the page protocol.",
75
- retry: "Retry",
76
- empty: "No files",
77
- preview: "Preview",
78
- download: "Download",
79
- loaded: "{count} loaded file(s)",
80
- selected: "{count} selected",
81
- unloaded: "{count} folder(s) not expanded, excluded from select-all",
82
- folderFailed: "Failed to open folder: {message}",
83
- demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
84
- demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
85
- demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
86
- downloadFailed: "Download failed: {message}",
87
- previewFailed: "Preview failed: {message}",
88
- batchFailed: "Failed to create ZIP: {message}",
89
- defaultZip: "resources",
90
- clear: "Clear",
91
- clearSelection: "Clear selection",
92
- totalSize: "{size} total",
93
- folderItems: "{count} items",
94
- sortBy: "Sort",
95
- selectAll: "Select all",
96
- selectItem: "Select {name}",
97
- expand: "Expand {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."
108
- }
109
- };
110
- function formatLabel(template, values = {}) {
111
- return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
112
- }
113
- function resolveLabels(locale = "zh-CN", overrides) {
114
- return {
115
- ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
116
- ...overrides
117
- };
118
- }
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, unref, useId, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
119
3
  // src/client/utils.ts
120
4
  function joinPath(parent, name) {
121
5
  return (parent.endsWith("/") ? parent : `${parent}/`) + name;
122
6
  }
7
+ /** Whether `path` is `folder` itself or inside it (segment-wise, trailing slashes ignored). */
8
+ function isWithin(path, folder) {
9
+ const p = path.replace(/\/+$/, "") || "/";
10
+ const f = folder.replace(/\/+$/, "") || "/";
11
+ return f === "/" || p === f || p.startsWith(`${f}/`);
12
+ }
123
13
  function encodePath(path) {
124
14
  return path.split("/").map(encodeURIComponent).join("/");
125
15
  }
@@ -158,8 +48,9 @@ function uniqueEntryName(name, used) {
158
48
  used.add(candidate);
159
49
  return candidate;
160
50
  }
51
+ /** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
161
52
  function formatSize(size) {
162
- if (!size || !Number.isFinite(size)) return "-";
53
+ if (!Number.isFinite(size) || size < 0) return "-";
163
54
  const units = [
164
55
  "B",
165
56
  "KB",
@@ -183,96 +74,106 @@ function formatDate(iso) {
183
74
  const pad = (value) => String(value).padStart(2, "0");
184
75
  return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
185
76
  }
186
- function extOf(name) {
77
+ /** Lower-case extension (letters and digits only), or `''`. */
78
+ function extension(name) {
187
79
  const match = name.match(/\.([a-z0-9]+)$/i);
188
- return match ? match[1].toUpperCase() : "";
80
+ return match ? match[1].toLowerCase() : "";
81
+ }
82
+ /** Upper-case extension for the type badge, e.g. `PDF`. */
83
+ function extOf(name) {
84
+ return extension(name).toUpperCase();
189
85
  }
190
- const FILE_KINDS = {};
191
- for (const [kind, exts] of Object.entries({
192
- doc: [
86
+ /**
87
+ * One table for every known extension: the badge family (`FileKind`) and how
88
+ * the preview dialog shows it (`PreviewKind`; Office and the like are `none`).
89
+ */
90
+ const EXTENSIONS = [
91
+ [
92
+ "doc",
93
+ "text",
94
+ "txt md markdown"
95
+ ],
96
+ [
193
97
  "doc",
194
- "docx",
195
- "odt",
196
- "rtf",
197
- "txt",
198
- "md",
199
- "pages",
200
- "wps"
98
+ "none",
99
+ "doc docx odt rtf pages wps"
100
+ ],
101
+ [
102
+ "sheet",
103
+ "text",
104
+ "csv tsv"
105
+ ],
106
+ [
107
+ "sheet",
108
+ "none",
109
+ "xls xlsx ods numbers et"
110
+ ],
111
+ [
112
+ "slide",
113
+ "none",
114
+ "ppt pptx odp key dps"
201
115
  ],
202
- sheet: [
203
- "xls",
204
- "xlsx",
205
- "ods",
206
- "csv",
207
- "tsv",
208
- "numbers",
209
- "et"
116
+ [
117
+ "pdf",
118
+ "pdf",
119
+ "pdf"
210
120
  ],
211
- slide: [
212
- "ppt",
213
- "pptx",
214
- "odp",
215
- "key",
216
- "dps"
121
+ [
122
+ "image",
123
+ "image",
124
+ "png jpg jpeg gif webp svg bmp ico avif"
217
125
  ],
218
- pdf: ["pdf"],
219
- image: [
220
- "png",
221
- "jpg",
222
- "jpeg",
223
- "gif",
224
- "webp",
225
- "svg",
226
- "bmp",
227
- "ico",
228
- "avif",
126
+ [
127
+ "image",
128
+ "none",
229
129
  "heic"
230
130
  ],
231
- media: [
232
- "mp3",
233
- "wav",
234
- "flac",
235
- "aac",
236
- "ogg",
237
- "m4a",
238
- "mp4",
239
- "webm",
240
- "mov",
241
- "mkv",
242
- "avi"
131
+ [
132
+ "media",
133
+ "video",
134
+ "mp4 webm m4v mov ogv"
135
+ ],
136
+ [
137
+ "media",
138
+ "audio",
139
+ "mp3 wav ogg oga m4a aac flac opus"
140
+ ],
141
+ [
142
+ "media",
143
+ "none",
144
+ "mkv avi"
145
+ ],
146
+ [
147
+ "archive",
148
+ "none",
149
+ "zip rar 7z tar gz tgz bz2 xz iso"
243
150
  ],
244
- archive: [
245
- "zip",
246
- "rar",
247
- "7z",
248
- "tar",
249
- "gz",
250
- "tgz",
251
- "bz2",
252
- "xz",
253
- "iso"
151
+ [
152
+ "code",
153
+ "text",
154
+ "js mjs cjs ts tsx jsx vue json html htm css scss less py java kt c h cpp hpp cs go rs rb php sh bat ps1 sql yml yaml xml toml ini conf properties"
254
155
  ],
255
- code: [
256
- "js",
257
- "ts",
258
- "json",
259
- "html",
260
- "css",
261
- "py",
262
- "java",
263
- "c",
264
- "cpp",
265
- "go",
266
- "rs",
267
- "sh",
268
- "yml",
269
- "yaml",
270
- "xml"
156
+ [
157
+ "other",
158
+ "text",
159
+ "log srt vtt ass lrc gitignore env"
271
160
  ]
272
- })) for (const ext of exts) FILE_KINDS[ext] = kind;
161
+ ];
162
+ const BY_EXTENSION = /* @__PURE__ */ new Map();
163
+ for (const [kind, preview, list] of EXTENSIONS) for (const ext of list.split(" ")) BY_EXTENSION.set(ext, {
164
+ kind,
165
+ preview
166
+ });
273
167
  /** Rough file family, used to colour the type badge. */
274
168
  function fileKind(name) {
275
- return FILE_KINDS[extOf(name).toLowerCase()] ?? "other";
169
+ return BY_EXTENSION.get(extension(name))?.kind ?? "other";
170
+ }
171
+ /**
172
+ * How a file is shown in the preview dialog. Office documents and anything
173
+ * else the browser cannot render are `none` (info + download).
174
+ */
175
+ function previewKind(name) {
176
+ return BY_EXTENSION.get(extension(name))?.preview ?? "none";
276
177
  }
277
178
  /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
278
179
  function totalSize(items) {
@@ -286,100 +187,6 @@ function compareNodes(a, b, key, ascending = true) {
286
187
  else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
287
188
  return ascending ? result : -result;
288
189
  }
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
190
  /**
384
191
  * Viewers from OpenList's public `iframe_previews` setting that match a file,
385
192
  * in the order OpenList lists them. Keys are comma-separated extensions or a
@@ -394,7 +201,7 @@ function iframePreviewsFor(name, setting) {
394
201
  return [];
395
202
  }
396
203
  if (!parsed || typeof parsed !== "object") return [];
397
- const ext = lowerExt(name);
204
+ const ext = extension(name);
398
205
  const out = [];
399
206
  for (const [key, viewers] of Object.entries(parsed)) {
400
207
  let hit = false;
@@ -428,6 +235,579 @@ function applyUrlOps(placeholder, url) {
428
235
  function fillPreviewUrl(scheme, args) {
429
236
  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
237
  }
238
+ // src/client/api.ts
239
+ var ApiError = class extends Error {
240
+ code;
241
+ constructor(message, code) {
242
+ super(message);
243
+ this.code = code;
244
+ }
245
+ };
246
+ function errorText(error) {
247
+ return error instanceof Error ? error.message : String(error);
248
+ }
249
+ /**
250
+ * OpenList has no dedicated code for a missing / wrong folder password; its
251
+ * message says "password is incorrect or you have no permission".
252
+ */
253
+ function isPasswordError(error) {
254
+ return /password|密码/i.test(errorText(error));
255
+ }
256
+ /** Browser-level fetch failures (Chrome, Firefox, Safari wording), usually CORS. */
257
+ function isNetworkError(error) {
258
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
259
+ }
260
+ function createOpenListApi(base, token) {
261
+ const root = base.replace(/\/+$/, "");
262
+ const urls = /* @__PURE__ */ new Map();
263
+ async function request(apiPath, body) {
264
+ const response = await fetch(`${root}${apiPath}`, {
265
+ method: body === void 0 ? "GET" : "POST",
266
+ headers: {
267
+ ...body === void 0 ? {} : { "Content-Type": "application/json" },
268
+ ...token ? { Authorization: token } : {}
269
+ },
270
+ body: body === void 0 ? void 0 : JSON.stringify(body)
271
+ });
272
+ const payload = await response.json().catch(() => null);
273
+ if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
274
+ if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
275
+ return payload.data;
276
+ }
277
+ function absoluteUrl(raw) {
278
+ if (!raw) return "";
279
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
280
+ const origin = root || (typeof window !== "undefined" ? window.location.origin : "");
281
+ try {
282
+ return new URL(raw, `${origin}/`).href;
283
+ } catch {
284
+ return raw;
285
+ }
286
+ }
287
+ const directUrl = (file) => `${root}/d${encodePath(file.path)}${file.sign ? `?sign=${encodeURIComponent(file.sign)}` : ""}`;
288
+ return {
289
+ async list(path, password) {
290
+ return (await request("/api/fs/list", {
291
+ path,
292
+ password,
293
+ page: 1,
294
+ per_page: 0,
295
+ refresh: false
296
+ }))?.content ?? [];
297
+ },
298
+ get: (path, password) => request("/api/fs/get", {
299
+ path,
300
+ password
301
+ }),
302
+ async settings() {
303
+ try {
304
+ const data = await request("/api/public/settings");
305
+ return {
306
+ title: data?.site_title || data?.title || "",
307
+ iframePreviews: data?.iframe_previews || ""
308
+ };
309
+ } catch {
310
+ return {
311
+ title: "",
312
+ iframePreviews: ""
313
+ };
314
+ }
315
+ },
316
+ async fileUrl(file, password) {
317
+ if (file.raw_url) return absoluteUrl(file.raw_url);
318
+ const cached = urls.get(file.path);
319
+ if (cached) return cached;
320
+ let url = "";
321
+ try {
322
+ url = absoluteUrl((await request("/api/fs/get", {
323
+ path: file.path,
324
+ password
325
+ }))?.raw_url ?? "");
326
+ } catch {}
327
+ url ||= directUrl(file);
328
+ urls.set(file.path, url);
329
+ return url;
330
+ },
331
+ directUrl,
332
+ clearCache(path) {
333
+ if (!path) urls.clear();
334
+ else for (const key of [...urls.keys()]) if (isWithin(key, path)) urls.delete(key);
335
+ }
336
+ };
337
+ }
338
+ // src/client/demo.ts
339
+ /** Built-in sample data, used when no `base` is given. */
340
+ const TREE = {
341
+ "/": [
342
+ {
343
+ name: "assignments",
344
+ size: 0,
345
+ is_dir: true,
346
+ modified: "2026-09-01T08:00:00+08:00"
347
+ },
348
+ {
349
+ name: "exams",
350
+ size: 0,
351
+ is_dir: true,
352
+ modified: "2026-09-01T08:00:00+08:00"
353
+ },
354
+ {
355
+ name: "materials",
356
+ size: 0,
357
+ is_dir: true,
358
+ modified: "2026-09-01T08:00:00+08:00"
359
+ },
360
+ {
361
+ name: "notes",
362
+ size: 0,
363
+ is_dir: true,
364
+ modified: "2026-09-01T08:00:00+08:00"
365
+ },
366
+ {
367
+ name: "slides",
368
+ size: 0,
369
+ is_dir: true,
370
+ modified: "2026-09-01T08:00:00+08:00"
371
+ },
372
+ {
373
+ name: "加密示例(密码 1234)",
374
+ size: 0,
375
+ is_dir: true,
376
+ modified: "2026-10-20T18:00:00+08:00"
377
+ }
378
+ ],
379
+ "/assignments": [{
380
+ name: "线代作业参考答案",
381
+ size: 0,
382
+ is_dir: true,
383
+ modified: "2026-10-12T21:30:00+08:00"
384
+ }, {
385
+ name: "作业提交说明.pdf",
386
+ size: 186432,
387
+ is_dir: false,
388
+ modified: "2026-09-03T10:15:00+08:00"
389
+ }],
390
+ "/assignments/线代作业参考答案": [
391
+ {
392
+ name: "第1次作业参考答案.doc",
393
+ size: 412672,
394
+ is_dir: false,
395
+ modified: "2026-09-15T22:41:00+08:00"
396
+ },
397
+ {
398
+ name: "第2次作业参考答案.pdf",
399
+ size: 1048576,
400
+ is_dir: false,
401
+ modified: "2026-09-22T22:03:00+08:00"
402
+ },
403
+ {
404
+ name: "第3次作业参考答案.pdf",
405
+ size: 986112,
406
+ is_dir: false,
407
+ modified: "2026-09-29T21:57:00+08:00"
408
+ },
409
+ {
410
+ name: "第4次作业参考答案.pdf",
411
+ size: 1572864,
412
+ is_dir: false,
413
+ modified: "2026-10-13T22:10:00+08:00"
414
+ }
415
+ ],
416
+ "/exams": [
417
+ {
418
+ name: "2023 期末试题和答案.pdf",
419
+ size: 2345678,
420
+ is_dir: false,
421
+ modified: "2025-01-18T09:20:00+08:00"
422
+ },
423
+ {
424
+ name: "2024 期中试题.pdf",
425
+ size: 1234567,
426
+ is_dir: false,
427
+ modified: "2025-11-02T14:08:00+08:00"
428
+ },
429
+ {
430
+ name: "代数与几何-多年合集-试题和答案.pdf",
431
+ size: 18874368,
432
+ is_dir: false,
433
+ modified: "2025-01-18T09:25:00+08:00"
434
+ }
435
+ ],
436
+ "/materials": [
437
+ {
438
+ name: "线性代数及其应用(Lay 第5版).pdf",
439
+ size: 48231424,
440
+ is_dir: false,
441
+ modified: "2024-07-29T16:44:00+08:00"
442
+ },
443
+ {
444
+ name: "Introduction to Linear Algebra(Strang).pdf",
445
+ size: 36700160,
446
+ is_dir: false,
447
+ modified: "2024-07-29T16:50:00+08:00"
448
+ },
449
+ {
450
+ name: "疑难解答与习题指导.zip",
451
+ size: 9437184,
452
+ is_dir: false,
453
+ modified: "2024-08-05T11:12:00+08:00"
454
+ }
455
+ ],
456
+ "/notes": [{
457
+ name: "线性代数笔记(106页).pdf",
458
+ size: 12582912,
459
+ is_dir: false,
460
+ modified: "2025-06-30T20:31:00+08:00"
461
+ }, {
462
+ name: "期末复习提纲.docx",
463
+ size: 245760,
464
+ is_dir: false,
465
+ modified: "2026-01-05T19:02:00+08:00"
466
+ }],
467
+ "/加密示例(密码 1234)": [{
468
+ name: "期末答案(教师版).pdf",
469
+ size: 2621440,
470
+ is_dir: false,
471
+ modified: "2026-10-20T18:00:00+08:00"
472
+ }, {
473
+ name: "成绩登记表.xlsx",
474
+ size: 48128,
475
+ is_dir: false,
476
+ modified: "2026-10-21T09:12:00+08:00"
477
+ }],
478
+ "/slides": [{
479
+ name: "第一章 行列式.pptx",
480
+ size: 3145728,
481
+ is_dir: false,
482
+ modified: "2026-09-08T13:26:00+08:00"
483
+ }, {
484
+ name: "第二章 矩阵.pptx",
485
+ size: 4194304,
486
+ is_dir: false,
487
+ modified: "2026-09-15T13:30:00+08:00"
488
+ }]
489
+ };
490
+ /** Demo-only folder passwords, so the demo can show the password flow. */
491
+ const PASSWORDS = { "/加密示例(密码 1234)": "1234" };
492
+ function guard(path, password) {
493
+ const lock = Object.entries(PASSWORDS).find(([folder]) => isWithin(path, folder));
494
+ if (lock && lock[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
495
+ }
496
+ function parentOf(path) {
497
+ return path.slice(0, path.lastIndexOf("/")) || "/";
498
+ }
499
+ /** Demo OpenList: same API, no network; files have no URL. */
500
+ function createDemoApi(delay = 220) {
501
+ return {
502
+ async list(path, password) {
503
+ await new Promise((resolve) => setTimeout(resolve, delay));
504
+ guard(path, password);
505
+ return TREE[path] ?? [];
506
+ },
507
+ async get(path, password) {
508
+ guard(path, password);
509
+ const name = path.split("/").filter(Boolean).pop() ?? "";
510
+ const item = (TREE[parentOf(path)] ?? []).find((entry) => entry.name === name);
511
+ if (!item) throw new ApiError("object not found", 404);
512
+ return item;
513
+ },
514
+ settings: async () => ({
515
+ title: "",
516
+ iframePreviews: ""
517
+ }),
518
+ fileUrl: async () => "",
519
+ directUrl: () => "",
520
+ clearCache: () => {}
521
+ };
522
+ }
523
+ // src/client/download.ts
524
+ /** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
525
+ function openUrl(url) {
526
+ const anchor = document.createElement("a");
527
+ anchor.href = url;
528
+ anchor.target = "_blank";
529
+ anchor.rel = "noopener";
530
+ document.body.appendChild(anchor);
531
+ anchor.click();
532
+ anchor.remove();
533
+ }
534
+ function saveBlob(blob, name) {
535
+ const href = URL.createObjectURL(blob);
536
+ const anchor = document.createElement("a");
537
+ anchor.href = href;
538
+ anchor.download = name;
539
+ document.body.appendChild(anchor);
540
+ anchor.click();
541
+ anchor.remove();
542
+ setTimeout(() => URL.revokeObjectURL(href), 3e4);
543
+ }
544
+ /** File-system-safe ZIP name from a title. */
545
+ function zipFileName(title) {
546
+ return `${title.replace(/[\\/:*?"<>|]/g, "_")}.zip`;
547
+ }
548
+ /**
549
+ * Fetch files (three at a time) and pack them into one ZIP. `zipPath` is the
550
+ * entry path; clashing names get ` (2)`, ` (3)`... `onProgress` goes 0–90 while
551
+ * fetching; packing is the rest.
552
+ */
553
+ async function zipFiles(files, urlOf, onProgress) {
554
+ const entries = {};
555
+ const used = /* @__PURE__ */ new Set();
556
+ const queue = [...files];
557
+ let done = 0;
558
+ const worker = async () => {
559
+ for (let file = queue.shift(); file; file = queue.shift()) {
560
+ const response = await fetch(await urlOf(file));
561
+ if (!response.ok) throw new Error(`${file.name}:HTTP ${response.status}`);
562
+ entries[uniqueEntryName(zipEntryName(file.zipPath), used)] = new Uint8Array(await response.arrayBuffer());
563
+ done += 1;
564
+ onProgress(Math.round(done / files.length * 90));
565
+ }
566
+ };
567
+ await Promise.all(Array.from({ length: Math.min(3, files.length) }, worker));
568
+ const { zip } = await import("fflate");
569
+ const data = await new Promise((resolve, reject) => {
570
+ zip(entries, (error, output) => error ? reject(error) : resolve(output));
571
+ });
572
+ onProgress(100);
573
+ return new Blob([data], { type: "application/zip" });
574
+ }
575
+ // src/client/icons.ts
576
+ /** Stroke icons on a 24×24 grid, drawn by `Icon.vue`. */
577
+ const ICONS = {
578
+ lock: {
579
+ stroke: 2.2,
580
+ paths: ["M7 11h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2z", "M8 11V8a4 4 0 0 1 8 0v3"]
581
+ },
582
+ key: {
583
+ stroke: 2,
584
+ paths: [
585
+ "M4 15a4 4 0 1 0 8 0a4 4 0 1 0-8 0",
586
+ "m11 12 9-9",
587
+ "m17 6 3 3"
588
+ ]
589
+ },
590
+ close: {
591
+ stroke: 2.2,
592
+ paths: ["M6 6l12 12", "M18 6 6 18"]
593
+ },
594
+ download: {
595
+ stroke: 2.2,
596
+ paths: [
597
+ "M12 3v12",
598
+ "m7 10 5 5 5-5",
599
+ "M5 21h14"
600
+ ]
601
+ },
602
+ eye: {
603
+ stroke: 2,
604
+ paths: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0"]
605
+ },
606
+ refresh: {
607
+ stroke: 2,
608
+ paths: ["M21 12a9 9 0 1 1-3-6.7", "M21 4v5h-5"]
609
+ },
610
+ search: {
611
+ stroke: 2,
612
+ paths: ["M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0", "m20 20-3.5-3.5"]
613
+ },
614
+ next: {
615
+ stroke: 2.2,
616
+ paths: ["m9 6 6 6-6 6"]
617
+ },
618
+ previous: {
619
+ stroke: 2.2,
620
+ paths: ["m15 6-6 6 6 6"]
621
+ },
622
+ zoomIn: {
623
+ stroke: 2,
624
+ paths: [
625
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
626
+ "m20 20-3.5-3.5",
627
+ "M11 8v6M8 11h6"
628
+ ]
629
+ },
630
+ zoomOut: {
631
+ stroke: 2,
632
+ paths: [
633
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
634
+ "m20 20-3.5-3.5",
635
+ "M8 11h6"
636
+ ]
637
+ }
638
+ };
639
+ // src/client/Icon.vue
640
+ const _hoisted_1$2 = [
641
+ "width",
642
+ "height",
643
+ "stroke-width"
644
+ ];
645
+ const _hoisted_2$2 = ["d"];
646
+ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
647
+ __name: "Icon",
648
+ props: {
649
+ name: {},
650
+ size: { default: 16 }
651
+ },
652
+ setup(__props) {
653
+ return (_ctx, _cache) => {
654
+ return openBlock(), createElementBlock("svg", {
655
+ viewBox: "0 0 24 24",
656
+ width: __props.size,
657
+ height: __props.size,
658
+ fill: "none",
659
+ stroke: "currentColor",
660
+ "stroke-width": unref(ICONS)[__props.name].stroke,
661
+ "stroke-linecap": "round",
662
+ "stroke-linejoin": "round",
663
+ "aria-hidden": "true"
664
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(ICONS)[__props.name].paths, (d) => {
665
+ return openBlock(), createElementBlock("path", {
666
+ key: d,
667
+ d
668
+ }, null, 8, _hoisted_2$2);
669
+ }), 128))], 8, _hoisted_1$2);
670
+ };
671
+ }
672
+ });
673
+ // src/client/locales.ts
674
+ const fileListLocales = {
675
+ "zh-CN": {
676
+ allFiles: "全部文件",
677
+ demoBadge: "演示数据",
678
+ filter: "筛选文件名",
679
+ refresh: "刷新",
680
+ batch: "下载 ZIP",
681
+ batchCount: "批量下载({count})",
682
+ batching: "打包中 {percent}%",
683
+ passwordPrompt: "该目录受密码保护:",
684
+ passwordPlaceholder: "输入密码",
685
+ open: "打开",
686
+ columnName: "文件名",
687
+ columnSize: "文件大小",
688
+ columnDate: "最后修改日期",
689
+ loading: "加载中…",
690
+ loadFailed: "加载失败:{message}",
691
+ guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
692
+ corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
693
+ retry: "重试",
694
+ empty: "没有文件",
695
+ preview: "预览",
696
+ download: "下载",
697
+ loaded: "{count} 个已加载文件",
698
+ selected: "已选 {count} 项",
699
+ unloaded: "{count} 个目录未展开,未计入全选",
700
+ folderFailed: "打开文件夹失败:{message}",
701
+ demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
702
+ demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
703
+ demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
704
+ downloadFailed: "下载失败:{message}",
705
+ previewFailed: "预览失败:{message}",
706
+ batchFailed: "打包失败:{message}",
707
+ defaultZip: "资源",
708
+ clear: "清除",
709
+ clearSelection: "取消选择",
710
+ totalSize: "共 {size}",
711
+ folderItems: "{count} 项",
712
+ sortBy: "排序",
713
+ selectAll: "全选",
714
+ selectItem: "选择 {name}",
715
+ expand: "展开 {name}",
716
+ collapse: "收起 {name}",
717
+ close: "关闭",
718
+ previous: "上一个",
719
+ next: "下一个",
720
+ rotate: "旋转",
721
+ actualSize: "原始大小",
722
+ previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
723
+ previewUnavailable: "该文件无法在页面内预览,可下载后查看",
724
+ previewTooLarge: "文件较大({size}),请下载后查看",
725
+ textTruncated: "仅显示前 {size},完整内容请下载",
726
+ refreshing: "刷新中",
727
+ refreshFailed: "刷新失败:{message}",
728
+ cancel: "取消",
729
+ passwordFor: "{name} 的密码",
730
+ passwordWrong: "密码不正确或无权访问",
731
+ passwordStale: "已保存的密码已失效,请重新输入",
732
+ rememberPassword: "在此标签页中记住(关闭标签页后失效)",
733
+ remembered: "已记住",
734
+ forgetPassword: "忘记",
735
+ unlockFolder: "输入 {name} 的密码",
736
+ networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
737
+ },
738
+ "en-US": {
739
+ allFiles: "All files",
740
+ demoBadge: "Demo data",
741
+ filter: "Filter files",
742
+ refresh: "Refresh",
743
+ batch: "Download ZIP",
744
+ batchCount: "Download ZIP ({count})",
745
+ batching: "Zipping {percent}%",
746
+ passwordPrompt: "This folder is password protected:",
747
+ passwordPlaceholder: "Folder password",
748
+ open: "Open",
749
+ columnName: "Name",
750
+ columnSize: "Size",
751
+ columnDate: "Modified",
752
+ loading: "Loading…",
753
+ loadFailed: "Failed to load: {message}",
754
+ guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
755
+ corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
756
+ retry: "Retry",
757
+ empty: "No files",
758
+ preview: "Preview",
759
+ download: "Download",
760
+ loaded: "{count} loaded file(s)",
761
+ selected: "{count} selected",
762
+ unloaded: "{count} folder(s) not expanded, excluded from select-all",
763
+ folderFailed: "Failed to open folder: {message}",
764
+ demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
765
+ demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
766
+ demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
767
+ downloadFailed: "Download failed: {message}",
768
+ previewFailed: "Preview failed: {message}",
769
+ batchFailed: "Failed to create ZIP: {message}",
770
+ defaultZip: "resources",
771
+ clear: "Clear",
772
+ clearSelection: "Clear selection",
773
+ totalSize: "{size} total",
774
+ folderItems: "{count} items",
775
+ sortBy: "Sort",
776
+ selectAll: "Select all",
777
+ selectItem: "Select {name}",
778
+ expand: "Expand {name}",
779
+ collapse: "Collapse {name}",
780
+ close: "Close",
781
+ previous: "Previous",
782
+ next: "Next",
783
+ rotate: "Rotate",
784
+ actualSize: "Actual size",
785
+ previewUnsupported: "This file type cannot be previewed here. Download it to view.",
786
+ previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
787
+ previewTooLarge: "This file is large ({size}). Download it to view.",
788
+ textTruncated: "Showing the first {size} only. Download for the full file.",
789
+ refreshing: "Refreshing",
790
+ refreshFailed: "Refresh failed: {message}",
791
+ cancel: "Cancel",
792
+ passwordFor: "Password for {name}",
793
+ passwordWrong: "Wrong password or no permission",
794
+ passwordStale: "The saved password no longer works. Enter it again.",
795
+ rememberPassword: "Remember in this tab (cleared when the tab closes)",
796
+ remembered: "Remembered",
797
+ forgetPassword: "Forget",
798
+ unlockFolder: "Enter the password for {name}",
799
+ networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
800
+ }
801
+ };
802
+ function formatLabel(template, values = {}) {
803
+ return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
804
+ }
805
+ function resolveLabels(locale = "zh-CN", overrides) {
806
+ return {
807
+ ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
808
+ ...overrides
809
+ };
810
+ }
431
811
  // src/client/PreviewDialog.vue
432
812
  const _hoisted_1$1 = ["aria-label"];
433
813
  const _hoisted_2$1 = {
@@ -452,51 +832,33 @@ const _hoisted_13$1 = [
452
832
  "aria-label",
453
833
  "title"
454
834
  ];
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 = {
835
+ const _hoisted_14$1 = { class: "fl-preview-tools fl-preview-actions" };
836
+ const _hoisted_15$1 = ["aria-label", "title"];
837
+ const _hoisted_16$1 = ["aria-label", "title"];
838
+ const _hoisted_17$1 = ["data-mode"];
839
+ const _hoisted_18$1 = {
478
840
  key: 0,
479
841
  class: "fl-preview-status",
480
842
  role: "status"
481
843
  };
482
- const _hoisted_22$1 = ["src", "alt"];
483
- const _hoisted_23$1 = ["src"];
484
- const _hoisted_24$1 = ["src"];
485
- const _hoisted_25$1 = {
844
+ const _hoisted_19$1 = ["src", "alt"];
845
+ const _hoisted_20$1 = ["src"];
846
+ const _hoisted_21$1 = ["src"];
847
+ const _hoisted_22$1 = {
486
848
  key: 4,
487
849
  class: "fl-preview-text"
488
850
  };
489
- const _hoisted_26$1 = {
851
+ const _hoisted_23$1 = {
490
852
  key: 0,
491
853
  class: "fl-preview-note"
492
854
  };
493
- const _hoisted_27$1 = ["src", "title"];
494
- const _hoisted_28$1 = {
855
+ const _hoisted_24$1 = ["src", "title"];
856
+ const _hoisted_25$1 = {
495
857
  key: 6,
496
858
  class: "fl-preview-info"
497
859
  };
498
- const _hoisted_29$1 = ["data-kind"];
499
- const _hoisted_30$1 = { class: "fl-preview-message" };
860
+ const _hoisted_26$1 = ["data-kind"];
861
+ const _hoisted_27$1 = { class: "fl-preview-message" };
500
862
  const _sfc_main$1 = /*@__PURE__*/ defineComponent({
501
863
  __name: "PreviewDialog",
502
864
  props: {
@@ -527,11 +889,12 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
527
889
  ],
528
890
  setup(__props, { emit: __emit }) {
529
891
  /**
530
- * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
892
+ * File preview in a native <dialog>, without leaving the page.
531
893
  *
532
- * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
533
- * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
534
- * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
894
+ * image / video / audio: native elements; text: the first `maxTextBytes`
895
+ * through a Range request; pdf: the first matching OpenList `iframe_previews`
896
+ * viewer, else the browser's reader through a Blob. Anything else (Office
897
+ * included), failures, oversized PDFs and demo mode: file info + download.
535
898
  */
536
899
  const props = __props;
537
900
  const emit = __emit;
@@ -715,17 +1078,10 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
715
1078
  "aria-label": t("previous"),
716
1079
  title: t("previous"),
717
1080
  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),
1081
+ }, [createVNode(_sfc_main$2, {
1082
+ name: "previous",
1083
+ size: 18
1084
+ })], 8, _hoisted_9$1),
729
1085
  createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
730
1086
  createElementVNode("button", {
731
1087
  class: "fl-preview-tool",
@@ -733,98 +1089,62 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
733
1089
  "aria-label": t("next"),
734
1090
  title: t("next"),
735
1091
  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)
1092
+ }, [createVNode(_sfc_main$2, {
1093
+ name: "next",
1094
+ size: 18
1095
+ })], 8, _hoisted_11$1)
747
1096
  ], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
748
1097
  class: "fl-preview-tool fl-preview-rotate",
749
1098
  type: "button",
750
1099
  "aria-label": t("rotate"),
751
1100
  title: t("rotate"),
752
1101
  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", {
1102
+ }, [createVNode(_sfc_main$2, {
1103
+ name: "refresh",
1104
+ size: 17
1105
+ })], 8, _hoisted_12$1), createElementVNode("button", {
764
1106
  class: "fl-preview-tool fl-preview-zoom",
765
1107
  type: "button",
766
1108
  "aria-pressed": actualSize.value,
767
1109
  "aria-label": t("actualSize"),
768
1110
  title: t("actualSize"),
769
1111
  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", {
1112
+ }, [createVNode(_sfc_main$2, {
1113
+ name: actualSize.value ? "zoomOut" : "zoomIn",
1114
+ size: 17
1115
+ }, null, 8, ["name"])], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
1116
+ createElementVNode("div", _hoisted_14$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
780
1117
  key: 0,
781
1118
  class: "fl-preview-tool fl-preview-download",
782
1119
  type: "button",
783
1120
  "aria-label": t("download"),
784
1121
  title: t("download"),
785
1122
  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", {
1123
+ }, [createVNode(_sfc_main$2, {
1124
+ name: "download",
1125
+ size: 17
1126
+ })], 8, _hoisted_15$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
801
1127
  class: "fl-preview-tool fl-preview-close",
802
1128
  type: "button",
803
1129
  autofocus: "",
804
1130
  "aria-label": t("close"),
805
1131
  title: t("close"),
806
1132
  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)])
1133
+ }, [createVNode(_sfc_main$2, {
1134
+ name: "close",
1135
+ size: 18
1136
+ })], 8, _hoisted_16$1)])
817
1137
  ]), createElementVNode("div", {
818
1138
  class: "fl-preview-body",
819
1139
  "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", {
1140
+ }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_18$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
821
1141
  key: 1,
822
1142
  class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
823
1143
  src: __props.src,
824
1144
  alt: __props.file.name,
825
1145
  style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
826
1146
  onError: markFailed
827
- }, null, 46, _hoisted_22$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
1147
+ }, null, 46, _hoisted_19$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
828
1148
  key: 2,
829
1149
  class: "fl-preview-video",
830
1150
  src: __props.src,
@@ -832,189 +1152,421 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
832
1152
  playsinline: "",
833
1153
  preload: "metadata",
834
1154
  onError: markFailed
835
- }, null, 40, _hoisted_23$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
1155
+ }, null, 40, _hoisted_20$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
836
1156
  key: 3,
837
1157
  class: "fl-preview-audio",
838
1158
  src: __props.src,
839
1159
  controls: "",
840
1160
  preload: "metadata",
841
1161
  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", {
1162
+ }, null, 40, _hoisted_21$1)) : mode.value === "text" ? (openBlock(), createElementBlock("div", _hoisted_22$1, [truncated.value ? (openBlock(), createElementBlock("p", _hoisted_23$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
1163
  key: 5,
844
1164
  class: "fl-preview-frame",
845
1165
  src: __props.viewer || blobUrl.value,
846
1166
  title: __props.file.name
847
- }, null, 8, _hoisted_27$1)) : (openBlock(), createElementBlock("div", _hoisted_28$1, [
1167
+ }, null, 8, _hoisted_24$1)) : (openBlock(), createElementBlock("div", _hoisted_25$1, [
848
1168
  createElementVNode("span", {
849
1169
  class: "fl-type fl-preview-badge",
850
1170
  "data-kind": unref(fileKind)(__props.file.name),
851
1171
  "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),
1172
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_26$1),
1173
+ createElementVNode("p", _hoisted_27$1, toDisplayString(infoMessage.value), 1),
854
1174
  !__props.demo ? (openBlock(), createElementBlock("button", {
855
1175
  key: 0,
856
1176
  class: "fl-btn fl-btn-primary",
857
1177
  type: "button",
858
1178
  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);
1179
+ }, [createVNode(_sfc_main$2, {
1180
+ name: "download",
1181
+ size: 15
1182
+ }), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
1183
+ ]))], 8, _hoisted_17$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
875
1184
  };
876
1185
  }
877
1186
  });
1187
+ // src/client/tree.ts
1188
+ function newLock(prompting = false, error = "") {
1189
+ return {
1190
+ prompting,
1191
+ input: "",
1192
+ error,
1193
+ remember: false,
1194
+ busy: false
1195
+ };
1196
+ }
1197
+ function toNodes(items, parent, password) {
1198
+ return items.map((item) => ({
1199
+ ...item,
1200
+ path: joinPath(parent.path, item.name),
1201
+ depth: parent.depth + 1,
1202
+ expanded: false,
1203
+ loaded: false,
1204
+ loading: false,
1205
+ children: [],
1206
+ password,
1207
+ zipPath: [...parent.zipPath, item.name]
1208
+ }));
1209
+ }
1210
+ function sortTree(nodes, key, ascending) {
1211
+ nodes.sort((a, b) => compareNodes(a, b, key, ascending));
1212
+ for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
1213
+ }
1214
+ /** Loaded files at or under a node. */
1215
+ function descendantFiles(node) {
1216
+ return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
1217
+ }
1218
+ function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1219
+ for (const node of nodes) if (node.is_dir && node.expanded) {
1220
+ out.add(node.path);
1221
+ expandedPathsOf(node.children, out);
1222
+ }
1223
+ return out;
1224
+ }
1225
+ /** Last path segment, for labels. */
1226
+ function sourceName(path) {
1227
+ return path.split("/").filter(Boolean).pop() || path;
1228
+ }
1229
+ /** Loaded file count, size and selected count for every folder, in one pass. */
1230
+ function folderStats(roots, selected) {
1231
+ const stats = /* @__PURE__ */ new Map();
1232
+ const walk = (node) => {
1233
+ if (!node.is_dir) return {
1234
+ files: 1,
1235
+ bytes: totalSize([node]),
1236
+ selected: selected.has(node.path) ? 1 : 0
1237
+ };
1238
+ const total = {
1239
+ files: 0,
1240
+ bytes: 0,
1241
+ selected: 0
1242
+ };
1243
+ for (const child of node.children) {
1244
+ const part = walk(child);
1245
+ total.files += part.files;
1246
+ total.bytes += part.bytes;
1247
+ total.selected += part.selected;
1248
+ }
1249
+ stats.set(node.path, total);
1250
+ return total;
1251
+ };
1252
+ roots.forEach(walk);
1253
+ return stats;
1254
+ }
1255
+ // src/client/passwords.ts
1256
+ /**
1257
+ * Folder passwords remembered for the current browser tab.
1258
+ *
1259
+ * Stored in `sessionStorage`, so they live only in this tab and are gone when
1260
+ * it closes. Keyed by OpenList base URL, then by folder path; a remembered
1261
+ * folder also covers its sub-folders. Every access is guarded: private mode
1262
+ * or blocked storage simply means nothing is remembered.
1263
+ */
1264
+ const PREFIX = "vitepress-plugin-filelist:passwords:";
1265
+ function store() {
1266
+ try {
1267
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
1268
+ } catch {
1269
+ return null;
1270
+ }
1271
+ }
1272
+ function read(base) {
1273
+ try {
1274
+ const raw = store()?.getItem(PREFIX + base);
1275
+ const parsed = raw ? JSON.parse(raw) : {};
1276
+ return parsed && typeof parsed === "object" ? parsed : {};
1277
+ } catch {
1278
+ return {};
1279
+ }
1280
+ }
1281
+ function write(base, map) {
1282
+ try {
1283
+ const target = store();
1284
+ if (!target) return;
1285
+ if (Object.keys(map).length) target.setItem(PREFIX + base, JSON.stringify(map));
1286
+ else target.removeItem(PREFIX + base);
1287
+ } catch {}
1288
+ }
1289
+ function normalise(path) {
1290
+ return path.replace(/\/+$/, "") || "/";
1291
+ }
1292
+ /** The closest remembered folder at or above `path`, if any. */
1293
+ function findRememberedPassword(base, path) {
1294
+ let best = null;
1295
+ for (const [folder, password] of Object.entries(read(base))) {
1296
+ if (typeof password !== "string" || !isWithin(path, folder)) continue;
1297
+ if (!best || folder.length > best.path.length) best = {
1298
+ path: folder,
1299
+ password
1300
+ };
1301
+ }
1302
+ return best;
1303
+ }
1304
+ function rememberFolderPassword(base, path, password) {
1305
+ write(base, {
1306
+ ...read(base),
1307
+ [normalise(path)]: password
1308
+ });
1309
+ }
1310
+ function forgetFolderPassword(base, path) {
1311
+ const map = read(base);
1312
+ delete map[normalise(path)];
1313
+ write(base, map);
1314
+ }
1315
+ /** Folders with a remembered password, for showing the "remembered" tag. */
1316
+ function rememberedFolders(base) {
1317
+ return Object.keys(read(base));
1318
+ }
1319
+ // src/client/usePasswords.ts
1320
+ /**
1321
+ * The one place that decides which folder password to send.
1322
+ *
1323
+ * Order: typed on this page for this exact folder → the item's own password
1324
+ * (site-configured, or inherited from the folder it was listed with) → typed
1325
+ * for an enclosing folder → remembered in this tab → the component default.
1326
+ */
1327
+ function usePasswords(options) {
1328
+ /** Typed on this page; not necessarily remembered. Survives refreshes. */
1329
+ const typed = /* @__PURE__ */ new Map();
1330
+ /** Remembered folders, mirrored from sessionStorage so templates react. */
1331
+ const remembered = ref(/* @__PURE__ */ new Set());
1332
+ function sync() {
1333
+ remembered.value = new Set(options.enabled() ? rememberedFolders(options.scope()) : []);
1334
+ }
1335
+ function typedAbove(path) {
1336
+ let best = "";
1337
+ let bestFolder = "";
1338
+ for (const [folder, password] of typed) if (isWithin(path, folder) && folder.length > bestFolder.length) {
1339
+ best = password;
1340
+ bestFolder = folder;
1341
+ }
1342
+ return best;
1343
+ }
1344
+ function resolve(path, own = "") {
1345
+ const exact = typed.get(path);
1346
+ if (exact) return { password: exact };
1347
+ if (own) return { password: own };
1348
+ const above = typedAbove(path);
1349
+ if (above) return { password: above };
1350
+ const stored = options.enabled() ? findRememberedPassword(options.scope(), path) : null;
1351
+ if (stored) return {
1352
+ password: stored.password,
1353
+ rememberedAt: stored.path
1354
+ };
1355
+ return { password: options.fallback() };
1356
+ }
1357
+ /** A password worked: keep it for this page, and in this tab when asked. */
1358
+ function accept(path, password, remember) {
1359
+ typed.set(path, password);
1360
+ if (remember && options.enabled()) {
1361
+ rememberFolderPassword(options.scope(), path, password);
1362
+ sync();
1363
+ }
1364
+ }
1365
+ function forget(path) {
1366
+ typed.delete(path);
1367
+ forgetFolderPassword(options.scope(), path);
1368
+ sync();
1369
+ }
1370
+ /** A remembered password stopped working: drop it. Returns whether one was dropped. */
1371
+ function dropStale(rememberedAt) {
1372
+ if (!rememberedAt) return false;
1373
+ forgetFolderPassword(options.scope(), rememberedAt);
1374
+ sync();
1375
+ return true;
1376
+ }
1377
+ return {
1378
+ remembered,
1379
+ sync,
1380
+ resolve,
1381
+ accept,
1382
+ forget,
1383
+ dropStale
1384
+ };
1385
+ }
878
1386
  // src/client/FileList.vue
879
- const _hoisted_1 = { class: "fl-bar" };
880
- const _hoisted_2 = {
1387
+ const _hoisted_1 = ["aria-busy"];
1388
+ const _hoisted_2 = { class: "fl-bar" };
1389
+ const _hoisted_3 = {
881
1390
  key: 0,
882
1391
  class: "fl-selection",
883
1392
  role: "status"
884
1393
  };
885
- const _hoisted_3 = { class: "fl-selection-count" };
886
- const _hoisted_4 = { class: "fl-selection-size" };
887
- const _hoisted_5 = {
1394
+ const _hoisted_4 = { class: "fl-selection-count" };
1395
+ const _hoisted_5 = { class: "fl-selection-size" };
1396
+ const _hoisted_6 = {
888
1397
  key: 1,
889
1398
  class: "fl-title"
890
1399
  };
891
- const _hoisted_6 = { class: "fl-title-text" };
892
- const _hoisted_7 = {
1400
+ const _hoisted_7 = { class: "fl-title-text" };
1401
+ const _hoisted_8 = {
893
1402
  key: 0,
894
1403
  class: "fl-demo-badge"
895
1404
  };
896
- const _hoisted_8 = ["aria-label", "title"];
897
- const _hoisted_9 = { class: "fl-clear-text" };
898
- const _hoisted_10 = { class: "fl-tools" };
899
- const _hoisted_11 = { class: "fl-search" };
900
- const _hoisted_12 = ["placeholder", "aria-label"];
901
- const _hoisted_13 = ["aria-label", "title"];
902
- const _hoisted_14 = ["disabled"];
903
- const _hoisted_15 = {
1405
+ const _hoisted_9 = ["aria-label", "title"];
1406
+ const _hoisted_10 = { class: "fl-clear-text" };
1407
+ const _hoisted_11 = { class: "fl-tools" };
1408
+ const _hoisted_12 = { class: "fl-search" };
1409
+ const _hoisted_13 = ["placeholder", "aria-label"];
1410
+ const _hoisted_14 = [
1411
+ "disabled",
1412
+ "aria-label",
1413
+ "title"
1414
+ ];
1415
+ const _hoisted_15 = ["disabled"];
1416
+ const _hoisted_16 = { class: "fl-row fl-head" };
1417
+ const _hoisted_17 = {
904
1418
  key: 0,
905
- class: "fl-password"
1419
+ class: "fl-progress",
1420
+ "aria-hidden": "true"
906
1421
  };
907
- const _hoisted_16 = ["placeholder", "aria-label"];
908
- const _hoisted_17 = { class: "fl-row fl-head" };
909
1422
  const _hoisted_18 = { class: "fl-cell fl-cell-check" };
910
1423
  const _hoisted_19 = [
911
1424
  "aria-label",
912
1425
  "checked",
913
1426
  ".indeterminate"
914
1427
  ];
915
- const _hoisted_20 = { class: "fl-cell fl-cell-name" };
916
- const _hoisted_21 = ["aria-sort"];
917
- const _hoisted_22 = {
1428
+ const _hoisted_20 = ["aria-sort", "onClick"];
1429
+ const _hoisted_21 = {
918
1430
  class: "fl-sort",
919
1431
  "aria-hidden": "true"
920
1432
  };
921
- const _hoisted_23 = { class: "fl-cell fl-cell-size" };
1433
+ const _hoisted_22 = { class: "fl-cell fl-cell-op" };
1434
+ const _hoisted_23 = { class: "fl-sort-select" };
922
1435
  const _hoisted_24 = {
923
- class: "fl-sort",
924
- "aria-hidden": "true"
925
- };
926
- const _hoisted_25 = { class: "fl-cell fl-cell-date" };
927
- const _hoisted_26 = {
928
- class: "fl-sort",
929
- "aria-hidden": "true"
930
- };
931
- const _hoisted_27 = { class: "fl-cell fl-cell-op" };
932
- const _hoisted_28 = { class: "fl-sort-select" };
933
- const _hoisted_29 = {
934
1436
  class: "fl-sort-current",
935
1437
  "aria-hidden": "true"
936
1438
  };
937
- const _hoisted_30 = ["aria-label"];
938
- const _hoisted_31 = ["value"];
939
- const _hoisted_32 = {
940
- key: 1,
1439
+ const _hoisted_25 = ["aria-label"];
1440
+ const _hoisted_26 = ["value"];
1441
+ const _hoisted_27 = {
1442
+ key: 0,
941
1443
  class: "fl-status"
942
1444
  };
943
- const _hoisted_33 = {
1445
+ const _hoisted_28 = ["for"];
1446
+ const _hoisted_29 = ["id"];
1447
+ const _hoisted_30 = { class: "fl-unlock" };
1448
+ const _hoisted_31 = [
1449
+ "id",
1450
+ "data-unlock",
1451
+ "placeholder",
1452
+ "aria-invalid",
1453
+ "aria-describedby"
1454
+ ];
1455
+ const _hoisted_32 = [
1456
+ "aria-pressed",
1457
+ "aria-label",
1458
+ "title"
1459
+ ];
1460
+ const _hoisted_33 = ["disabled"];
1461
+ const _hoisted_34 = {
944
1462
  key: 2,
945
1463
  class: "fl-status fl-error"
946
1464
  };
947
- const _hoisted_34 = {
1465
+ const _hoisted_35 = {
948
1466
  key: 0,
949
1467
  class: "fl-hint"
950
1468
  };
951
- const _hoisted_35 = {
1469
+ const _hoisted_36 = {
952
1470
  key: 1,
953
1471
  class: "fl-hint"
954
1472
  };
955
- const _hoisted_36 = {
1473
+ const _hoisted_37 = {
956
1474
  key: 3,
957
1475
  class: "fl-status"
958
1476
  };
959
- const _hoisted_37 = { class: "fl-cell fl-cell-check" };
960
- const _hoisted_38 = [
1477
+ const _hoisted_38 = { class: "fl-cell fl-cell-check" };
1478
+ const _hoisted_39 = [
1479
+ "disabled",
961
1480
  "aria-label",
962
1481
  "checked",
963
1482
  ".indeterminate",
964
1483
  "onChange"
965
1484
  ];
966
- const _hoisted_39 = ["title"];
967
- const _hoisted_40 = [
1485
+ const _hoisted_40 = ["title"];
1486
+ const _hoisted_41 = [
1487
+ "aria-label",
1488
+ "title",
1489
+ "onClick"
1490
+ ];
1491
+ const _hoisted_42 = [
968
1492
  "aria-label",
969
1493
  "aria-expanded",
970
1494
  "onClick"
971
1495
  ];
972
- const _hoisted_41 = {
973
- key: 1,
1496
+ const _hoisted_43 = {
1497
+ key: 2,
974
1498
  class: "fl-arrow-placeholder",
975
1499
  "aria-hidden": "true"
976
1500
  };
977
- const _hoisted_42 = {
978
- key: 2,
1501
+ const _hoisted_44 = {
1502
+ key: 3,
979
1503
  class: "fl-folder-icon",
980
1504
  viewBox: "0 0 24 24",
981
1505
  width: "18",
982
1506
  height: "18",
983
1507
  "aria-hidden": "true"
984
1508
  };
985
- const _hoisted_43 = ["data-kind"];
986
- const _hoisted_44 = { class: "fl-label" };
987
- const _hoisted_45 = ["onClick"];
988
- const _hoisted_46 = { class: "fl-meta" };
989
- const _hoisted_47 = {
990
- key: 4,
1509
+ const _hoisted_45 = ["data-kind"];
1510
+ const _hoisted_46 = { class: "fl-label" };
1511
+ const _hoisted_47 = ["onClick"];
1512
+ const _hoisted_48 = { class: "fl-meta" };
1513
+ const _hoisted_49 = {
1514
+ key: 5,
991
1515
  class: "fl-count"
992
1516
  };
993
- const _hoisted_48 = {
994
- key: 5,
1517
+ const _hoisted_50 = ["id"];
1518
+ const _hoisted_51 = [
1519
+ "aria-label",
1520
+ "title",
1521
+ "onClick"
1522
+ ];
1523
+ const _hoisted_52 = {
1524
+ key: 8,
995
1525
  class: "fl-loading",
996
1526
  "aria-hidden": "true"
997
1527
  };
998
- const _hoisted_49 = { class: "fl-cell fl-cell-size" };
999
- const _hoisted_50 = { class: "fl-cell fl-cell-date" };
1000
- const _hoisted_51 = { class: "fl-cell fl-cell-op" };
1001
- const _hoisted_52 = [
1528
+ const _hoisted_53 = ["onSubmit", "onKeydown"];
1529
+ const _hoisted_54 = [
1530
+ "onUpdate:modelValue",
1531
+ "data-unlock",
1532
+ "placeholder",
1533
+ "aria-label",
1534
+ "aria-invalid",
1535
+ "aria-describedby"
1536
+ ];
1537
+ const _hoisted_55 = [
1538
+ "aria-pressed",
1539
+ "aria-label",
1540
+ "title",
1541
+ "onClick"
1542
+ ];
1543
+ const _hoisted_56 = ["disabled"];
1544
+ const _hoisted_57 = [
1545
+ "aria-label",
1546
+ "title",
1547
+ "onClick"
1548
+ ];
1549
+ const _hoisted_58 = { class: "fl-cell fl-cell-size" };
1550
+ const _hoisted_59 = { class: "fl-cell fl-cell-date" };
1551
+ const _hoisted_60 = { class: "fl-cell fl-cell-op" };
1552
+ const _hoisted_61 = ["aria-label", "onClick"];
1553
+ const _hoisted_62 = [
1002
1554
  "aria-label",
1003
1555
  "aria-expanded",
1004
1556
  "onClick"
1005
1557
  ];
1006
- const _hoisted_53 = [
1558
+ const _hoisted_63 = [
1007
1559
  "aria-label",
1008
1560
  "title",
1009
1561
  "onClick"
1010
1562
  ];
1011
- const _hoisted_54 = [
1563
+ const _hoisted_64 = [
1012
1564
  "aria-label",
1013
1565
  "title",
1014
1566
  "onClick"
1015
1567
  ];
1016
- const _hoisted_55 = { class: "fl-foot" };
1017
- const _hoisted_56 = {
1568
+ const _hoisted_65 = { class: "fl-foot" };
1569
+ const _hoisted_66 = {
1018
1570
  key: 4,
1019
1571
  class: "fl-toast",
1020
1572
  role: "status"
@@ -1032,391 +1584,365 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1032
1584
  token: { default: "" },
1033
1585
  password: { default: "" },
1034
1586
  title: { default: "" },
1587
+ rememberPassword: {
1588
+ type: Boolean,
1589
+ default: true
1590
+ },
1035
1591
  locale: { default: "zh-CN" },
1036
1592
  labels: {}
1037
1593
  },
1038
1594
  setup(__props) {
1039
1595
  /**
1040
- * FileList.vue —— VitePress 网盘文件列表(界面沿用 FileList,数据来自 OpenList)
1041
- *
1042
- * 用法:
1043
- * <FileList base="https://pan.example.com" path="/课程资料" />
1044
- * <FileList /> // 演示模式
1596
+ * OpenList file browser: folder tree, batch ZIP, folder passwords and an
1597
+ * in-page preview. OpenList access is in api.ts (demo.ts when `base` is empty),
1598
+ * password choice in usePasswords.ts, tree helpers in tree.ts and downloads in
1599
+ * download.ts; this component wires them to the UI.
1045
1600
  */
1046
1601
  const props = __props;
1047
1602
  const text = computed(() => resolveLabels(props.locale, props.labels));
1048
1603
  function t(key, values) {
1049
1604
  return formatLabel(text.value[key], values);
1050
1605
  }
1606
+ /** Prefix for element ids, unique per component instance. */
1607
+ const uid = useId();
1051
1608
  const demo = computed(() => !props.base);
1052
- const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
1053
- const activePassword = ref(props.password);
1609
+ const api = computed(() => demo.value ? createDemoApi() : createOpenListApi(props.base, props.token));
1610
+ const canRemember = computed(() => props.rememberPassword !== false);
1611
+ const passwords = usePasswords({
1612
+ scope: () => props.base.replace(/\/+$/, "") || "demo",
1613
+ enabled: () => canRemember.value,
1614
+ fallback: () => props.password
1615
+ });
1616
+ const remembered = passwords.remembered;
1617
+ const rootEl = ref();
1054
1618
  const roots = ref([]);
1055
1619
  const loadingRoot = ref(false);
1056
1620
  const errorMsg = ref("");
1057
- const needsPassword = ref(false);
1058
- const passwordInput = ref("");
1621
+ const refreshing = ref(false);
1622
+ let refreshSeq = 0;
1623
+ /** Single-path mode: the listed folder itself needs a password. */
1624
+ const rootLock = ref(null);
1059
1625
  const keyword = ref("");
1060
1626
  const selected = ref(/* @__PURE__ */ new Set());
1061
1627
  const toast = ref("");
1062
1628
  const sortKey = ref("name");
1063
1629
  const sortAsc = ref(true);
1064
1630
  const siteTitle = ref("");
1065
- /** OpenList's public `iframe_previews` setting (JSON text). */
1066
1631
  const iframePreviews = ref("");
1067
1632
  const batchBusy = ref(false);
1068
1633
  const batchProgress = ref(0);
1069
- const downloadCache = /* @__PURE__ */ new Map();
1070
1634
  let toastTimer;
1071
- function showToast(msg) {
1072
- toast.value = msg;
1635
+ /** The folder listed in single-path mode, as the parent of the top rows. */
1636
+ const listedFolder = computed(() => ({
1637
+ path: props.path,
1638
+ depth: -1,
1639
+ zipPath: []
1640
+ }));
1641
+ function showToast(message) {
1642
+ toast.value = message;
1073
1643
  clearTimeout(toastTimer);
1074
1644
  toastTimer = setTimeout(() => toast.value = "", 3200);
1075
1645
  }
1076
- function errorText(error) {
1077
- return error instanceof Error ? error.message : String(error);
1646
+ /** A failure toast; browser network errors are usually CORS, so say where to look. */
1647
+ function failureMessage(key, error) {
1648
+ const message = t(key, { message: errorText(error) });
1649
+ return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
1078
1650
  }
1079
- function isPasswordError(error) {
1080
- return /password|密码/i.test(errorText(error));
1651
+ function sortNodes(nodes) {
1652
+ sortTree(nodes, sortKey.value, sortAsc.value);
1081
1653
  }
1082
- const DEMO_TREE = {
1083
- "/": [
1084
- {
1085
- name: "assignments",
1086
- size: 0,
1087
- is_dir: true,
1088
- modified: "2026-09-01T08:00:00+08:00"
1089
- },
1090
- {
1091
- name: "exams",
1092
- size: 0,
1093
- is_dir: true,
1094
- modified: "2026-09-01T08:00:00+08:00"
1095
- },
1096
- {
1097
- name: "materials",
1098
- size: 0,
1099
- is_dir: true,
1100
- modified: "2026-09-01T08:00:00+08:00"
1101
- },
1102
- {
1103
- name: "notes",
1104
- size: 0,
1105
- is_dir: true,
1106
- modified: "2026-09-01T08:00:00+08:00"
1107
- },
1108
- {
1109
- name: "slides",
1110
- size: 0,
1111
- is_dir: true,
1112
- modified: "2026-09-01T08:00:00+08:00"
1113
- }
1114
- ],
1115
- "/assignments": [{
1116
- name: "线代作业参考答案",
1117
- size: 0,
1118
- is_dir: true,
1119
- modified: "2026-10-12T21:30:00+08:00"
1120
- }, {
1121
- name: "作业提交说明.pdf",
1122
- size: 186432,
1123
- is_dir: false,
1124
- modified: "2026-09-03T10:15:00+08:00"
1125
- }],
1126
- "/assignments/线代作业参考答案": [
1127
- {
1128
- name: "第1次作业参考答案.doc",
1129
- size: 412672,
1130
- is_dir: false,
1131
- modified: "2026-09-15T22:41:00+08:00"
1132
- },
1133
- {
1134
- name: "第2次作业参考答案.pdf",
1135
- size: 1048576,
1136
- is_dir: false,
1137
- modified: "2026-09-22T22:03:00+08:00"
1138
- },
1139
- {
1140
- name: "第3次作业参考答案.pdf",
1141
- size: 986112,
1142
- is_dir: false,
1143
- modified: "2026-09-29T21:57:00+08:00"
1144
- },
1145
- {
1146
- name: "第4次作业参考答案.pdf",
1147
- size: 1572864,
1148
- is_dir: false,
1149
- modified: "2026-10-13T22:10:00+08:00"
1150
- }
1151
- ],
1152
- "/exams": [
1153
- {
1154
- name: "2023 期末试题和答案.pdf",
1155
- size: 2345678,
1156
- is_dir: false,
1157
- modified: "2025-01-18T09:20:00+08:00"
1158
- },
1159
- {
1160
- name: "2024 期中试题.pdf",
1161
- size: 1234567,
1162
- is_dir: false,
1163
- modified: "2025-11-02T14:08:00+08:00"
1164
- },
1165
- {
1166
- name: "代数与几何-多年合集-试题和答案.pdf",
1167
- size: 18874368,
1168
- is_dir: false,
1169
- modified: "2025-01-18T09:25:00+08:00"
1170
- }
1171
- ],
1172
- "/materials": [
1173
- {
1174
- name: "线性代数及其应用(Lay 第5版).pdf",
1175
- size: 48231424,
1176
- is_dir: false,
1177
- modified: "2024-07-29T16:44:00+08:00"
1178
- },
1179
- {
1180
- name: "Introduction to Linear Algebra(Strang).pdf",
1181
- size: 36700160,
1182
- is_dir: false,
1183
- modified: "2024-07-29T16:50:00+08:00"
1184
- },
1185
- {
1186
- name: "疑难解答与习题指导.zip",
1187
- size: 9437184,
1188
- is_dir: false,
1189
- modified: "2024-08-05T11:12:00+08:00"
1190
- }
1191
- ],
1192
- "/notes": [{
1193
- name: "线性代数笔记(106页).pdf",
1194
- size: 12582912,
1195
- is_dir: false,
1196
- modified: "2025-06-30T20:31:00+08:00"
1197
- }, {
1198
- name: "期末复习提纲.docx",
1199
- size: 245760,
1200
- is_dir: false,
1201
- modified: "2026-01-05T19:02:00+08:00"
1202
- }],
1203
- "/slides": [{
1204
- name: "第一章 行列式.pptx",
1205
- size: 3145728,
1206
- is_dir: false,
1207
- modified: "2026-09-08T13:26:00+08:00"
1208
- }, {
1209
- name: "第二章 矩阵.pptx",
1210
- size: 4194304,
1211
- is_dir: false,
1212
- modified: "2026-09-15T13:30:00+08:00"
1213
- }]
1214
- };
1215
- class ApiError extends Error {
1216
- code;
1217
- constructor(message, code) {
1218
- super(message);
1219
- this.code = code;
1220
- }
1654
+ function focusUnlock(path) {
1655
+ nextTick(() => {
1656
+ rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
1657
+ });
1221
1658
  }
1222
- function authHeaders() {
1223
- return {
1224
- "Content-Type": "application/json",
1225
- ...props.token ? { Authorization: props.token } : {}
1226
- };
1659
+ /** Show a folder's same-row input; focus only on user actions, never on page load. */
1660
+ function startPrompt(node, focus) {
1661
+ node.lock ??= newLock();
1662
+ node.lock.prompting = true;
1663
+ if (focus) focusUnlock(node.path);
1227
1664
  }
1228
- async function request(apiPath, body) {
1229
- const response = await fetch(`${baseUrl.value}${apiPath}`, {
1230
- method: body === void 0 ? "GET" : "POST",
1231
- headers: authHeaders(),
1232
- body: body === void 0 ? void 0 : JSON.stringify(body)
1665
+ function cancelUnlock(lock) {
1666
+ Object.assign(lock, {
1667
+ prompting: false,
1668
+ input: "",
1669
+ error: ""
1233
1670
  });
1234
- const payload = await response.json().catch(() => null);
1235
- if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
1236
- if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
1237
- return payload.data;
1238
- }
1239
- async function listDir(path, password = activePassword.value) {
1240
- if (demo.value) {
1241
- await new Promise((resolve) => setTimeout(resolve, 220));
1242
- return DEMO_TREE[path] ?? [];
1243
- }
1244
- return (await request("/api/fs/list", {
1245
- path,
1246
- password,
1247
- page: 1,
1248
- per_page: 0,
1249
- refresh: false
1250
- }))?.content ?? [];
1251
1671
  }
1252
- function absoluteUrl(raw) {
1253
- if (!raw) return "";
1254
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
1255
- const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
1256
- try {
1257
- return new URL(raw, `${root}/`).href;
1258
- } catch {
1259
- return raw;
1260
- }
1672
+ function unlockErrorText(error) {
1673
+ if (error === "wrong") return t("passwordWrong");
1674
+ if (error === "stale") return t("passwordStale");
1675
+ return "";
1261
1676
  }
1262
- async function resolveUrl(node) {
1263
- if (demo.value) return "";
1264
- if (node.raw_url) return absoluteUrl(node.raw_url);
1265
- const cached = downloadCache.get(node.path);
1266
- if (cached) return cached;
1267
- let url = "";
1677
+ /**
1678
+ * Try the typed password with `open`. On success keep it (and remember it in
1679
+ * this tab when asked); on a wrong one clear the input and refocus it.
1680
+ */
1681
+ async function tryUnlock(lock, path, open, failure) {
1682
+ const password = lock.input;
1683
+ if (!password || lock.busy) return false;
1684
+ lock.busy = true;
1268
1685
  try {
1269
- url = absoluteUrl((await request("/api/fs/get", {
1270
- path: node.path,
1271
- password: node.password || activePassword.value
1272
- }))?.raw_url ?? "");
1273
- } catch {
1274
- url = "";
1686
+ await open(password);
1687
+ passwords.accept(path, password, lock.remember);
1688
+ return true;
1689
+ } catch (error) {
1690
+ if (isPasswordError(error)) {
1691
+ Object.assign(lock, {
1692
+ error: "wrong",
1693
+ input: ""
1694
+ });
1695
+ focusUnlock(path);
1696
+ } else showToast(failureMessage(failure, error));
1697
+ return false;
1698
+ } finally {
1699
+ lock.busy = false;
1275
1700
  }
1276
- if (!url) url = directUrl(node);
1277
- downloadCache.set(node.path, url);
1278
- return url;
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
1701
  }
1284
- async function loadSiteTitle() {
1285
- if (demo.value) return;
1286
- try {
1287
- const data = await request("/api/public/settings");
1288
- siteTitle.value = data?.site_title || data?.title || "";
1289
- iframePreviews.value = data?.iframe_previews || "";
1290
- } catch {
1291
- siteTitle.value = "";
1292
- iframePreviews.value = "";
1293
- }
1702
+ async function submitUnlock(node) {
1703
+ if (!node.lock) return;
1704
+ if (await tryUnlock(node.lock, node.path, async (password) => {
1705
+ if (node.unresolved) {
1706
+ const data = await api.value.get(node.path, password);
1707
+ const name = node.name;
1708
+ Object.assign(node, data, {
1709
+ name,
1710
+ unresolved: false
1711
+ });
1712
+ node.zipPath = [node.is_dir ? name : data.name || node.zipPath[0]];
1713
+ }
1714
+ if (node.is_dir) {
1715
+ await fetchChildren(node, password);
1716
+ node.expanded = true;
1717
+ }
1718
+ }, "folderFailed")) node.lock = void 0;
1294
1719
  }
1295
- function sortTree(nodes) {
1296
- nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
1297
- for (const node of nodes) if (node.children.length) sortTree(node.children);
1720
+ async function submitRootUnlock() {
1721
+ if (!rootLock.value) return;
1722
+ let nodes = [];
1723
+ if (!await tryUnlock(rootLock.value, props.path, async (password) => {
1724
+ nodes = await listRoot(password);
1725
+ }, "loadFailed")) return;
1726
+ roots.value = nodes;
1727
+ rootLock.value = null;
1728
+ selected.value = /* @__PURE__ */ new Set();
1729
+ api.value.clearCache();
1298
1730
  }
1299
- function toNodes(items, parent, depth, password = "", zipParent = []) {
1300
- return items.map((item) => ({
1301
- ...item,
1302
- path: joinPath(parent, item.name),
1303
- depth,
1731
+ /** Forget a remembered password: the folder folds up and asks again next time. */
1732
+ function forgetPassword(node) {
1733
+ passwords.forget(node.path);
1734
+ Object.assign(node, {
1304
1735
  expanded: false,
1305
1736
  loaded: false,
1306
- loading: false,
1307
1737
  children: [],
1308
- password,
1309
- zipPath: [...zipParent, item.name]
1310
- }));
1311
- }
1312
- function setSort(key) {
1313
- if (sortKey.value === key) sortAsc.value = !sortAsc.value;
1314
- else {
1315
- sortKey.value = key;
1316
- sortAsc.value = true;
1317
- }
1318
- resortAll();
1738
+ lock: void 0
1739
+ });
1740
+ selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
1741
+ api.value.clearCache(node.path);
1319
1742
  }
1320
- function resortAll() {
1321
- if (props.sources.length) {
1322
- for (const root of roots.value) if (root.children.length) sortTree(root.children);
1323
- } else sortTree(roots.value);
1743
+ async function fetchChildren(node, password) {
1744
+ node.children = toNodes(await api.value.list(node.path, password), node, password);
1745
+ sortNodes(node.children);
1746
+ node.loaded = true;
1324
1747
  }
1325
- function sourceName(path) {
1326
- const parts = path.split("/").filter(Boolean);
1327
- return parts[parts.length - 1] || path;
1748
+ async function listRoot(password) {
1749
+ const nodes = toNodes(await api.value.list(props.path, password), listedFolder.value, password);
1750
+ sortNodes(nodes);
1751
+ return nodes;
1328
1752
  }
1329
- async function loadChildren(node) {
1753
+ /**
1754
+ * Load a folder's children. A password error locks the folder instead of
1755
+ * toasting; `prompt` also shows its same-row input (and focuses it).
1756
+ */
1757
+ async function loadChildren(node, prompt = false) {
1330
1758
  if (node.loaded) return true;
1759
+ if (node.lock) {
1760
+ if (prompt) startPrompt(node, prompt === "focus");
1761
+ return false;
1762
+ }
1331
1763
  node.loading = true;
1764
+ const { password, rememberedAt } = passwords.resolve(node.path, node.password);
1332
1765
  try {
1333
- node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
1334
- sortTree(node.children);
1335
- node.loaded = true;
1766
+ await fetchChildren(node, password);
1336
1767
  return true;
1337
1768
  } catch (error) {
1338
- if (isPasswordError(error)) needsPassword.value = true;
1339
- showToast(t("folderFailed", { message: errorText(error) }));
1769
+ if (isPasswordError(error)) {
1770
+ node.expanded = false;
1771
+ node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
1772
+ if (prompt) startPrompt(node, prompt === "focus");
1773
+ } else showToast(failureMessage("folderFailed", error));
1340
1774
  return false;
1341
1775
  } finally {
1342
1776
  node.loading = false;
1343
1777
  }
1344
1778
  }
1345
- async function loadSourceRoot(source) {
1779
+ /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1780
+ function looksLikeFile(path) {
1781
+ const name = sourceName(path);
1782
+ return fileKind(name) !== "other" || previewKind(name) !== "none";
1783
+ }
1784
+ async function loadSourceRoot(source, expanded) {
1346
1785
  const path = source.path;
1347
- const password = source.password ?? "";
1786
+ const own = source.password ?? "";
1348
1787
  let item = {
1349
1788
  name: sourceName(path),
1350
1789
  size: 0,
1351
- is_dir: true,
1790
+ is_dir: !looksLikeFile(path),
1352
1791
  modified: ""
1353
1792
  };
1354
- if (demo.value) {
1355
- if (/\/[^/]+\.[^/]+$/.test(path)) item = {
1356
- ...item,
1357
- is_dir: false,
1358
- modified: "2026-09-01T08:00:00+08:00"
1359
- };
1360
- } else try {
1361
- const data = await request("/api/fs/get", {
1362
- path,
1363
- password: password || activePassword.value
1364
- });
1793
+ let lock;
1794
+ const { password, rememberedAt } = passwords.resolve(path, own);
1795
+ try {
1365
1796
  item = {
1366
1797
  ...item,
1367
- ...data
1798
+ ...await api.value.get(path, password)
1368
1799
  };
1369
- } catch {}
1800
+ } catch (error) {
1801
+ if (isPasswordError(error)) lock = newLock(expanded, passwords.dropStale(rememberedAt) ? "stale" : "");
1802
+ }
1370
1803
  const name = source.title || item.name || sourceName(path);
1371
1804
  return {
1372
1805
  ...item,
1373
1806
  name,
1374
1807
  path,
1375
1808
  depth: 0,
1376
- expanded: source.expanded ?? props.expanded,
1809
+ expanded: expanded && !lock,
1377
1810
  loaded: false,
1378
1811
  loading: false,
1379
1812
  children: [],
1380
- password,
1813
+ password: own,
1381
1814
  isRoot: true,
1815
+ lock,
1816
+ unresolved: !!lock,
1382
1817
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
1383
1818
  };
1384
1819
  }
1820
+ class RootLockedError extends Error {
1821
+ rememberedAt;
1822
+ constructor(rememberedAt) {
1823
+ super("root locked");
1824
+ this.rememberedAt = rememberedAt;
1825
+ }
1826
+ }
1827
+ /**
1828
+ * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
1829
+ * what was open); without it the configured `expanded` defaults apply.
1830
+ */
1831
+ async function buildTree(expandedPaths) {
1832
+ let nodes;
1833
+ if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1834
+ else {
1835
+ const { password, rememberedAt } = passwords.resolve(props.path);
1836
+ try {
1837
+ nodes = await listRoot(password);
1838
+ } catch (error) {
1839
+ throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
1840
+ }
1841
+ }
1842
+ const open = async (list) => {
1843
+ for (const node of list) {
1844
+ const wanted = node.expanded || expandedPaths?.has(node.path);
1845
+ if (!node.is_dir || node.lock || !wanted) continue;
1846
+ node.expanded = await loadChildren(node, "show");
1847
+ if (node.expanded && expandedPaths) await open(node.children);
1848
+ }
1849
+ };
1850
+ await open(nodes);
1851
+ return nodes;
1852
+ }
1853
+ function lockRoot(error, fallback) {
1854
+ roots.value = [];
1855
+ selected.value = /* @__PURE__ */ new Set();
1856
+ rootLock.value = newLock(true, passwords.dropStale(error.rememberedAt) ? "stale" : fallback);
1857
+ }
1858
+ /** First load, or a retry after an error: shows the loading state. */
1385
1859
  async function loadRoot() {
1860
+ refreshSeq += 1;
1861
+ refreshing.value = false;
1386
1862
  loadingRoot.value = true;
1387
1863
  errorMsg.value = "";
1388
- needsPassword.value = false;
1389
1864
  try {
1390
- if (props.sources.length) {
1391
- const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
1392
- roots.value = nodes;
1393
- for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
1394
- selected.value = /* @__PURE__ */ new Set();
1395
- downloadCache.clear();
1396
- return;
1397
- }
1398
- const items = await listDir(props.path);
1399
- roots.value = toNodes(items, props.path, 0, activePassword.value);
1400
- sortTree(roots.value);
1865
+ roots.value = await buildTree();
1866
+ rootLock.value = null;
1401
1867
  selected.value = /* @__PURE__ */ new Set();
1402
- downloadCache.clear();
1868
+ api.value.clearCache();
1403
1869
  } catch (error) {
1404
- const message = errorText(error);
1405
- if (isPasswordError(error)) needsPassword.value = true;
1406
- else errorMsg.value = message;
1870
+ if (error instanceof RootLockedError) lockRoot(error, "");
1871
+ else errorMsg.value = errorText(error);
1407
1872
  } finally {
1408
1873
  loadingRoot.value = false;
1409
1874
  }
1410
1875
  }
1876
+ /**
1877
+ * Refresh keeps the old rows in place (dimmed) until the new tree is ready, then
1878
+ * swaps it in. Open folders stay open; selected files that still exist stay selected.
1879
+ */
1880
+ async function refresh() {
1881
+ if (refreshing.value || loadingRoot.value) return;
1882
+ if (!roots.value.length || errorMsg.value || rootLock.value) {
1883
+ await loadRoot();
1884
+ return;
1885
+ }
1886
+ const seq = ++refreshSeq;
1887
+ refreshing.value = true;
1888
+ try {
1889
+ const fresh = await buildTree(expandedPathsOf(roots.value));
1890
+ if (seq !== refreshSeq) return;
1891
+ roots.value = fresh;
1892
+ const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
1893
+ selected.value = new Set([...selected.value].filter((path) => existing.has(path)));
1894
+ api.value.clearCache();
1895
+ } catch (error) {
1896
+ if (seq !== refreshSeq) return;
1897
+ if (error instanceof RootLockedError) lockRoot(error, "wrong");
1898
+ else showToast(failureMessage("refreshFailed", error));
1899
+ } finally {
1900
+ if (seq === refreshSeq) refreshing.value = false;
1901
+ }
1902
+ }
1411
1903
  async function toggleExpand(node) {
1904
+ if (node.lock) {
1905
+ startPrompt(node, true);
1906
+ return;
1907
+ }
1412
1908
  if (!node.is_dir) return;
1413
1909
  if (node.expanded) {
1414
1910
  node.expanded = false;
1415
1911
  return;
1416
1912
  }
1417
- if (!await loadChildren(node)) return;
1418
- node.expanded = true;
1913
+ node.expanded = await loadChildren(node, "focus");
1914
+ }
1915
+ function applySort(key, ascending) {
1916
+ sortKey.value = key;
1917
+ sortAsc.value = ascending;
1918
+ if (props.sources.length) roots.value.forEach((root) => sortNodes(root.children));
1919
+ else sortNodes(roots.value);
1920
+ }
1921
+ function setSort(key) {
1922
+ applySort(key, sortKey.value === key ? !sortAsc.value : true);
1419
1923
  }
1924
+ /** Compact sort control for narrow containers, where header cells are hidden. */
1925
+ const sortOptions = computed(() => [
1926
+ "name",
1927
+ "size",
1928
+ "modified"
1929
+ ].flatMap((key) => {
1930
+ const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
1931
+ return [{
1932
+ value: `${key}:asc`,
1933
+ label: `${label} ↑`
1934
+ }, {
1935
+ value: `${key}:desc`,
1936
+ label: `${label} ↓`
1937
+ }];
1938
+ }));
1939
+ const sortValue = computed({
1940
+ get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
1941
+ set: (value) => {
1942
+ const [key, direction] = value.split(":");
1943
+ applySort(key, direction !== "desc");
1944
+ }
1945
+ });
1420
1946
  const visible = computed(() => {
1421
1947
  const out = [];
1422
1948
  const query = keyword.value.trim().toLowerCase();
@@ -1431,34 +1957,55 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1431
1957
  walk(roots.value);
1432
1958
  return out;
1433
1959
  });
1434
- function descendantFiles(node) {
1435
- if (!node.is_dir) return [node];
1436
- return node.children.flatMap(descendantFiles);
1960
+ const stats = computed(() => folderStats(roots.value, selected.value));
1961
+ const noStats = {
1962
+ files: 0,
1963
+ bytes: 0,
1964
+ selected: 0
1965
+ };
1966
+ function statsOf(node) {
1967
+ return stats.value.get(node.path) ?? noStats;
1437
1968
  }
1438
1969
  function isChecked(node) {
1439
- const files = descendantFiles(node);
1440
- return files.length > 0 && files.every((file) => selected.value.has(file.path));
1970
+ if (!node.is_dir) return selected.value.has(node.path);
1971
+ const { files, selected: hit } = statsOf(node);
1972
+ return files > 0 && hit === files;
1441
1973
  }
1442
1974
  function isIndeterminate(node) {
1443
- const files = descendantFiles(node);
1444
- const hit = files.filter((file) => selected.value.has(file.path)).length;
1445
- return hit > 0 && hit < files.length;
1975
+ if (!node.is_dir) return false;
1976
+ const { files, selected: hit } = statsOf(node);
1977
+ return hit > 0 && hit < files;
1446
1978
  }
1447
- /** Load a directory and all of its sub-directories. */
1979
+ /**
1980
+ * Load a directory and all of its sub-directories. Stops at the first folder
1981
+ * that needs a password and returns it, so nothing is silently left out.
1982
+ */
1448
1983
  async function loadTree(node) {
1449
- if (!node.is_dir) return true;
1450
- if (!await loadChildren(node)) return false;
1451
- for (const child of node.children) if (child.is_dir && !await loadTree(child)) return false;
1452
- return true;
1984
+ if (!node.is_dir) return { ok: true };
1985
+ if (!await loadChildren(node)) return node.lock ? {
1986
+ ok: false,
1987
+ locked: node
1988
+ } : { ok: false };
1989
+ for (const child of node.children) {
1990
+ if (!child.is_dir) continue;
1991
+ const result = await loadTree(child);
1992
+ if (!result.ok) {
1993
+ node.expanded = true;
1994
+ return result;
1995
+ }
1996
+ }
1997
+ return { ok: true };
1453
1998
  }
1454
1999
  async function toggleCheck(node, event) {
1455
2000
  const input = event?.target;
1456
2001
  if (node.is_dir && !isChecked(node)) {
1457
- if (await loadTree(node)) node.expanded = true;
1458
- else {
2002
+ const result = await loadTree(node);
2003
+ if (!result.ok) {
2004
+ if (result.locked) startPrompt(result.locked, true);
1459
2005
  if (input) input.checked = isChecked(node);
1460
2006
  return;
1461
2007
  }
2008
+ node.expanded = true;
1462
2009
  }
1463
2010
  const files = descendantFiles(node);
1464
2011
  if (!files.length) {
@@ -1466,67 +2013,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1466
2013
  return;
1467
2014
  }
1468
2015
  const next = new Set(selected.value);
1469
- if (files.every((file) => next.has(file.path))) files.forEach((file) => next.delete(file.path));
1470
- else files.forEach((file) => next.add(file.path));
2016
+ const adding = files.some((file) => !next.has(file.path));
2017
+ for (const file of files) if (adding) next.add(file.path);
2018
+ else next.delete(file.path);
1471
2019
  selected.value = next;
1472
2020
  }
1473
2021
  const allFiles = computed(() => roots.value.flatMap(descendantFiles));
1474
- const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
1475
- const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
1476
2022
  const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
1477
- const selectedBytes = computed(() => totalSize(selectedFiles.value));
1478
- const loadedBytes = computed(() => totalSize(allFiles.value));
1479
- function clearSelection() {
1480
- selected.value = /* @__PURE__ */ new Set();
1481
- }
1482
- function folderFiles(node) {
1483
- return node.is_dir && node.loaded ? descendantFiles(node) : [];
1484
- }
1485
- /** Compact sort control for narrow containers, where header cells are hidden. */
1486
- const sortOptions = computed(() => [
1487
- "name",
1488
- "size",
1489
- "modified"
1490
- ].flatMap((key) => {
1491
- const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
1492
- return [{
1493
- value: `${key}:asc`,
1494
- label: `${label} ↑`
1495
- }, {
1496
- value: `${key}:desc`,
1497
- label: `${label} ↓`
1498
- }];
1499
- }));
1500
- const sortValue = computed({
1501
- get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
1502
- set: (value) => {
1503
- const [key, direction] = value.split(":");
1504
- sortKey.value = key;
1505
- sortAsc.value = direction !== "desc";
1506
- resortAll();
1507
- }
1508
- });
2023
+ const allChecked = computed(() => allFiles.value.length > 0 && selectedFiles.value.length === allFiles.value.length);
2024
+ const someChecked = computed(() => !allChecked.value && selectedFiles.value.length > 0);
1509
2025
  function toggleCheckAll() {
1510
2026
  selected.value = allChecked.value ? /* @__PURE__ */ new Set() : new Set(allFiles.value.map((file) => file.path));
1511
2027
  }
1512
- function triggerUrl(url) {
1513
- const anchor = document.createElement("a");
1514
- anchor.href = url;
1515
- anchor.target = "_blank";
1516
- anchor.rel = "noopener";
1517
- document.body.appendChild(anchor);
1518
- anchor.click();
1519
- anchor.remove();
2028
+ function clearSelection() {
2029
+ selected.value = /* @__PURE__ */ new Set();
1520
2030
  }
1521
- function triggerBlob(blob, name) {
1522
- const href = URL.createObjectURL(blob);
1523
- const anchor = document.createElement("a");
1524
- anchor.href = href;
1525
- anchor.download = name;
1526
- document.body.appendChild(anchor);
1527
- anchor.click();
1528
- anchor.remove();
1529
- URL.revokeObjectURL(href);
2031
+ function fileUrlOf(node) {
2032
+ return api.value.fileUrl(node, passwords.resolve(node.path, node.password).password);
1530
2033
  }
1531
2034
  async function download(node) {
1532
2035
  if (node.is_dir) return;
@@ -1535,11 +2038,29 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1535
2038
  return;
1536
2039
  }
1537
2040
  try {
1538
- triggerUrl(await resolveUrl(node));
2041
+ openUrl(await fileUrlOf(node));
1539
2042
  } catch (error) {
1540
2043
  showToast(t("downloadFailed", { message: errorText(error) }));
1541
2044
  }
1542
2045
  }
2046
+ async function batchDownload() {
2047
+ const files = selectedFiles.value;
2048
+ if (!files.length) return;
2049
+ if (demo.value) {
2050
+ showToast(t("demoBatch", { count: files.length }));
2051
+ return;
2052
+ }
2053
+ batchBusy.value = true;
2054
+ batchProgress.value = 0;
2055
+ try {
2056
+ saveBlob(await zipFiles(files, fileUrlOf, (percent) => batchProgress.value = percent), zipFileName(props.title || t("defaultZip")));
2057
+ selected.value = /* @__PURE__ */ new Set();
2058
+ } catch (error) {
2059
+ showToast(t("batchFailed", { message: errorText(error) }));
2060
+ } finally {
2061
+ batchBusy.value = false;
2062
+ }
2063
+ }
1543
2064
  /** Only types the dialog can actually render get a preview button. */
1544
2065
  function canPreview(node) {
1545
2066
  return !node.is_dir && previewKind(node.name) !== "none";
@@ -1563,17 +2084,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1563
2084
  if (demo.value || !canPreview(node)) return;
1564
2085
  previewResolving.value = true;
1565
2086
  try {
1566
- const url = await resolveUrl(node);
2087
+ const url = await fileUrlOf(node);
1567
2088
  if (seq !== previewSeq) return;
1568
2089
  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
- }
2090
+ const viewer = previewKind(node.name) === "pdf" ? iframePreviewsFor(node.name, iframePreviews.value)[0] : void 0;
2091
+ if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
2092
+ name: node.name,
2093
+ rawUrl: url,
2094
+ downloadUrl: api.value.directUrl(node)
2095
+ });
1577
2096
  } catch (error) {
1578
2097
  if (seq === previewSeq) previewError.value = errorText(error);
1579
2098
  } finally {
@@ -1589,54 +2108,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1589
2108
  previewSeq += 1;
1590
2109
  previewNode.value = null;
1591
2110
  }
1592
- async function batchDownload() {
1593
- const files = allFiles.value.filter((file) => selected.value.has(file.path));
1594
- if (!files.length) return;
1595
- if (demo.value) {
1596
- showToast(t("demoBatch", { count: files.length }));
1597
- return;
1598
- }
1599
- batchBusy.value = true;
1600
- batchProgress.value = 0;
1601
- try {
1602
- const entries = {};
1603
- const usedNames = /* @__PURE__ */ new Set();
1604
- const queue = [...files];
1605
- let done = 0;
1606
- const worker = async () => {
1607
- while (queue.length) {
1608
- const file = queue.shift();
1609
- if (!file) break;
1610
- const response = await fetch(await resolveUrl(file));
1611
- if (!response.ok) throw new Error(`${file.name}:HTTP ${response.status}`);
1612
- entries[uniqueEntryName(zipEntryName(file.zipPath), usedNames)] = new Uint8Array(await response.arrayBuffer());
1613
- done += 1;
1614
- batchProgress.value = Math.round(done / files.length * 90);
1615
- }
1616
- };
1617
- await Promise.all(Array.from({ length: Math.min(3, files.length) }, () => worker()));
1618
- const { zip } = await import("fflate");
1619
- const data = await new Promise((resolve, reject) => {
1620
- zip(entries, (error, output) => error ? reject(error) : resolve(output));
1621
- });
1622
- batchProgress.value = 100;
1623
- triggerBlob(new Blob([data], { type: "application/zip" }), `${(props.title || t("defaultZip")).replace(/[\\/:*?"<>|]/g, "_")}.zip`);
1624
- selected.value = /* @__PURE__ */ new Set();
1625
- } catch (error) {
1626
- showToast(t("batchFailed", { message: errorText(error) }));
1627
- } finally {
1628
- batchBusy.value = false;
1629
- }
1630
- }
1631
- function submitPassword() {
1632
- activePassword.value = passwordInput.value;
1633
- downloadCache.clear();
1634
- loadRoot();
1635
- }
1636
2111
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
2112
+ async function loadSettings() {
2113
+ const settings = await api.value.settings();
2114
+ siteTitle.value = settings.title;
2115
+ iframePreviews.value = settings.iframePreviews;
2116
+ }
1637
2117
  const stickyTop = ref(0);
1638
2118
  function measureStickyTop() {
1639
- if (typeof document === "undefined") return;
1640
2119
  let top = 0;
1641
2120
  for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
1642
2121
  const style = getComputedStyle(el);
@@ -1646,8 +2125,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1646
2125
  stickyTop.value = top;
1647
2126
  }
1648
2127
  onMounted(() => {
2128
+ passwords.sync();
1649
2129
  loadRoot();
1650
- loadSiteTitle();
2130
+ loadSettings();
1651
2131
  measureStickyTop();
1652
2132
  window.addEventListener("resize", measureStickyTop, { passive: true });
1653
2133
  });
@@ -1657,11 +2137,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1657
2137
  });
1658
2138
  return (_ctx, _cache) => {
1659
2139
  return openBlock(), createElementBlock("div", {
1660
- class: normalizeClass(["fl-root", { "fl-selecting": selected.value.size > 0 }]),
1661
- style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` })
2140
+ ref_key: "rootEl",
2141
+ ref: rootEl,
2142
+ class: normalizeClass(["fl-root", {
2143
+ "fl-selecting": selected.value.size > 0,
2144
+ "fl-refreshing": refreshing.value
2145
+ }]),
2146
+ style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` }),
2147
+ "aria-busy": refreshing.value || loadingRoot.value
1662
2148
  }, [
1663
- createElementVNode("div", _hoisted_1, [
1664
- selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_2, [createElementVNode("span", _hoisted_3, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_4, toDisplayString(t("totalSize", { size: unref(formatSize)(selectedBytes.value) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_5, [createElementVNode("span", _hoisted_6, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
2149
+ createElementVNode("div", _hoisted_2, [
2150
+ selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_5, toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(selectedFiles.value)) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("span", _hoisted_7, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
1665
2151
  selected.value.size ? (openBlock(), createElementBlock("button", {
1666
2152
  key: 2,
1667
2153
  class: "fl-clear",
@@ -1669,90 +2155,43 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1669
2155
  "aria-label": t("clearSelection"),
1670
2156
  title: t("clearSelection"),
1671
2157
  onClick: clearSelection
1672
- }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
2158
+ }, [createVNode(_sfc_main$2, {
1673
2159
  class: "fl-clear-icon",
1674
- viewBox: "0 0 24 24",
1675
- width: "18",
1676
- height: "18",
1677
- fill: "none",
1678
- stroke: "currentColor",
1679
- "stroke-width": "2.2",
1680
- "stroke-linecap": "round",
1681
- "aria-hidden": "true"
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),
1683
- createElementVNode("div", _hoisted_10, [createElementVNode("label", _hoisted_11, [_cache[10] || (_cache[10] = createElementVNode("svg", {
1684
- viewBox: "0 0 24 24",
1685
- width: "15",
1686
- height: "15",
1687
- fill: "none",
1688
- stroke: "currentColor",
1689
- "stroke-width": "2",
1690
- "stroke-linecap": "round",
1691
- "aria-hidden": "true"
1692
- }, [createElementVNode("circle", {
1693
- cx: "11",
1694
- cy: "11",
1695
- r: "7"
1696
- }), createElementVNode("path", { d: "m20 20-3.5-3.5" })], -1)), withDirectives(createElementVNode("input", {
2160
+ name: "close",
2161
+ size: 18
2162
+ }), createElementVNode("span", _hoisted_10, toDisplayString(t("clear")), 1)], 8, _hoisted_9)) : createCommentVNode("v-if", true),
2163
+ createElementVNode("div", _hoisted_11, [createElementVNode("label", _hoisted_12, [createVNode(_sfc_main$2, {
2164
+ name: "search",
2165
+ size: 15
2166
+ }), withDirectives(createElementVNode("input", {
1697
2167
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
1698
2168
  type: "text",
1699
2169
  placeholder: t("filter"),
1700
2170
  "aria-label": t("filter")
1701
- }, null, 8, _hoisted_12), [[vModelText, keyword.value]])]), createElementVNode("button", {
1702
- class: "fl-icon-btn",
2171
+ }, null, 8, _hoisted_13), [[vModelText, keyword.value]])]), createElementVNode("button", {
2172
+ class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
1703
2173
  type: "button",
1704
- "aria-label": t("refresh"),
1705
- title: t("refresh"),
1706
- onClick: loadRoot
1707
- }, [..._cache[11] || (_cache[11] = [createElementVNode("svg", {
1708
- viewBox: "0 0 24 24",
1709
- width: "15",
1710
- height: "15",
1711
- fill: "none",
1712
- stroke: "currentColor",
1713
- "stroke-width": "2",
1714
- "stroke-linecap": "round",
1715
- "stroke-linejoin": "round",
1716
- "aria-hidden": "true"
1717
- }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 8, _hoisted_13)]),
2174
+ disabled: refreshing.value || loadingRoot.value,
2175
+ "aria-label": refreshing.value ? t("refreshing") : t("refresh"),
2176
+ title: refreshing.value ? t("refreshing") : t("refresh"),
2177
+ onClick: refresh
2178
+ }, [createVNode(_sfc_main$2, {
2179
+ name: "refresh",
2180
+ size: 15
2181
+ })], 10, _hoisted_14)]),
1718
2182
  selected.value.size ? (openBlock(), createElementBlock("button", {
1719
2183
  key: 3,
1720
2184
  class: "fl-btn fl-btn-primary",
1721
2185
  type: "button",
1722
2186
  disabled: batchBusy.value,
1723
2187
  onClick: batchDownload
1724
- }, [_cache[12] || (_cache[12] = createElementVNode("svg", {
1725
- viewBox: "0 0 24 24",
1726
- width: "15",
1727
- height: "15",
1728
- fill: "none",
1729
- stroke: "currentColor",
1730
- "stroke-width": "2.2",
1731
- "stroke-linecap": "round",
1732
- "stroke-linejoin": "round",
1733
- "aria-hidden": "true"
1734
- }, [
1735
- createElementVNode("path", { d: "M12 3v12" }),
1736
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1737
- createElementVNode("path", { d: "M5 21h14" })
1738
- ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_14)) : createCommentVNode("v-if", true)
2188
+ }, [createVNode(_sfc_main$2, {
2189
+ name: "download",
2190
+ size: 15
2191
+ }), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_15)) : createCommentVNode("v-if", true)
1739
2192
  ]),
1740
- needsPassword.value ? (openBlock(), createElementBlock("div", _hoisted_15, [
1741
- createElementVNode("span", null, toDisplayString(t("passwordPrompt")), 1),
1742
- withDirectives(createElementVNode("input", {
1743
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => passwordInput.value = $event),
1744
- type: "password",
1745
- placeholder: t("passwordPlaceholder"),
1746
- "aria-label": t("passwordPlaceholder"),
1747
- onKeyup: withKeys(submitPassword, ["enter"])
1748
- }, null, 40, _hoisted_16), [[vModelText, passwordInput.value]]),
1749
- createElementVNode("button", {
1750
- class: "fl-btn fl-btn-primary",
1751
- type: "button",
1752
- onClick: submitPassword
1753
- }, toDisplayString(t("open")), 1)
1754
- ])) : createCommentVNode("v-if", true),
1755
- createElementVNode("div", _hoisted_17, [
2193
+ createElementVNode("div", _hoisted_16, [
2194
+ refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_17)) : createCommentVNode("v-if", true),
1756
2195
  createElementVNode("label", _hoisted_18, [createElementVNode("input", {
1757
2196
  type: "checkbox",
1758
2197
  "aria-label": t("selectAll"),
@@ -1760,167 +2199,256 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1760
2199
  ".indeterminate": someChecked.value,
1761
2200
  onChange: toggleCheckAll
1762
2201
  }, null, 40, _hoisted_19)]),
1763
- createElementVNode("span", _hoisted_20, [createElementVNode("button", {
1764
- class: "fl-sort-btn",
1765
- type: "button",
1766
- "aria-sort": sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0,
1767
- onClick: _cache[2] || (_cache[2] = ($event) => setSort("name"))
1768
- }, [createTextVNode(toDisplayString(t("columnName")), 1), createElementVNode("span", _hoisted_22, toDisplayString(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_21)]),
1769
- createElementVNode("span", _hoisted_23, [createElementVNode("button", {
1770
- class: "fl-sort-btn",
1771
- type: "button",
1772
- onClick: _cache[3] || (_cache[3] = ($event) => setSort("size"))
1773
- }, [createTextVNode(toDisplayString(t("columnSize")), 1), createElementVNode("span", _hoisted_24, toDisplayString(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1774
- createElementVNode("span", _hoisted_25, [createElementVNode("button", {
1775
- class: "fl-sort-btn",
1776
- type: "button",
1777
- onClick: _cache[4] || (_cache[4] = ($event) => setSort("modified"))
1778
- }, [createTextVNode(toDisplayString(t("columnDate")), 1), createElementVNode("span", _hoisted_26, toDisplayString(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1779
- createElementVNode("span", _hoisted_27, [createElementVNode("label", _hoisted_28, [
2202
+ (openBlock(), createElementBlock(Fragment, null, renderList([
2203
+ "name",
2204
+ "size",
2205
+ "modified"
2206
+ ], (column) => {
2207
+ return createElementVNode("span", {
2208
+ key: column,
2209
+ class: normalizeClass(["fl-cell", `fl-cell-${column === "modified" ? "date" : column}`])
2210
+ }, [createElementVNode("button", {
2211
+ class: "fl-sort-btn",
2212
+ type: "button",
2213
+ "aria-sort": sortKey.value === column ? sortAsc.value ? "ascending" : "descending" : void 0,
2214
+ onClick: ($event) => setSort(column)
2215
+ }, [createTextVNode(toDisplayString(t(column === "name" ? "columnName" : column === "size" ? "columnSize" : "columnDate")), 1), createElementVNode("span", _hoisted_21, toDisplayString(sortKey.value === column ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_20)], 2);
2216
+ }), 64)),
2217
+ createElementVNode("span", _hoisted_22, [createElementVNode("label", _hoisted_23, [
1780
2218
  createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
1781
- createElementVNode("span", _hoisted_29, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
2219
+ createElementVNode("span", _hoisted_24, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
1782
2220
  withDirectives(createElementVNode("select", {
1783
- "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => sortValue.value = $event),
2221
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => sortValue.value = $event),
1784
2222
  "aria-label": t("sortBy")
1785
2223
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
1786
2224
  return openBlock(), createElementBlock("option", {
1787
2225
  key: option.value,
1788
2226
  value: option.value
1789
- }, toDisplayString(option.label), 9, _hoisted_31);
1790
- }), 128))], 8, _hoisted_30), [[vModelSelect, sortValue.value]])
2227
+ }, toDisplayString(option.label), 9, _hoisted_26);
2228
+ }), 128))], 8, _hoisted_25), [[vModelSelect, sortValue.value]])
1791
2229
  ])])
1792
2230
  ]),
1793
- loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_33, [
2231
+ loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_27, toDisplayString(t("loading")), 1)) : rootLock.value ? (openBlock(), createElementBlock("form", {
2232
+ key: 1,
2233
+ class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.value.error }]),
2234
+ onSubmit: withModifiers(submitRootUnlock, ["prevent"])
2235
+ }, [
2236
+ createVNode(_sfc_main$2, {
2237
+ class: "fl-unlock-lock",
2238
+ name: "lock"
2239
+ }),
2240
+ createElementVNode("label", {
2241
+ class: "fl-unlock-label",
2242
+ for: `${unref(uid)}-root`
2243
+ }, toDisplayString(t("passwordPrompt")), 9, _hoisted_28),
2244
+ rootLock.value.error ? (openBlock(), createElementBlock("span", {
2245
+ key: 0,
2246
+ id: `${unref(uid)}-root-error`,
2247
+ class: "fl-unlock-message",
2248
+ role: "alert"
2249
+ }, toDisplayString(unlockErrorText(rootLock.value.error)), 9, _hoisted_29)) : createCommentVNode("v-if", true),
2250
+ createElementVNode("span", _hoisted_30, [
2251
+ withDirectives(createElementVNode("input", {
2252
+ id: `${unref(uid)}-root`,
2253
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => rootLock.value.input = $event),
2254
+ type: "password",
2255
+ autocomplete: "off",
2256
+ "data-unlock": __props.path,
2257
+ placeholder: t("passwordPlaceholder"),
2258
+ "aria-invalid": !!rootLock.value.error,
2259
+ "aria-describedby": rootLock.value.error ? `${unref(uid)}-root-error` : void 0
2260
+ }, null, 8, _hoisted_31), [[vModelText, rootLock.value.input]]),
2261
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2262
+ key: 0,
2263
+ class: "fl-unlock-remember",
2264
+ type: "button",
2265
+ "aria-pressed": rootLock.value.remember,
2266
+ "aria-label": t("rememberPassword"),
2267
+ title: t("rememberPassword"),
2268
+ onClick: _cache[3] || (_cache[3] = ($event) => rootLock.value.remember = !rootLock.value.remember)
2269
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_32)) : createCommentVNode("v-if", true),
2270
+ createElementVNode("button", {
2271
+ class: "fl-unlock-submit",
2272
+ type: "submit",
2273
+ disabled: rootLock.value.busy || !rootLock.value.input
2274
+ }, toDisplayString(t("open")), 9, _hoisted_33)
2275
+ ])
2276
+ ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_34, [
1794
2277
  createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
1795
- /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_34, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
1796
- /failed to fetch|networkerror/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_35, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
2278
+ /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_35, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
2279
+ unref(isNetworkError)(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_36, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
1797
2280
  createElementVNode("button", {
1798
2281
  class: "fl-btn",
1799
2282
  type: "button",
1800
2283
  onClick: loadRoot
1801
2284
  }, toDisplayString(t("retry")), 1)
1802
- ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_36, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
1803
- (openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (node) => {
2285
+ ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_37, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2286
+ (openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (node, index) => {
1804
2287
  return openBlock(), createElementBlock("div", {
1805
2288
  key: node.path,
1806
2289
  class: normalizeClass(["fl-row", {
1807
2290
  "fl-row-dir": node.is_dir,
1808
2291
  "fl-row-root": node.isRoot,
1809
- "fl-row-selected": !node.is_dir && selected.value.has(node.path)
2292
+ "fl-row-selected": !node.is_dir && selected.value.has(node.path),
2293
+ "fl-row-locked": !!node.lock,
2294
+ "fl-row-prompting": node.lock?.prompting,
2295
+ "fl-row-error": node.lock?.prompting && !!node.lock.error
1810
2296
  }]),
1811
2297
  style: normalizeStyle({ "--fl-depth": node.depth })
1812
2298
  }, [
1813
- createElementVNode("label", _hoisted_37, [createElementVNode("input", {
2299
+ createElementVNode("label", _hoisted_38, [createElementVNode("input", {
1814
2300
  type: "checkbox",
2301
+ disabled: !!node.lock,
1815
2302
  "aria-label": t("selectItem", { name: node.name }),
1816
2303
  checked: isChecked(node),
1817
2304
  ".indeterminate": isIndeterminate(node),
1818
2305
  onChange: ($event) => toggleCheck(node, $event)
1819
- }, null, 40, _hoisted_38)]),
2306
+ }, null, 40, _hoisted_39)]),
1820
2307
  createElementVNode("span", {
1821
2308
  class: "fl-cell fl-cell-name",
1822
2309
  title: node.isRoot ? node.path : void 0
1823
2310
  }, [
1824
- _cache[15] || (_cache[15] = createElementVNode("span", {
2311
+ _cache[8] || (_cache[8] = createElementVNode("span", {
1825
2312
  class: "fl-indent",
1826
2313
  "aria-hidden": "true"
1827
2314
  }, null, -1)),
1828
- node.is_dir ? (openBlock(), createElementBlock("button", {
2315
+ node.lock ? (openBlock(), createElementBlock("button", {
1829
2316
  key: 0,
2317
+ class: "fl-arrow fl-lock",
2318
+ type: "button",
2319
+ "aria-label": t("unlockFolder", { name: node.name }),
2320
+ title: t("unlockFolder", { name: node.name }),
2321
+ onClick: ($event) => toggleExpand(node)
2322
+ }, [createVNode(_sfc_main$2, {
2323
+ name: "lock",
2324
+ size: 14
2325
+ })], 8, _hoisted_41)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2326
+ key: 1,
1830
2327
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
1831
2328
  type: "button",
1832
2329
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1833
2330
  "aria-expanded": node.expanded,
1834
2331
  onClick: ($event) => toggleExpand(node)
1835
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
1836
- viewBox: "0 0 24 24",
1837
- width: "14",
1838
- height: "14",
1839
- fill: "none",
1840
- stroke: "currentColor",
1841
- "stroke-width": "2.4",
1842
- "stroke-linecap": "round",
1843
- "stroke-linejoin": "round",
1844
- "aria-hidden": "true"
1845
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_40)) : (openBlock(), createElementBlock("span", _hoisted_41)),
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", {
1847
- key: 3,
2332
+ }, [createVNode(_sfc_main$2, {
2333
+ name: "next",
2334
+ size: 14
2335
+ })], 10, _hoisted_42)) : (openBlock(), createElementBlock("span", _hoisted_43)),
2336
+ node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_44, [..._cache[7] || (_cache[7] = [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", {
2337
+ key: 4,
1848
2338
  class: "fl-type",
1849
2339
  "data-kind": unref(fileKind)(node.name),
1850
2340
  "aria-hidden": "true"
1851
- }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_43)),
1852
- createElementVNode("span", _hoisted_44, [createElementVNode("span", {
2341
+ }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_45)),
2342
+ createElementVNode("span", _hoisted_46, [createElementVNode("span", {
1853
2343
  class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
1854
- onClick: ($event) => node.is_dir ? toggleExpand(node) : download(node)
1855
- }, toDisplayString(node.name), 11, _hoisted_45), createElementVNode("span", _hoisted_46, [node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("folderItems", { count: folderFiles(node).length })) + " · " + toDisplayString(unref(formatSize)(unref(totalSize)(folderFiles(node)))), 1)], 64)) : createCommentVNode("v-if", true)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64))])]),
1856
- node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(t("folderItems", { count: folderFiles(node).length })), 1)) : createCommentVNode("v-if", true),
1857
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_48, "…")) : createCommentVNode("v-if", true)
1858
- ], 8, _hoisted_39),
1859
- createElementVNode("span", _hoisted_49, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
1860
- createElementVNode("span", _hoisted_50, toDisplayString(unref(formatDate)(node.modified)), 1),
1861
- createElementVNode("span", _hoisted_51, [node.is_dir ? (openBlock(), createElementBlock("button", {
1862
- key: 0,
1863
- class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
1864
- type: "button",
1865
- "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1866
- "aria-expanded": node.expanded,
1867
- onClick: ($event) => toggleExpand(node)
1868
- }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
1869
- viewBox: "0 0 24 24",
1870
- width: "18",
1871
- height: "18",
1872
- fill: "none",
1873
- stroke: "currentColor",
1874
- "stroke-width": "2.2",
1875
- "stroke-linecap": "round",
1876
- "stroke-linejoin": "round",
1877
- "aria-hidden": "true"
1878
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_52)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2344
+ onClick: ($event) => node.lock || node.is_dir ? toggleExpand(node) : download(node)
2345
+ }, toDisplayString(node.name), 11, _hoisted_47), createElementVNode("span", _hoisted_48, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64)) : node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(t("folderItems", { count: statsOf(node).files })) + " · " + toDisplayString(unref(formatSize)(statsOf(node).bytes)), 1)], 64)) : createCommentVNode("v-if", true)])]),
2346
+ node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_49, toDisplayString(t("folderItems", { count: statsOf(node).files })), 1)) : createCommentVNode("v-if", true),
2347
+ node.lock?.prompting && node.lock.error ? (openBlock(), createElementBlock("span", {
2348
+ key: 6,
2349
+ id: `${unref(uid)}-error-${index}`,
2350
+ class: "fl-unlock-message",
2351
+ role: "alert"
2352
+ }, toDisplayString(unlockErrorText(node.lock.error)), 9, _hoisted_50)) : createCommentVNode("v-if", true),
2353
+ !node.lock && unref(remembered).has(node.path) ? (openBlock(), createElementBlock("button", {
2354
+ key: 7,
2355
+ class: "fl-remembered",
2356
+ type: "button",
2357
+ "aria-label": `${t("forgetPassword")} ${node.name}`,
2358
+ title: t("forgetPassword"),
2359
+ onClick: ($event) => forgetPassword(node)
2360
+ }, [createVNode(_sfc_main$2, {
2361
+ name: "key",
2362
+ size: 11
2363
+ }), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_51)) : createCommentVNode("v-if", true),
2364
+ node.loading ? (openBlock(), createElementBlock("span", _hoisted_52, "…")) : createCommentVNode("v-if", true)
2365
+ ], 8, _hoisted_40),
2366
+ node.lock?.prompting ? (openBlock(), createElementBlock("form", {
1879
2367
  key: 0,
1880
- class: "fl-action fl-download",
1881
- type: "button",
1882
- "aria-label": `${t("preview")} ${node.name}`,
1883
- title: t("preview"),
1884
- onClick: ($event) => preview(node)
1885
- }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1886
- viewBox: "0 0 24 24",
1887
- width: "16",
1888
- height: "16",
1889
- fill: "none",
1890
- stroke: "currentColor",
1891
- "stroke-width": "2",
1892
- "stroke-linecap": "round",
1893
- "stroke-linejoin": "round",
1894
- "aria-hidden": "true"
1895
- }, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
1896
- cx: "12",
1897
- cy: "12",
1898
- r: "3"
1899
- })], -1)])], 8, _hoisted_53)) : createCommentVNode("v-if", true), createElementVNode("button", {
1900
- class: "fl-action fl-download",
1901
- type: "button",
1902
- "aria-label": `${t("download")} ${node.name}`,
1903
- title: t("download"),
1904
- onClick: ($event) => download(node)
1905
- }, [..._cache[18] || (_cache[18] = [createElementVNode("svg", {
1906
- viewBox: "0 0 24 24",
1907
- width: "16",
1908
- height: "16",
1909
- fill: "none",
1910
- stroke: "currentColor",
1911
- "stroke-width": "2",
1912
- "stroke-linecap": "round",
1913
- "stroke-linejoin": "round",
1914
- "aria-hidden": "true"
2368
+ class: "fl-unlock fl-unlock-row",
2369
+ onSubmit: withModifiers(($event) => submitUnlock(node), ["prevent"]),
2370
+ onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node.lock), ["prevent"]), ["esc"])
1915
2371
  }, [
1916
- createElementVNode("path", { d: "M12 3v12" }),
1917
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1918
- createElementVNode("path", { d: "M5 21h14" })
1919
- ], -1)])], 8, _hoisted_54)], 64))])
2372
+ createVNode(_sfc_main$2, {
2373
+ class: "fl-unlock-lock",
2374
+ name: "lock",
2375
+ size: 18
2376
+ }),
2377
+ withDirectives(createElementVNode("input", {
2378
+ "onUpdate:modelValue": ($event) => node.lock.input = $event,
2379
+ type: "password",
2380
+ autocomplete: "off",
2381
+ "data-unlock": node.path,
2382
+ placeholder: node.lock.error ? unlockErrorText(node.lock.error) : t("passwordPlaceholder"),
2383
+ "aria-label": t("passwordFor", { name: node.name }),
2384
+ "aria-invalid": !!node.lock.error,
2385
+ "aria-describedby": node.lock.error ? `${unref(uid)}-error-${index}` : void 0
2386
+ }, null, 8, _hoisted_54), [[vModelText, node.lock.input]]),
2387
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2388
+ key: 0,
2389
+ class: "fl-unlock-remember",
2390
+ type: "button",
2391
+ "aria-pressed": node.lock.remember,
2392
+ "aria-label": t("rememberPassword"),
2393
+ title: t("rememberPassword"),
2394
+ onClick: ($event) => node.lock.remember = !node.lock.remember
2395
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_55)) : createCommentVNode("v-if", true),
2396
+ createElementVNode("button", {
2397
+ class: "fl-unlock-submit",
2398
+ type: "submit",
2399
+ disabled: node.lock.busy || !node.lock.input
2400
+ }, toDisplayString(t("open")), 9, _hoisted_56),
2401
+ createElementVNode("button", {
2402
+ class: "fl-unlock-cancel",
2403
+ type: "button",
2404
+ "aria-label": t("cancel"),
2405
+ title: t("cancel"),
2406
+ onClick: ($event) => cancelUnlock(node.lock)
2407
+ }, [createVNode(_sfc_main$2, {
2408
+ name: "close",
2409
+ size: 14
2410
+ })], 8, _hoisted_57)
2411
+ ], 40, _hoisted_53)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2412
+ createElementVNode("span", _hoisted_58, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
2413
+ createElementVNode("span", _hoisted_59, toDisplayString(unref(formatDate)(node.modified)), 1),
2414
+ createElementVNode("span", _hoisted_60, [node.lock ? (openBlock(), createElementBlock("button", {
2415
+ key: 0,
2416
+ class: "fl-action fl-arrow-end fl-lock",
2417
+ type: "button",
2418
+ "aria-label": t("unlockFolder", { name: node.name }),
2419
+ onClick: ($event) => toggleExpand(node)
2420
+ }, [createVNode(_sfc_main$2, {
2421
+ name: "lock",
2422
+ size: 18
2423
+ })], 8, _hoisted_61)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2424
+ key: 1,
2425
+ class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
2426
+ type: "button",
2427
+ "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
2428
+ "aria-expanded": node.expanded,
2429
+ onClick: ($event) => toggleExpand(node)
2430
+ }, [createVNode(_sfc_main$2, {
2431
+ name: "next",
2432
+ size: 18
2433
+ })], 10, _hoisted_62)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2434
+ key: 0,
2435
+ class: "fl-action fl-preview-btn",
2436
+ type: "button",
2437
+ "aria-label": `${t("preview")} ${node.name}`,
2438
+ title: t("preview"),
2439
+ onClick: ($event) => preview(node)
2440
+ }, [createVNode(_sfc_main$2, { name: "eye" })], 8, _hoisted_63)) : createCommentVNode("v-if", true), createElementVNode("button", {
2441
+ class: "fl-action fl-download",
2442
+ type: "button",
2443
+ "aria-label": `${t("download")} ${node.name}`,
2444
+ title: t("download"),
2445
+ onClick: ($event) => download(node)
2446
+ }, [createVNode(_sfc_main$2, { name: "download" })], 8, _hoisted_64)], 64))])
2447
+ ], 64))
1920
2448
  ], 6);
1921
2449
  }), 128)),
1922
- createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1)]),
1923
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_56, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
2450
+ createElementVNode("div", _hoisted_65, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) })), 1)]),
2451
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_66, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
1924
2452
  createVNode(_sfc_main$1, {
1925
2453
  open: !!previewNode.value,
1926
2454
  file: previewNode.value,
@@ -1933,9 +2461,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1933
2461
  position: previewPosition.value,
1934
2462
  total: previewPosition.value ? previewList.value.length : 0,
1935
2463
  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))
2464
+ onPrev: _cache[4] || (_cache[4] = ($event) => stepPreview(-1)),
2465
+ onNext: _cache[5] || (_cache[5] = ($event) => stepPreview(1)),
2466
+ onDownload: _cache[6] || (_cache[6] = ($event) => previewNode.value && download(previewNode.value))
1939
2467
  }, null, 8, [
1940
2468
  "open",
1941
2469
  "file",
@@ -1948,7 +2476,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1948
2476
  "position",
1949
2477
  "total"
1950
2478
  ])
1951
- ], 6);
2479
+ ], 14, _hoisted_1);
1952
2480
  };
1953
2481
  }
1954
2482
  });