vitepress-plugin-filelist 0.5.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,147 +1,15 @@
1
- import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, reactive, ref, unref, useSSRContext, watch } from "vue";
1
+ import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, ref, unref, useId, useSSRContext, watch } from "vue";
2
2
  import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
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.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
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
- refreshing: "刷新中",
57
- refreshFailed: "刷新失败:{message}",
58
- cancel: "取消",
59
- passwordFor: "{name} 的密码",
60
- passwordWrong: "密码不正确或无权访问",
61
- passwordStale: "已保存的密码已失效,请重新输入",
62
- rememberPassword: "在此标签页中记住(关闭标签页后失效)",
63
- remembered: "已记住",
64
- forgetPassword: "忘记",
65
- unlockFolder: "输入 {name} 的密码",
66
- networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
67
- },
68
- "en-US": {
69
- allFiles: "All files",
70
- demoBadge: "Demo data",
71
- filter: "Filter files",
72
- refresh: "Refresh",
73
- batch: "Download ZIP",
74
- batchCount: "Download ZIP ({count})",
75
- batching: "Zipping {percent}%",
76
- passwordPrompt: "This folder is password protected:",
77
- passwordPlaceholder: "Folder password",
78
- open: "Open",
79
- columnName: "Name",
80
- columnSize: "Size",
81
- columnDate: "Modified",
82
- loading: "Loading…",
83
- loadFailed: "Failed to load: {message}",
84
- guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
85
- corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
86
- retry: "Retry",
87
- empty: "No files",
88
- preview: "Preview",
89
- download: "Download",
90
- loaded: "{count} loaded file(s)",
91
- selected: "{count} selected",
92
- unloaded: "{count} folder(s) not expanded, excluded from select-all",
93
- folderFailed: "Failed to open folder: {message}",
94
- demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
95
- demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
96
- demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
97
- downloadFailed: "Download failed: {message}",
98
- previewFailed: "Preview failed: {message}",
99
- batchFailed: "Failed to create ZIP: {message}",
100
- defaultZip: "resources",
101
- clear: "Clear",
102
- clearSelection: "Clear selection",
103
- totalSize: "{size} total",
104
- folderItems: "{count} items",
105
- sortBy: "Sort",
106
- selectAll: "Select all",
107
- selectItem: "Select {name}",
108
- expand: "Expand {name}",
109
- collapse: "Collapse {name}",
110
- close: "Close",
111
- previous: "Previous",
112
- next: "Next",
113
- rotate: "Rotate",
114
- actualSize: "Actual size",
115
- previewUnsupported: "This file type cannot be previewed here. Download it to view.",
116
- previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
117
- previewTooLarge: "This file is large ({size}). Download it to view.",
118
- textTruncated: "Showing the first {size} only. Download for the full file.",
119
- refreshing: "Refreshing",
120
- refreshFailed: "Refresh failed: {message}",
121
- cancel: "Cancel",
122
- passwordFor: "Password for {name}",
123
- passwordWrong: "Wrong password or no permission",
124
- passwordStale: "The saved password no longer works. Enter it again.",
125
- rememberPassword: "Remember in this tab (cleared when the tab closes)",
126
- remembered: "Remembered",
127
- forgetPassword: "Forget",
128
- unlockFolder: "Enter the password for {name}",
129
- networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
130
- }
131
- };
132
- function formatLabel(template, values = {}) {
133
- return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
134
- }
135
- function resolveLabels(locale = "zh-CN", overrides) {
136
- return {
137
- ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
138
- ...overrides
139
- };
140
- }
141
3
  // src/client/utils.ts
142
4
  function joinPath(parent, name) {
143
5
  return (parent.endsWith("/") ? parent : `${parent}/`) + name;
144
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
+ }
145
13
  function encodePath(path) {
146
14
  return path.split("/").map(encodeURIComponent).join("/");
147
15
  }
@@ -180,8 +48,9 @@ function uniqueEntryName(name, used) {
180
48
  used.add(candidate);
181
49
  return candidate;
182
50
  }
51
+ /** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
183
52
  function formatSize(size) {
184
- if (!size || !Number.isFinite(size)) return "-";
53
+ if (!Number.isFinite(size) || size < 0) return "-";
185
54
  const units = [
186
55
  "B",
187
56
  "KB",
@@ -205,96 +74,106 @@ function formatDate(iso) {
205
74
  const pad = (value) => String(value).padStart(2, "0");
206
75
  return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
207
76
  }
208
- function extOf(name) {
77
+ /** Lower-case extension (letters and digits only), or `''`. */
78
+ function extension(name) {
209
79
  const match = name.match(/\.([a-z0-9]+)$/i);
210
- 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();
211
85
  }
212
- const FILE_KINDS = {};
213
- for (const [kind, exts] of Object.entries({
214
- 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
+ [
215
92
  "doc",
216
- "docx",
217
- "odt",
218
- "rtf",
219
- "txt",
220
- "md",
221
- "pages",
222
- "wps"
93
+ "text",
94
+ "txt md markdown"
223
95
  ],
224
- sheet: [
225
- "xls",
226
- "xlsx",
227
- "ods",
228
- "csv",
229
- "tsv",
230
- "numbers",
231
- "et"
96
+ [
97
+ "doc",
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"
232
110
  ],
233
- slide: [
234
- "ppt",
235
- "pptx",
236
- "odp",
237
- "key",
238
- "dps"
111
+ [
112
+ "slide",
113
+ "none",
114
+ "ppt pptx odp key dps"
239
115
  ],
240
- pdf: ["pdf"],
241
- image: [
242
- "png",
243
- "jpg",
244
- "jpeg",
245
- "gif",
246
- "webp",
247
- "svg",
248
- "bmp",
249
- "ico",
250
- "avif",
116
+ [
117
+ "pdf",
118
+ "pdf",
119
+ "pdf"
120
+ ],
121
+ [
122
+ "image",
123
+ "image",
124
+ "png jpg jpeg gif webp svg bmp ico avif"
125
+ ],
126
+ [
127
+ "image",
128
+ "none",
251
129
  "heic"
252
130
  ],
253
- media: [
254
- "mp3",
255
- "wav",
256
- "flac",
257
- "aac",
258
- "ogg",
259
- "m4a",
260
- "mp4",
261
- "webm",
262
- "mov",
263
- "mkv",
264
- "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"
265
140
  ],
266
- archive: [
267
- "zip",
268
- "rar",
269
- "7z",
270
- "tar",
271
- "gz",
272
- "tgz",
273
- "bz2",
274
- "xz",
275
- "iso"
141
+ [
142
+ "media",
143
+ "none",
144
+ "mkv avi"
276
145
  ],
277
- code: [
278
- "js",
279
- "ts",
280
- "json",
281
- "html",
282
- "css",
283
- "py",
284
- "java",
285
- "c",
286
- "cpp",
287
- "go",
288
- "rs",
289
- "sh",
290
- "yml",
291
- "yaml",
292
- "xml"
146
+ [
147
+ "archive",
148
+ "none",
149
+ "zip rar 7z tar gz tgz bz2 xz iso"
150
+ ],
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"
155
+ ],
156
+ [
157
+ "other",
158
+ "text",
159
+ "log srt vtt ass lrc gitignore env"
293
160
  ]
294
- })) 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
+ });
295
167
  /** Rough file family, used to colour the type badge. */
296
168
  function fileKind(name) {
297
- 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";
298
177
  }
299
178
  /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
300
179
  function totalSize(items) {
@@ -308,100 +187,6 @@ function compareNodes(a, b, key, ascending = true) {
308
187
  else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
309
188
  return ascending ? result : -result;
310
189
  }
311
- const PREVIEW_EXTS = {
312
- image: [
313
- "png",
314
- "jpg",
315
- "jpeg",
316
- "gif",
317
- "webp",
318
- "svg",
319
- "bmp",
320
- "ico",
321
- "avif"
322
- ],
323
- video: [
324
- "mp4",
325
- "webm",
326
- "m4v",
327
- "mov",
328
- "ogv"
329
- ],
330
- audio: [
331
- "mp3",
332
- "wav",
333
- "ogg",
334
- "oga",
335
- "m4a",
336
- "aac",
337
- "flac",
338
- "opus"
339
- ],
340
- text: [
341
- "txt",
342
- "md",
343
- "markdown",
344
- "log",
345
- "json",
346
- "csv",
347
- "tsv",
348
- "xml",
349
- "yml",
350
- "yaml",
351
- "toml",
352
- "ini",
353
- "conf",
354
- "properties",
355
- "js",
356
- "mjs",
357
- "cjs",
358
- "ts",
359
- "tsx",
360
- "jsx",
361
- "vue",
362
- "css",
363
- "scss",
364
- "less",
365
- "html",
366
- "htm",
367
- "py",
368
- "java",
369
- "kt",
370
- "c",
371
- "h",
372
- "cpp",
373
- "hpp",
374
- "cs",
375
- "go",
376
- "rs",
377
- "rb",
378
- "php",
379
- "sh",
380
- "bat",
381
- "ps1",
382
- "sql",
383
- "srt",
384
- "vtt",
385
- "ass",
386
- "lrc",
387
- "gitignore",
388
- "env"
389
- ],
390
- pdf: ["pdf"]
391
- };
392
- const PREVIEW_KINDS = {};
393
- for (const [kind, exts] of Object.entries(PREVIEW_EXTS)) for (const ext of exts) PREVIEW_KINDS[ext] = kind;
394
- function lowerExt(name) {
395
- const dot = name.lastIndexOf(".");
396
- return dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
397
- }
398
- /**
399
- * How a file is shown in the preview dialog. Office documents and anything
400
- * else the browser cannot render are `none` (info + download).
401
- */
402
- function previewKind(name) {
403
- return PREVIEW_KINDS[lowerExt(name)] ?? "none";
404
- }
405
190
  /**
406
191
  * Viewers from OpenList's public `iframe_previews` setting that match a file,
407
192
  * in the order OpenList lists them. Keys are comma-separated extensions or a
@@ -416,7 +201,7 @@ function iframePreviewsFor(name, setting) {
416
201
  return [];
417
202
  }
418
203
  if (!parsed || typeof parsed !== "object") return [];
419
- const ext = lowerExt(name);
204
+ const ext = extension(name);
420
205
  const out = [];
421
206
  for (const [key, viewers] of Object.entries(parsed)) {
422
207
  let hit = false;
@@ -434,21 +219,553 @@ function iframePreviewsFor(name, setting) {
434
219
  scheme
435
220
  });
436
221
  }
437
- return out;
438
- }
439
- function applyUrlOps(placeholder, url) {
440
- const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
441
- let value = url;
442
- for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
443
- return value;
222
+ return out;
223
+ }
224
+ function applyUrlOps(placeholder, url) {
225
+ const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
226
+ let value = url;
227
+ for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
228
+ return value;
229
+ }
230
+ /**
231
+ * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
232
+ * (signed download URL), each optionally prefixed with `e` (URI-encode) or
233
+ * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
234
+ */
235
+ function fillPreviewUrl(scheme, args) {
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));
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
+ // src/client/icons.ts
535
+ /** Stroke icons on a 24×24 grid, drawn by `Icon.vue`. */
536
+ const ICONS = {
537
+ lock: {
538
+ stroke: 2.2,
539
+ 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"]
540
+ },
541
+ key: {
542
+ stroke: 2,
543
+ paths: [
544
+ "M4 15a4 4 0 1 0 8 0a4 4 0 1 0-8 0",
545
+ "m11 12 9-9",
546
+ "m17 6 3 3"
547
+ ]
548
+ },
549
+ close: {
550
+ stroke: 2.2,
551
+ paths: ["M6 6l12 12", "M18 6 6 18"]
552
+ },
553
+ download: {
554
+ stroke: 2.2,
555
+ paths: [
556
+ "M12 3v12",
557
+ "m7 10 5 5 5-5",
558
+ "M5 21h14"
559
+ ]
560
+ },
561
+ eye: {
562
+ stroke: 2,
563
+ 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"]
564
+ },
565
+ refresh: {
566
+ stroke: 2,
567
+ paths: ["M21 12a9 9 0 1 1-3-6.7", "M21 4v5h-5"]
568
+ },
569
+ search: {
570
+ stroke: 2,
571
+ paths: ["M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0", "m20 20-3.5-3.5"]
572
+ },
573
+ next: {
574
+ stroke: 2.2,
575
+ paths: ["m9 6 6 6-6 6"]
576
+ },
577
+ previous: {
578
+ stroke: 2.2,
579
+ paths: ["m15 6-6 6 6 6"]
580
+ },
581
+ zoomIn: {
582
+ stroke: 2,
583
+ paths: [
584
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
585
+ "m20 20-3.5-3.5",
586
+ "M11 8v6M8 11h6"
587
+ ]
588
+ },
589
+ zoomOut: {
590
+ stroke: 2,
591
+ paths: [
592
+ "M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
593
+ "m20 20-3.5-3.5",
594
+ "M8 11h6"
595
+ ]
596
+ }
597
+ };
598
+ // src/client/Icon.vue
599
+ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
600
+ __name: "Icon",
601
+ __ssrInlineRender: true,
602
+ props: {
603
+ name: {},
604
+ size: { default: 16 }
605
+ },
606
+ setup(__props) {
607
+ return (_ctx, _push, _parent, _attrs) => {
608
+ _push(`<svg${ssrRenderAttrs(mergeProps({
609
+ viewBox: "0 0 24 24",
610
+ width: __props.size,
611
+ height: __props.size,
612
+ fill: "none",
613
+ stroke: "currentColor",
614
+ "stroke-width": unref(ICONS)[__props.name].stroke,
615
+ "stroke-linecap": "round",
616
+ "stroke-linejoin": "round",
617
+ "aria-hidden": "true"
618
+ }, _attrs))}><!--[-->`);
619
+ ssrRenderList(unref(ICONS)[__props.name].paths, (d) => {
620
+ _push(`<path${ssrRenderAttr("d", d)}></path>`);
621
+ });
622
+ _push(`<!--]--></svg>`);
623
+ };
624
+ }
625
+ });
626
+ const _sfc_setup$2 = _sfc_main$2.setup;
627
+ _sfc_main$2.setup = (props, ctx) => {
628
+ const ssrContext = useSSRContext();
629
+ (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/Icon.vue");
630
+ return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
631
+ };
632
+ // src/client/locales.ts
633
+ const fileListLocales = {
634
+ "zh-CN": {
635
+ allFiles: "全部文件",
636
+ demoBadge: "演示数据",
637
+ filter: "筛选文件名",
638
+ refresh: "刷新",
639
+ batch: "下载 ZIP",
640
+ batchCount: "批量下载({count})",
641
+ batching: "打包中 {percent}%",
642
+ passwordPrompt: "该目录受密码保护:",
643
+ passwordPlaceholder: "输入密码",
644
+ open: "打开",
645
+ columnName: "文件名",
646
+ columnSize: "文件大小",
647
+ columnDate: "最后修改日期",
648
+ loading: "加载中…",
649
+ loadFailed: "加载失败:{message}",
650
+ guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
651
+ corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
652
+ retry: "重试",
653
+ empty: "没有文件",
654
+ preview: "预览",
655
+ download: "下载",
656
+ loaded: "{count} 个已加载文件",
657
+ selected: "已选 {count} 项",
658
+ unloaded: "{count} 个目录未展开,未计入全选",
659
+ folderFailed: "打开文件夹失败:{message}",
660
+ demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
661
+ demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
662
+ demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
663
+ downloadFailed: "下载失败:{message}",
664
+ previewFailed: "预览失败:{message}",
665
+ batchFailed: "打包失败:{message}",
666
+ defaultZip: "资源",
667
+ clear: "清除",
668
+ clearSelection: "取消选择",
669
+ totalSize: "共 {size}",
670
+ folderItems: "{count} 项",
671
+ sortBy: "排序",
672
+ selectAll: "全选",
673
+ selectItem: "选择 {name}",
674
+ expand: "展开 {name}",
675
+ collapse: "收起 {name}",
676
+ close: "关闭",
677
+ previous: "上一个",
678
+ next: "下一个",
679
+ rotate: "旋转",
680
+ actualSize: "原始大小",
681
+ previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
682
+ previewUnavailable: "该文件无法在页面内预览,可下载后查看",
683
+ previewTooLarge: "文件较大({size}),请下载后查看",
684
+ textTruncated: "仅显示前 {size},完整内容请下载",
685
+ refreshing: "刷新中",
686
+ refreshFailed: "刷新失败:{message}",
687
+ cancel: "取消",
688
+ passwordFor: "{name} 的密码",
689
+ passwordWrong: "密码不正确或无权访问",
690
+ passwordStale: "已保存的密码已失效,请重新输入",
691
+ rememberPassword: "在此标签页中记住(关闭标签页后失效)",
692
+ remembered: "已记住",
693
+ forgetPassword: "忘记",
694
+ unlockFolder: "输入 {name} 的密码",
695
+ networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
696
+ },
697
+ "en-US": {
698
+ allFiles: "All files",
699
+ demoBadge: "Demo data",
700
+ filter: "Filter files",
701
+ refresh: "Refresh",
702
+ batch: "Download ZIP",
703
+ batchCount: "Download ZIP ({count})",
704
+ batching: "Zipping {percent}%",
705
+ passwordPrompt: "This folder is password protected:",
706
+ passwordPlaceholder: "Folder password",
707
+ open: "Open",
708
+ columnName: "Name",
709
+ columnSize: "Size",
710
+ columnDate: "Modified",
711
+ loading: "Loading…",
712
+ loadFailed: "Failed to load: {message}",
713
+ guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
714
+ corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
715
+ retry: "Retry",
716
+ empty: "No files",
717
+ preview: "Preview",
718
+ download: "Download",
719
+ loaded: "{count} loaded file(s)",
720
+ selected: "{count} selected",
721
+ unloaded: "{count} folder(s) not expanded, excluded from select-all",
722
+ folderFailed: "Failed to open folder: {message}",
723
+ demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
724
+ demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
725
+ demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
726
+ downloadFailed: "Download failed: {message}",
727
+ previewFailed: "Preview failed: {message}",
728
+ batchFailed: "Failed to create ZIP: {message}",
729
+ defaultZip: "resources",
730
+ clear: "Clear",
731
+ clearSelection: "Clear selection",
732
+ totalSize: "{size} total",
733
+ folderItems: "{count} items",
734
+ sortBy: "Sort",
735
+ selectAll: "Select all",
736
+ selectItem: "Select {name}",
737
+ expand: "Expand {name}",
738
+ collapse: "Collapse {name}",
739
+ close: "Close",
740
+ previous: "Previous",
741
+ next: "Next",
742
+ rotate: "Rotate",
743
+ actualSize: "Actual size",
744
+ previewUnsupported: "This file type cannot be previewed here. Download it to view.",
745
+ previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
746
+ previewTooLarge: "This file is large ({size}). Download it to view.",
747
+ textTruncated: "Showing the first {size} only. Download for the full file.",
748
+ refreshing: "Refreshing",
749
+ refreshFailed: "Refresh failed: {message}",
750
+ cancel: "Cancel",
751
+ passwordFor: "Password for {name}",
752
+ passwordWrong: "Wrong password or no permission",
753
+ passwordStale: "The saved password no longer works. Enter it again.",
754
+ rememberPassword: "Remember in this tab (cleared when the tab closes)",
755
+ remembered: "Remembered",
756
+ forgetPassword: "Forget",
757
+ unlockFolder: "Enter the password for {name}",
758
+ networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
759
+ }
760
+ };
761
+ function formatLabel(template, values = {}) {
762
+ return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
444
763
  }
445
- /**
446
- * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
447
- * (signed download URL), each optionally prefixed with `e` (URI-encode) or
448
- * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
449
- */
450
- function fillPreviewUrl(scheme, args) {
451
- return scheme.replace(/\$name/g, args.name).replace(/\$[eb_]*durl/g, (placeholder) => applyUrlOps(placeholder, args.downloadUrl)).replace(/\$[eb_]*url/g, (placeholder) => applyUrlOps(placeholder, args.rawUrl));
764
+ function resolveLabels(locale = "zh-CN", overrides) {
765
+ return {
766
+ ...fileListLocales[locale] ?? fileListLocales["zh-CN"],
767
+ ...overrides
768
+ };
452
769
  }
453
770
  // src/client/PreviewDialog.vue
454
771
  const _sfc_main$1 = /*@__PURE__*/ defineComponent({
@@ -482,11 +799,12 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
482
799
  ],
483
800
  setup(__props, { emit: __emit }) {
484
801
  /**
485
- * PreviewDialog.vue —— 文件预览弹窗(原生 <dialog>,不跳转页面)
802
+ * File preview in a native <dialog>, without leaving the page.
486
803
  *
487
- * image / video / audio:原生标签;text:Range 读取前 maxTextBytes;
488
- * pdf:OpenList `iframe_previews` 中的查看器,未配置时用浏览器自带阅读器(Blob);
489
- * 其它类型(含 Office)、失败、过大、演示模式:文件信息 + 下载。
804
+ * image / video / audio: native elements; text: the first `maxTextBytes`
805
+ * through a Range request; pdf: the first matching OpenList `iframe_previews`
806
+ * viewer, else the browser's reader through a Blob. Anything else (Office
807
+ * included), failures, oversized PDFs and demo mode: file info + download.
490
808
  */
491
809
  const props = __props;
492
810
  function t(key, values) {
@@ -636,20 +954,49 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
636
954
  _push(`<div class="fl-preview-panel"><header class="fl-preview-head"><span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><div class="fl-preview-title"><span class="fl-preview-name">${ssrInterpolate(__props.file.name)}</span><span class="fl-preview-meta">${ssrInterpolate(unref(formatSize)(__props.file.size))} · ${ssrInterpolate(unref(formatDate)(__props.file.modified))}</span></div>`);
637
955
  if (__props.total > 1 || mode.value === "image" && !busy.value) {
638
956
  _push(`<div class="fl-preview-tools fl-preview-nav">`);
639
- if (__props.total > 1) _push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 6-6 6 6 6"></path></svg></button><span class="fl-preview-count">${ssrInterpolate(__props.position)} / ${ssrInterpolate(__props.total)}</span><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("next"))}${ssrRenderAttr("title", t("next"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button><!--]-->`);
640
- else _push(`<!---->`);
957
+ if (__props.total > 1) {
958
+ _push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}>`);
959
+ _push(ssrRenderComponent(_sfc_main$2, {
960
+ name: "previous",
961
+ size: 18
962
+ }, null, _parent));
963
+ _push(`</button><span class="fl-preview-count">${ssrInterpolate(__props.position)} / ${ssrInterpolate(__props.total)}</span><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("next"))}${ssrRenderAttr("title", t("next"))}>`);
964
+ _push(ssrRenderComponent(_sfc_main$2, {
965
+ name: "next",
966
+ size: 18
967
+ }, null, _parent));
968
+ _push(`</button><!--]-->`);
969
+ } else _push(`<!---->`);
641
970
  if (mode.value === "image" && !busy.value) {
642
- _push(`<!--[--><button class="fl-preview-tool fl-preview-rotate" type="button"${ssrRenderAttr("aria-label", t("rotate"))}${ssrRenderAttr("title", t("rotate"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button><button class="fl-preview-tool fl-preview-zoom" type="button"${ssrRenderAttr("aria-pressed", actualSize.value)}${ssrRenderAttr("aria-label", t("actualSize"))}${ssrRenderAttr("title", t("actualSize"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path>`);
643
- if (!actualSize.value) _push(`<path d="M11 8v6M8 11h6"></path>`);
644
- else _push(`<path d="M8 11h6"></path>`);
645
- _push(`</svg></button><!--]-->`);
971
+ _push(`<!--[--><button class="fl-preview-tool fl-preview-rotate" type="button"${ssrRenderAttr("aria-label", t("rotate"))}${ssrRenderAttr("title", t("rotate"))}>`);
972
+ _push(ssrRenderComponent(_sfc_main$2, {
973
+ name: "refresh",
974
+ size: 17
975
+ }, null, _parent));
976
+ _push(`</button><button class="fl-preview-tool fl-preview-zoom" type="button"${ssrRenderAttr("aria-pressed", actualSize.value)}${ssrRenderAttr("aria-label", t("actualSize"))}${ssrRenderAttr("title", t("actualSize"))}>`);
977
+ _push(ssrRenderComponent(_sfc_main$2, {
978
+ name: actualSize.value ? "zoomOut" : "zoomIn",
979
+ size: 17
980
+ }, null, _parent));
981
+ _push(`</button><!--]-->`);
646
982
  } else _push(`<!---->`);
647
983
  _push(`</div>`);
648
984
  } else _push(`<!---->`);
649
985
  _push(`<div class="fl-preview-tools fl-preview-actions">`);
650
- if (!__props.demo) _push(`<button class="fl-preview-tool fl-preview-download" type="button"${ssrRenderAttr("aria-label", t("download"))}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button>`);
651
- else _push(`<!---->`);
652
- _push(`<button class="fl-preview-tool fl-preview-close" type="button" autofocus${ssrRenderAttr("aria-label", t("close"))}${ssrRenderAttr("title", t("close"))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg></button></div></header><div class="fl-preview-body"${ssrRenderAttr("data-mode", busy.value ? "loading" : mode.value)}>`);
986
+ if (!__props.demo) {
987
+ _push(`<button class="fl-preview-tool fl-preview-download" type="button"${ssrRenderAttr("aria-label", t("download"))}${ssrRenderAttr("title", t("download"))}>`);
988
+ _push(ssrRenderComponent(_sfc_main$2, {
989
+ name: "download",
990
+ size: 17
991
+ }, null, _parent));
992
+ _push(`</button>`);
993
+ } else _push(`<!---->`);
994
+ _push(`<button class="fl-preview-tool fl-preview-close" type="button" autofocus${ssrRenderAttr("aria-label", t("close"))}${ssrRenderAttr("title", t("close"))}>`);
995
+ _push(ssrRenderComponent(_sfc_main$2, {
996
+ name: "close",
997
+ size: 18
998
+ }, null, _parent));
999
+ _push(`</button></div></header><div class="fl-preview-body"${ssrRenderAttr("data-mode", busy.value ? "loading" : mode.value)}>`);
653
1000
  if (busy.value) _push(`<div class="fl-preview-status" role="status">${ssrInterpolate(t("loading"))}</div>`);
654
1001
  else if (mode.value === "image") _push(`<img class="${ssrRenderClass([{ "fl-preview-actual": actualSize.value }, "fl-preview-image"])}"${ssrRenderAttr("src", __props.src)}${ssrRenderAttr("alt", __props.file.name)} style="${ssrRenderStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 })}">`);
655
1002
  else if (mode.value === "video") _push(`<video class="fl-preview-video"${ssrRenderAttr("src", __props.src)} controls playsinline preload="metadata"></video>`);
@@ -662,8 +1009,14 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
662
1009
  } else if (mode.value === "pdf") _push(`<iframe class="fl-preview-frame"${ssrRenderAttr("src", __props.viewer || blobUrl.value)}${ssrRenderAttr("title", __props.file.name)}></iframe>`);
663
1010
  else {
664
1011
  _push(`<div class="fl-preview-info"><span class="fl-type fl-preview-badge"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><p class="fl-preview-message">${ssrInterpolate(infoMessage.value)}</p>`);
665
- if (!__props.demo) _push(`<button class="fl-btn fl-btn-primary" type="button"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(t("download"))}</button>`);
666
- else _push(`<!---->`);
1012
+ if (!__props.demo) {
1013
+ _push(`<button class="fl-btn fl-btn-primary" type="button">`);
1014
+ _push(ssrRenderComponent(_sfc_main$2, {
1015
+ name: "download",
1016
+ size: 15
1017
+ }, null, _parent));
1018
+ _push(` ${ssrInterpolate(t("download"))}</button>`);
1019
+ } else _push(`<!---->`);
667
1020
  _push(`</div>`);
668
1021
  }
669
1022
  _push(`</div></div>`);
@@ -678,6 +1031,67 @@ _sfc_main$1.setup = (props, ctx) => {
678
1031
  (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/PreviewDialog.vue");
679
1032
  return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
680
1033
  };
1034
+ // src/client/tree.ts
1035
+ function newLock(prompting = false, error = "") {
1036
+ return {
1037
+ prompting,
1038
+ input: "",
1039
+ error,
1040
+ remember: false,
1041
+ busy: false
1042
+ };
1043
+ }
1044
+ function toNodes(items, parent, password) {
1045
+ return items.map((item) => ({
1046
+ ...item,
1047
+ path: joinPath(parent.path, item.name),
1048
+ depth: parent.depth + 1,
1049
+ expanded: false,
1050
+ loaded: false,
1051
+ loading: false,
1052
+ children: [],
1053
+ password,
1054
+ zipPath: [...parent.zipPath, item.name]
1055
+ }));
1056
+ }
1057
+ function sortTree(nodes, key, ascending) {
1058
+ nodes.sort((a, b) => compareNodes(a, b, key, ascending));
1059
+ for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
1060
+ }
1061
+ /** Loaded files at or under a node. */
1062
+ function descendantFiles(node) {
1063
+ return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
1064
+ }
1065
+ /** Last path segment, for labels. */
1066
+ function sourceName(path) {
1067
+ return path.split("/").filter(Boolean).pop() || path;
1068
+ }
1069
+ /** Loaded file count, size and selected count for every folder, in one pass. */
1070
+ function folderStats(roots, selected) {
1071
+ const stats = /* @__PURE__ */ new Map();
1072
+ const walk = (node) => {
1073
+ if (!node.is_dir) return {
1074
+ files: 1,
1075
+ bytes: totalSize([node]),
1076
+ selected: selected.has(node.path) ? 1 : 0
1077
+ };
1078
+ const total = {
1079
+ files: 0,
1080
+ bytes: 0,
1081
+ selected: 0
1082
+ };
1083
+ for (const child of node.children) {
1084
+ const part = walk(child);
1085
+ total.files += part.files;
1086
+ total.bytes += part.bytes;
1087
+ total.selected += part.selected;
1088
+ }
1089
+ stats.set(node.path, total);
1090
+ return total;
1091
+ };
1092
+ roots.forEach(walk);
1093
+ return stats;
1094
+ }
681
1095
  // src/client/passwords.ts
682
1096
  /**
683
1097
  * Folder passwords remembered for the current browser tab.
@@ -715,12 +1129,6 @@ function write(base, map) {
715
1129
  function normalise(path) {
716
1130
  return path.replace(/\/+$/, "") || "/";
717
1131
  }
718
- /** Whether `path` is `folder` itself or inside it. */
719
- function isWithin(path, folder) {
720
- const p = normalise(path);
721
- const f = normalise(folder);
722
- return f === "/" || p === f || p.startsWith(`${f}/`);
723
- }
724
1132
  /** The closest remembered folder at or above `path`, if any. */
725
1133
  function findRememberedPassword(base, path) {
726
1134
  let best = null;
@@ -733,6 +1141,12 @@ function findRememberedPassword(base, path) {
733
1141
  }
734
1142
  return best;
735
1143
  }
1144
+ function rememberFolderPassword(base, path, password) {
1145
+ write(base, {
1146
+ ...read(base),
1147
+ [normalise(path)]: password
1148
+ });
1149
+ }
736
1150
  function forgetFolderPassword(base, path) {
737
1151
  const map = read(base);
738
1152
  delete map[normalise(path)];
@@ -742,6 +1156,73 @@ function forgetFolderPassword(base, path) {
742
1156
  function rememberedFolders(base) {
743
1157
  return Object.keys(read(base));
744
1158
  }
1159
+ // src/client/usePasswords.ts
1160
+ /**
1161
+ * The one place that decides which folder password to send.
1162
+ *
1163
+ * Order: typed on this page for this exact folder → the item's own password
1164
+ * (site-configured, or inherited from the folder it was listed with) → typed
1165
+ * for an enclosing folder → remembered in this tab → the component default.
1166
+ */
1167
+ function usePasswords(options) {
1168
+ /** Typed on this page; not necessarily remembered. Survives refreshes. */
1169
+ const typed = /* @__PURE__ */ new Map();
1170
+ /** Remembered folders, mirrored from sessionStorage so templates react. */
1171
+ const remembered = ref(/* @__PURE__ */ new Set());
1172
+ function sync() {
1173
+ remembered.value = new Set(options.enabled() ? rememberedFolders(options.scope()) : []);
1174
+ }
1175
+ function typedAbove(path) {
1176
+ let best = "";
1177
+ let bestFolder = "";
1178
+ for (const [folder, password] of typed) if (isWithin(path, folder) && folder.length > bestFolder.length) {
1179
+ best = password;
1180
+ bestFolder = folder;
1181
+ }
1182
+ return best;
1183
+ }
1184
+ function resolve(path, own = "") {
1185
+ const exact = typed.get(path);
1186
+ if (exact) return { password: exact };
1187
+ if (own) return { password: own };
1188
+ const above = typedAbove(path);
1189
+ if (above) return { password: above };
1190
+ const stored = options.enabled() ? findRememberedPassword(options.scope(), path) : null;
1191
+ if (stored) return {
1192
+ password: stored.password,
1193
+ rememberedAt: stored.path
1194
+ };
1195
+ return { password: options.fallback() };
1196
+ }
1197
+ /** A password worked: keep it for this page, and in this tab when asked. */
1198
+ function accept(path, password, remember) {
1199
+ typed.set(path, password);
1200
+ if (remember && options.enabled()) {
1201
+ rememberFolderPassword(options.scope(), path, password);
1202
+ sync();
1203
+ }
1204
+ }
1205
+ function forget(path) {
1206
+ typed.delete(path);
1207
+ forgetFolderPassword(options.scope(), path);
1208
+ sync();
1209
+ }
1210
+ /** A remembered password stopped working: drop it. Returns whether one was dropped. */
1211
+ function dropStale(rememberedAt) {
1212
+ if (!rememberedAt) return false;
1213
+ forgetFolderPassword(options.scope(), rememberedAt);
1214
+ sync();
1215
+ return true;
1216
+ }
1217
+ return {
1218
+ remembered,
1219
+ sync,
1220
+ resolve,
1221
+ accept,
1222
+ forget,
1223
+ dropStale
1224
+ };
1225
+ }
745
1226
  // src/client/FileList.vue
746
1227
  const _sfc_main = /*@__PURE__*/ defineComponent({
747
1228
  __name: "FileList",
@@ -766,19 +1247,27 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
766
1247
  },
767
1248
  setup(__props) {
768
1249
  /**
769
- * FileList.vue —— VitePress 网盘文件列表(界面沿用 FileList,数据来自 OpenList)
770
- *
771
- * 用法:
772
- * <FileList base="https://pan.example.com" path="/课程资料" />
773
- * <FileList /> // 演示模式
1250
+ * OpenList file browser: folder tree, batch ZIP, folder passwords and an
1251
+ * in-page preview. OpenList access is in api.ts (demo.ts when `base` is empty),
1252
+ * password choice in usePasswords.ts, tree helpers in tree.ts and downloads in
1253
+ * download.ts; this component wires them to the UI.
774
1254
  */
775
1255
  const props = __props;
776
1256
  const text = computed(() => resolveLabels(props.locale, props.labels));
777
1257
  function t(key, values) {
778
1258
  return formatLabel(text.value[key], values);
779
1259
  }
1260
+ /** Prefix for element ids, unique per component instance. */
1261
+ const uid = useId();
780
1262
  const demo = computed(() => !props.base);
781
- const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
1263
+ const api = computed(() => demo.value ? createDemoApi() : createOpenListApi(props.base, props.token));
1264
+ const canRemember = computed(() => props.rememberPassword !== false);
1265
+ const passwords = usePasswords({
1266
+ scope: () => props.base.replace(/\/+$/, "") || "demo",
1267
+ enabled: () => canRemember.value,
1268
+ fallback: () => props.password
1269
+ });
1270
+ const remembered = passwords.remembered;
782
1271
  const rootEl = ref();
783
1272
  const roots = ref([]);
784
1273
  const loadingRoot = ref(false);
@@ -786,110 +1275,45 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
786
1275
  const refreshing = ref(false);
787
1276
  let refreshSeq = 0;
788
1277
  /** Single-path mode: the listed folder itself needs a password. */
789
- const rootLock = reactive({
790
- active: false,
791
- input: "",
792
- error: "",
793
- remember: false,
794
- busy: false
795
- });
796
- /** Password the visitor typed for the listed folder (single-path mode). */
797
- const rootPassword = ref("");
1278
+ const rootLock = ref(null);
798
1279
  const keyword = ref("");
799
1280
  const selected = ref(/* @__PURE__ */ new Set());
800
1281
  const toast = ref("");
801
1282
  const sortKey = ref("name");
802
1283
  const sortAsc = ref(true);
803
1284
  const siteTitle = ref("");
804
- /** OpenList's public `iframe_previews` setting (JSON text). */
805
1285
  const iframePreviews = ref("");
806
1286
  const batchBusy = ref(false);
807
1287
  const batchProgress = ref(0);
808
- const downloadCache = /* @__PURE__ */ new Map();
809
1288
  let toastTimer;
810
- function showToast(msg) {
811
- toast.value = msg;
1289
+ /** The folder listed in single-path mode, as the parent of the top rows. */
1290
+ const listedFolder = computed(() => ({
1291
+ path: props.path,
1292
+ depth: -1,
1293
+ zipPath: []
1294
+ }));
1295
+ function showToast(message) {
1296
+ toast.value = message;
812
1297
  clearTimeout(toastTimer);
813
1298
  toastTimer = setTimeout(() => toast.value = "", 3200);
814
1299
  }
815
- function errorText(error) {
816
- return error instanceof Error ? error.message : String(error);
817
- }
818
- function isPasswordError(error) {
819
- return /password|密码/i.test(errorText(error));
820
- }
821
- function isNetworkError(error) {
822
- return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
823
- }
824
1300
  /** A failure toast; browser network errors are usually CORS, so say where to look. */
825
1301
  function failureMessage(key, error) {
826
1302
  const message = t(key, { message: errorText(error) });
827
- return isNetworkError(error) ? `${message} ${t("networkHint")}` : message;
828
- }
829
- /** Total size for display: an empty total reads `0 B`, not `-`. */
830
- function formatTotal(bytes) {
831
- return bytes > 0 ? formatSize(bytes) : "0 B";
832
- }
833
- const canRemember = computed(() => props.rememberPassword !== false);
834
- const passwordScope = computed(() => baseUrl.value || "demo");
835
- /** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
836
- const typedPasswords = /* @__PURE__ */ new Map();
837
- function typedPasswordFor(path) {
838
- let best = "";
839
- let bestPath = "";
840
- for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
841
- best = password;
842
- bestPath = folder;
843
- }
844
- return best;
845
- }
846
- /** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
847
- const rememberedPaths = ref(/* @__PURE__ */ new Set());
848
- function syncRemembered() {
849
- rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
850
- }
851
- function storedPassword(path) {
852
- return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
853
- }
854
- /**
855
- * Password for an item: its own or inherited one first (site-configured or
856
- * typed), then one remembered in this tab, then the component's `password`.
857
- */
858
- function passwordFor(node) {
859
- if (node.password) return { password: node.password };
860
- const typed = typedPasswordFor(node.path);
861
- if (typed) return { password: typed };
862
- const stored = storedPassword(node.path);
863
- if (stored) return {
864
- password: stored.password,
865
- rememberedAt: stored.path
866
- };
867
- return { password: props.password };
1303
+ return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
868
1304
  }
869
- function rootPasswordFor() {
870
- if (rootPassword.value) return { password: rootPassword.value };
871
- const stored = storedPassword(props.path);
872
- if (stored) return {
873
- password: stored.password,
874
- rememberedAt: stored.path
875
- };
876
- return { password: props.password };
877
- }
878
- /** A remembered password stopped working: drop it so we do not keep retrying it. */
879
- function dropStale(path) {
880
- if (!path) return false;
881
- forgetFolderPassword(passwordScope.value, path);
882
- syncRemembered();
883
- return true;
1305
+ function sortNodes(nodes) {
1306
+ sortTree(nodes, sortKey.value, sortAsc.value);
884
1307
  }
885
1308
  function focusUnlock(path) {
886
1309
  nextTick(() => {
887
- (rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
1310
+ rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
888
1311
  });
889
1312
  }
890
- /** Show the same-row password input; `focus` only for user actions, never on page load. */
1313
+ /** Show a folder's same-row input; focus only on user actions, never on page load. */
891
1314
  function startPrompt(node, focus) {
892
- node.prompting = true;
1315
+ node.lock ??= newLock();
1316
+ node.lock.prompting = true;
893
1317
  if (focus) focusUnlock(node.path);
894
1318
  }
895
1319
  function unlockErrorText(error) {
@@ -897,289 +1321,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
897
1321
  if (error === "stale") return t("passwordStale");
898
1322
  return "";
899
1323
  }
900
- const DEMO_TREE = {
901
- "/": [
902
- {
903
- name: "assignments",
904
- size: 0,
905
- is_dir: true,
906
- modified: "2026-09-01T08:00:00+08:00"
907
- },
908
- {
909
- name: "exams",
910
- size: 0,
911
- is_dir: true,
912
- modified: "2026-09-01T08:00:00+08:00"
913
- },
914
- {
915
- name: "materials",
916
- size: 0,
917
- is_dir: true,
918
- modified: "2026-09-01T08:00:00+08:00"
919
- },
920
- {
921
- name: "notes",
922
- size: 0,
923
- is_dir: true,
924
- modified: "2026-09-01T08:00:00+08:00"
925
- },
926
- {
927
- name: "slides",
928
- size: 0,
929
- is_dir: true,
930
- modified: "2026-09-01T08:00:00+08:00"
931
- },
932
- {
933
- name: "加密示例(密码 1234)",
934
- size: 0,
935
- is_dir: true,
936
- modified: "2026-10-20T18:00:00+08:00"
937
- }
938
- ],
939
- "/assignments": [{
940
- name: "线代作业参考答案",
941
- size: 0,
942
- is_dir: true,
943
- modified: "2026-10-12T21:30:00+08:00"
944
- }, {
945
- name: "作业提交说明.pdf",
946
- size: 186432,
947
- is_dir: false,
948
- modified: "2026-09-03T10:15:00+08:00"
949
- }],
950
- "/assignments/线代作业参考答案": [
951
- {
952
- name: "第1次作业参考答案.doc",
953
- size: 412672,
954
- is_dir: false,
955
- modified: "2026-09-15T22:41:00+08:00"
956
- },
957
- {
958
- name: "第2次作业参考答案.pdf",
959
- size: 1048576,
960
- is_dir: false,
961
- modified: "2026-09-22T22:03:00+08:00"
962
- },
963
- {
964
- name: "第3次作业参考答案.pdf",
965
- size: 986112,
966
- is_dir: false,
967
- modified: "2026-09-29T21:57:00+08:00"
968
- },
969
- {
970
- name: "第4次作业参考答案.pdf",
971
- size: 1572864,
972
- is_dir: false,
973
- modified: "2026-10-13T22:10:00+08:00"
974
- }
975
- ],
976
- "/exams": [
977
- {
978
- name: "2023 期末试题和答案.pdf",
979
- size: 2345678,
980
- is_dir: false,
981
- modified: "2025-01-18T09:20:00+08:00"
982
- },
983
- {
984
- name: "2024 期中试题.pdf",
985
- size: 1234567,
986
- is_dir: false,
987
- modified: "2025-11-02T14:08:00+08:00"
988
- },
989
- {
990
- name: "代数与几何-多年合集-试题和答案.pdf",
991
- size: 18874368,
992
- is_dir: false,
993
- modified: "2025-01-18T09:25:00+08:00"
994
- }
995
- ],
996
- "/materials": [
997
- {
998
- name: "线性代数及其应用(Lay 第5版).pdf",
999
- size: 48231424,
1000
- is_dir: false,
1001
- modified: "2024-07-29T16:44:00+08:00"
1002
- },
1003
- {
1004
- name: "Introduction to Linear Algebra(Strang).pdf",
1005
- size: 36700160,
1006
- is_dir: false,
1007
- modified: "2024-07-29T16:50:00+08:00"
1008
- },
1009
- {
1010
- name: "疑难解答与习题指导.zip",
1011
- size: 9437184,
1012
- is_dir: false,
1013
- modified: "2024-08-05T11:12:00+08:00"
1014
- }
1015
- ],
1016
- "/notes": [{
1017
- name: "线性代数笔记(106页).pdf",
1018
- size: 12582912,
1019
- is_dir: false,
1020
- modified: "2025-06-30T20:31:00+08:00"
1021
- }, {
1022
- name: "期末复习提纲.docx",
1023
- size: 245760,
1024
- is_dir: false,
1025
- modified: "2026-01-05T19:02:00+08:00"
1026
- }],
1027
- "/加密示例(密码 1234)": [{
1028
- name: "期末答案(教师版).pdf",
1029
- size: 2621440,
1030
- is_dir: false,
1031
- modified: "2026-10-20T18:00:00+08:00"
1032
- }, {
1033
- name: "成绩登记表.xlsx",
1034
- size: 48128,
1035
- is_dir: false,
1036
- modified: "2026-10-21T09:12:00+08:00"
1037
- }],
1038
- "/slides": [{
1039
- name: "第一章 行列式.pptx",
1040
- size: 3145728,
1041
- is_dir: false,
1042
- modified: "2026-09-08T13:26:00+08:00"
1043
- }, {
1044
- name: "第二章 矩阵.pptx",
1045
- size: 4194304,
1046
- is_dir: false,
1047
- modified: "2026-09-15T13:30:00+08:00"
1048
- }]
1049
- };
1050
- /** Demo-only folder passwords, so the demo can show the password flow. */
1051
- const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
1052
- class ApiError extends Error {
1053
- code;
1054
- constructor(message, code) {
1055
- super(message);
1056
- this.code = code;
1057
- }
1058
- }
1059
- /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1060
- function authHeaders(json) {
1061
- return {
1062
- ...json ? { "Content-Type": "application/json" } : {},
1063
- ...props.token ? { Authorization: props.token } : {}
1064
- };
1065
- }
1066
- async function request(apiPath, body) {
1067
- const response = await fetch(`${baseUrl.value}${apiPath}`, {
1068
- method: body === void 0 ? "GET" : "POST",
1069
- headers: authHeaders(body !== void 0),
1070
- body: body === void 0 ? void 0 : JSON.stringify(body)
1071
- });
1072
- const payload = await response.json().catch(() => null);
1073
- if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
1074
- if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
1075
- return payload.data;
1076
- }
1077
- async function listDir(path, password) {
1078
- if (demo.value) {
1079
- await new Promise((resolve) => setTimeout(resolve, 220));
1080
- const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
1081
- if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
1082
- return DEMO_TREE[path] ?? [];
1083
- }
1084
- return (await request("/api/fs/list", {
1085
- path,
1086
- password,
1087
- page: 1,
1088
- per_page: 0,
1089
- refresh: false
1090
- }))?.content ?? [];
1091
- }
1092
- function absoluteUrl(raw) {
1093
- if (!raw) return "";
1094
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
1095
- const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
1096
- try {
1097
- return new URL(raw, `${root}/`).href;
1098
- } catch {
1099
- return raw;
1100
- }
1101
- }
1102
- async function resolveUrl(node) {
1103
- if (demo.value) return "";
1104
- if (node.raw_url) return absoluteUrl(node.raw_url);
1105
- const cached = downloadCache.get(node.path);
1106
- if (cached) return cached;
1107
- let url = "";
1108
- try {
1109
- url = absoluteUrl((await request("/api/fs/get", {
1110
- path: node.path,
1111
- password: passwordFor(node).password
1112
- }))?.raw_url ?? "");
1113
- } catch {
1114
- url = "";
1115
- }
1116
- if (!url) url = directUrl(node);
1117
- downloadCache.set(node.path, url);
1118
- return url;
1119
- }
1120
- /** OpenList's signed `/d/` download link for a file. */
1121
- function directUrl(node) {
1122
- return `${baseUrl.value}/d${encodePath(node.path)}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
1123
- }
1124
- async function loadSiteTitle() {
1125
- if (demo.value) return;
1126
- try {
1127
- const data = await request("/api/public/settings");
1128
- siteTitle.value = data?.site_title || data?.title || "";
1129
- iframePreviews.value = data?.iframe_previews || "";
1130
- } catch {
1131
- siteTitle.value = "";
1132
- iframePreviews.value = "";
1133
- }
1134
- }
1135
- function sortTree(nodes) {
1136
- nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
1137
- for (const node of nodes) if (node.children.length) sortTree(node.children);
1324
+ async function fetchChildren(node, password) {
1325
+ node.children = toNodes(await api.value.list(node.path, password), node, password);
1326
+ sortNodes(node.children);
1327
+ node.loaded = true;
1138
1328
  }
1139
- function toNodes(items, parent, depth, password = "", zipParent = []) {
1140
- return items.map((item) => ({
1141
- ...item,
1142
- path: joinPath(parent, item.name),
1143
- depth,
1144
- expanded: false,
1145
- loaded: false,
1146
- loading: false,
1147
- children: [],
1148
- password,
1149
- zipPath: [...zipParent, item.name]
1150
- }));
1151
- }
1152
- function resortAll() {
1153
- if (props.sources.length) {
1154
- for (const root of roots.value) if (root.children.length) sortTree(root.children);
1155
- } else sortTree(roots.value);
1156
- }
1157
- function sourceName(path) {
1158
- const parts = path.split("/").filter(Boolean);
1159
- return parts[parts.length - 1] || path;
1329
+ async function listRoot(password) {
1330
+ const nodes = toNodes(await api.value.list(props.path, password), listedFolder.value, password);
1331
+ sortNodes(nodes);
1332
+ return nodes;
1160
1333
  }
1161
1334
  /**
1162
- * Load a folder's children. A password error marks the folder locked instead of
1335
+ * Load a folder's children. A password error locks the folder instead of
1163
1336
  * toasting; `prompt` also shows its same-row input (and focuses it).
1164
1337
  */
1165
1338
  async function loadChildren(node, prompt = false) {
1166
1339
  if (node.loaded) return true;
1167
- if (node.locked) {
1340
+ if (node.lock) {
1168
1341
  if (prompt) startPrompt(node, prompt === "focus");
1169
1342
  return false;
1170
1343
  }
1171
1344
  node.loading = true;
1172
- const { password, rememberedAt } = passwordFor(node);
1345
+ const { password, rememberedAt } = passwords.resolve(node.path, node.password);
1173
1346
  try {
1174
- node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
1175
- sortTree(node.children);
1176
- node.loaded = true;
1347
+ await fetchChildren(node, password);
1177
1348
  return true;
1178
1349
  } catch (error) {
1179
1350
  if (isPasswordError(error)) {
1180
- node.locked = true;
1181
1351
  node.expanded = false;
1182
- node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1352
+ node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
1183
1353
  if (prompt) startPrompt(node, prompt === "focus");
1184
1354
  } else showToast(failureMessage("folderFailed", error));
1185
1355
  return false;
@@ -1194,44 +1364,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1194
1364
  }
1195
1365
  async function loadSourceRoot(source, expanded) {
1196
1366
  const path = source.path;
1197
- const configured = source.password ?? "";
1367
+ const own = source.password ?? "";
1198
1368
  let item = {
1199
1369
  name: sourceName(path),
1200
1370
  size: 0,
1201
1371
  is_dir: !looksLikeFile(path),
1202
1372
  modified: ""
1203
1373
  };
1204
- let locked = false;
1205
- let unlockError = "";
1206
- if (!demo.value) {
1207
- const { password, rememberedAt } = configured ? {
1208
- password: configured,
1209
- rememberedAt: void 0
1210
- } : (() => {
1211
- const stored = storedPassword(path);
1212
- return stored ? {
1213
- password: stored.password,
1214
- rememberedAt: stored.path
1215
- } : {
1216
- password: props.password,
1217
- rememberedAt: void 0
1218
- };
1219
- })();
1220
- try {
1221
- const data = await request("/api/fs/get", {
1222
- path,
1223
- password
1224
- });
1225
- item = {
1226
- ...item,
1227
- ...data
1228
- };
1229
- } catch (error) {
1230
- if (isPasswordError(error)) {
1231
- locked = true;
1232
- unlockError = dropStale(rememberedAt) ? "stale" : "";
1233
- }
1234
- }
1374
+ let lock;
1375
+ const { password, rememberedAt } = passwords.resolve(path, own);
1376
+ try {
1377
+ item = {
1378
+ ...item,
1379
+ ...await api.value.get(path, password)
1380
+ };
1381
+ } catch (error) {
1382
+ if (isPasswordError(error)) lock = newLock(expanded, passwords.dropStale(rememberedAt) ? "stale" : "");
1235
1383
  }
1236
1384
  const name = source.title || item.name || sourceName(path);
1237
1385
  return {
@@ -1239,17 +1387,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1239
1387
  name,
1240
1388
  path,
1241
1389
  depth: 0,
1242
- expanded,
1390
+ expanded: expanded && !lock,
1243
1391
  loaded: false,
1244
1392
  loading: false,
1245
1393
  children: [],
1246
- password: configured,
1247
- configuredPassword: configured,
1394
+ password: own,
1248
1395
  isRoot: true,
1249
- locked,
1250
- unresolved: locked,
1251
- unlockError,
1252
- prompting: locked && expanded,
1396
+ lock,
1397
+ unresolved: !!lock,
1253
1398
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
1254
1399
  };
1255
1400
  }
@@ -1268,20 +1413,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1268
1413
  let nodes;
1269
1414
  if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1270
1415
  else {
1271
- const { password, rememberedAt } = rootPasswordFor();
1272
- let items;
1416
+ const { password, rememberedAt } = passwords.resolve(props.path);
1273
1417
  try {
1274
- items = await listDir(props.path, password);
1418
+ nodes = await listRoot(password);
1275
1419
  } catch (error) {
1276
- if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1277
- throw error;
1420
+ throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
1278
1421
  }
1279
- nodes = toNodes(items, props.path, 0, rootPassword.value);
1280
- sortTree(nodes);
1281
1422
  }
1282
1423
  const open = async (list) => {
1283
1424
  for (const node of list) {
1284
- if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
1425
+ const wanted = node.expanded || expandedPaths?.has(node.path);
1426
+ if (!node.is_dir || node.lock || !wanted) continue;
1285
1427
  node.expanded = await loadChildren(node, "show");
1286
1428
  if (node.expanded && expandedPaths) await open(node.children);
1287
1429
  }
@@ -1289,6 +1431,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1289
1431
  await open(nodes);
1290
1432
  return nodes;
1291
1433
  }
1434
+ function lockRoot(error, fallback) {
1435
+ roots.value = [];
1436
+ selected.value = /* @__PURE__ */ new Set();
1437
+ rootLock.value = newLock(true, passwords.dropStale(error.rememberedAt) ? "stale" : fallback);
1438
+ }
1292
1439
  /** First load, or a retry after an error: shows the loading state. */
1293
1440
  async function loadRoot() {
1294
1441
  refreshSeq += 1;
@@ -1297,54 +1444,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1297
1444
  errorMsg.value = "";
1298
1445
  try {
1299
1446
  roots.value = await buildTree();
1300
- rootLock.active = false;
1447
+ rootLock.value = null;
1301
1448
  selected.value = /* @__PURE__ */ new Set();
1302
- downloadCache.clear();
1449
+ api.value.clearCache();
1303
1450
  } catch (error) {
1304
- if (error instanceof RootLockedError) {
1305
- roots.value = [];
1306
- rootLock.active = true;
1307
- rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
1308
- } else errorMsg.value = errorText(error);
1451
+ if (error instanceof RootLockedError) lockRoot(error, "");
1452
+ else errorMsg.value = errorText(error);
1309
1453
  } finally {
1310
1454
  loadingRoot.value = false;
1311
1455
  }
1312
1456
  }
1313
- const visible = computed(() => {
1314
- const out = [];
1315
- const query = keyword.value.trim().toLowerCase();
1316
- const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
1317
- const walk = (nodes) => {
1318
- for (const node of nodes) {
1319
- if (!matches(node)) continue;
1320
- out.push(node);
1321
- if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children);
1322
- }
1323
- };
1324
- walk(roots.value);
1325
- return out;
1326
- });
1327
- function descendantFiles(node) {
1328
- if (!node.is_dir) return [node];
1329
- return node.children.flatMap(descendantFiles);
1330
- }
1331
- function isChecked(node) {
1332
- const files = descendantFiles(node);
1333
- return files.length > 0 && files.every((file) => selected.value.has(file.path));
1334
- }
1335
- function isIndeterminate(node) {
1336
- const files = descendantFiles(node);
1337
- const hit = files.filter((file) => selected.value.has(file.path)).length;
1338
- return hit > 0 && hit < files.length;
1339
- }
1340
- const allFiles = computed(() => roots.value.flatMap(descendantFiles));
1341
- const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
1342
- const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
1343
- const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
1344
- const selectedBytes = computed(() => totalSize(selectedFiles.value));
1345
- const loadedBytes = computed(() => totalSize(allFiles.value));
1346
- function folderFiles(node) {
1347
- return node.is_dir && node.loaded ? descendantFiles(node) : [];
1457
+ function applySort(key, ascending) {
1458
+ sortKey.value = key;
1459
+ sortAsc.value = ascending;
1460
+ if (props.sources.length) roots.value.forEach((root) => sortNodes(root.children));
1461
+ else sortNodes(roots.value);
1348
1462
  }
1349
1463
  /** Compact sort control for narrow containers, where header cells are hidden. */
1350
1464
  const sortOptions = computed(() => [
@@ -1365,19 +1479,48 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1365
1479
  get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
1366
1480
  set: (value) => {
1367
1481
  const [key, direction] = value.split(":");
1368
- sortKey.value = key;
1369
- sortAsc.value = direction !== "desc";
1370
- resortAll();
1482
+ applySort(key, direction !== "desc");
1371
1483
  }
1372
1484
  });
1373
- function triggerUrl(url) {
1374
- const anchor = document.createElement("a");
1375
- anchor.href = url;
1376
- anchor.target = "_blank";
1377
- anchor.rel = "noopener";
1378
- document.body.appendChild(anchor);
1379
- anchor.click();
1380
- anchor.remove();
1485
+ const visible = computed(() => {
1486
+ const out = [];
1487
+ const query = keyword.value.trim().toLowerCase();
1488
+ const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
1489
+ const walk = (nodes) => {
1490
+ for (const node of nodes) {
1491
+ if (!matches(node)) continue;
1492
+ out.push(node);
1493
+ if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children);
1494
+ }
1495
+ };
1496
+ walk(roots.value);
1497
+ return out;
1498
+ });
1499
+ const stats = computed(() => folderStats(roots.value, selected.value));
1500
+ const noStats = {
1501
+ files: 0,
1502
+ bytes: 0,
1503
+ selected: 0
1504
+ };
1505
+ function statsOf(node) {
1506
+ return stats.value.get(node.path) ?? noStats;
1507
+ }
1508
+ function isChecked(node) {
1509
+ if (!node.is_dir) return selected.value.has(node.path);
1510
+ const { files, selected: hit } = statsOf(node);
1511
+ return files > 0 && hit === files;
1512
+ }
1513
+ function isIndeterminate(node) {
1514
+ if (!node.is_dir) return false;
1515
+ const { files, selected: hit } = statsOf(node);
1516
+ return hit > 0 && hit < files;
1517
+ }
1518
+ const allFiles = computed(() => roots.value.flatMap(descendantFiles));
1519
+ const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
1520
+ const allChecked = computed(() => allFiles.value.length > 0 && selectedFiles.value.length === allFiles.value.length);
1521
+ const someChecked = computed(() => !allChecked.value && selectedFiles.value.length > 0);
1522
+ function fileUrlOf(node) {
1523
+ return api.value.fileUrl(node, passwords.resolve(node.path, node.password).password);
1381
1524
  }
1382
1525
  async function download(node) {
1383
1526
  if (node.is_dir) return;
@@ -1386,7 +1529,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1386
1529
  return;
1387
1530
  }
1388
1531
  try {
1389
- triggerUrl(await resolveUrl(node));
1532
+ openUrl(await fileUrlOf(node));
1390
1533
  } catch (error) {
1391
1534
  showToast(t("downloadFailed", { message: errorText(error) }));
1392
1535
  }
@@ -1414,17 +1557,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1414
1557
  if (demo.value || !canPreview(node)) return;
1415
1558
  previewResolving.value = true;
1416
1559
  try {
1417
- const url = await resolveUrl(node);
1560
+ const url = await fileUrlOf(node);
1418
1561
  if (seq !== previewSeq) return;
1419
1562
  previewSrc.value = url;
1420
- if (previewKind(node.name) === "pdf") {
1421
- const viewer = iframePreviewsFor(node.name, iframePreviews.value)[0];
1422
- if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
1423
- name: node.name,
1424
- rawUrl: url,
1425
- downloadUrl: directUrl(node)
1426
- });
1427
- }
1563
+ const viewer = previewKind(node.name) === "pdf" ? iframePreviewsFor(node.name, iframePreviews.value)[0] : void 0;
1564
+ if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
1565
+ name: node.name,
1566
+ rawUrl: url,
1567
+ downloadUrl: api.value.directUrl(node)
1568
+ });
1428
1569
  } catch (error) {
1429
1570
  if (seq === previewSeq) previewError.value = errorText(error);
1430
1571
  } finally {
@@ -1441,9 +1582,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1441
1582
  previewNode.value = null;
1442
1583
  }
1443
1584
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
1585
+ async function loadSettings() {
1586
+ const settings = await api.value.settings();
1587
+ siteTitle.value = settings.title;
1588
+ iframePreviews.value = settings.iframePreviews;
1589
+ }
1444
1590
  const stickyTop = ref(0);
1445
1591
  function measureStickyTop() {
1446
- if (typeof document === "undefined") return;
1447
1592
  let top = 0;
1448
1593
  for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
1449
1594
  const style = getComputedStyle(el);
@@ -1453,9 +1598,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1453
1598
  stickyTop.value = top;
1454
1599
  }
1455
1600
  onMounted(() => {
1456
- syncRemembered();
1601
+ passwords.sync();
1457
1602
  loadRoot();
1458
- loadSiteTitle();
1603
+ loadSettings();
1459
1604
  measureStickyTop();
1460
1605
  window.addEventListener("resize", measureStickyTop, { passive: true });
1461
1606
  });
@@ -1474,96 +1619,181 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1474
1619
  style: { "--fl-sticky-top-auto": `${stickyTop.value}px` },
1475
1620
  "aria-busy": refreshing.value || loadingRoot.value
1476
1621
  }, _attrs))}><div class="fl-bar">`);
1477
- if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: formatTotal(selectedBytes.value) }))}</span></div>`);
1622
+ if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(selectedFiles.value)) }))}</span></div>`);
1478
1623
  else {
1479
1624
  _push(`<div class="fl-title"><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
1480
1625
  if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
1481
1626
  else _push(`<!---->`);
1482
1627
  _push(`</div>`);
1483
1628
  }
1484
- if (selected.value.size) _push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}><svg class="fl-clear-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg><span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
1485
- else _push(`<!---->`);
1486
- _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="${ssrRenderClass([{ "fl-spinning": refreshing.value }, "fl-icon-btn fl-refresh"])}" type="button"${ssrIncludeBooleanAttr(refreshing.value || loadingRoot.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", refreshing.value ? t("refreshing") : t("refresh"))}${ssrRenderAttr("title", refreshing.value ? t("refreshing") : t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
1487
- if (selected.value.size) _push(`<button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch"))}</button>`);
1488
- else _push(`<!---->`);
1629
+ if (selected.value.size) {
1630
+ _push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}>`);
1631
+ _push(ssrRenderComponent(_sfc_main$2, {
1632
+ class: "fl-clear-icon",
1633
+ name: "close",
1634
+ size: 18
1635
+ }, null, _parent));
1636
+ _push(`<span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
1637
+ } else _push(`<!---->`);
1638
+ _push(`<div class="fl-tools"><label class="fl-search">`);
1639
+ _push(ssrRenderComponent(_sfc_main$2, {
1640
+ name: "search",
1641
+ size: 15
1642
+ }, null, _parent));
1643
+ _push(`<input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="${ssrRenderClass([{ "fl-spinning": refreshing.value }, "fl-icon-btn fl-refresh"])}" type="button"${ssrIncludeBooleanAttr(refreshing.value || loadingRoot.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", refreshing.value ? t("refreshing") : t("refresh"))}${ssrRenderAttr("title", refreshing.value ? t("refreshing") : t("refresh"))}>`);
1644
+ _push(ssrRenderComponent(_sfc_main$2, {
1645
+ name: "refresh",
1646
+ size: 15
1647
+ }, null, _parent));
1648
+ _push(`</button></div>`);
1649
+ if (selected.value.size) {
1650
+ _push(`<button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}>`);
1651
+ _push(ssrRenderComponent(_sfc_main$2, {
1652
+ name: "download",
1653
+ size: 15
1654
+ }, null, _parent));
1655
+ _push(` ${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch"))}</button>`);
1656
+ } else _push(`<!---->`);
1489
1657
  _push(`</div><div class="fl-row fl-head">`);
1490
1658
  if (refreshing.value) _push(`<span class="fl-progress" aria-hidden="true"></span>`);
1491
1659
  else _push(`<!---->`);
1492
- _push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
1660
+ _push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><!--[-->`);
1661
+ ssrRenderList([
1662
+ "name",
1663
+ "size",
1664
+ "modified"
1665
+ ], (column) => {
1666
+ _push(`<span class="${ssrRenderClass([`fl-cell-${column === "modified" ? "date" : column}`, "fl-cell"])}"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === column ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t(column === "name" ? "columnName" : column === "size" ? "columnSize" : "columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === column ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span>`);
1667
+ });
1668
+ _push(`<!--]--><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
1493
1669
  ssrRenderList(sortOptions.value, (option) => {
1494
1670
  _push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
1495
1671
  });
1496
1672
  _push(`<!--]--></select></label></span></div>`);
1497
1673
  if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
1498
- else if (rootLock.active) {
1499
- _push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.error }, "fl-unlock-root"])}"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><label class="fl-unlock-label"${ssrRenderAttr("for", `fl-root-unlock-${__props.path}`)}>${ssrInterpolate(t("passwordPrompt"))}</label>`);
1500
- if (rootLock.error) _push(`<span id="fl-root-unlock-error" class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(rootLock.error))}</span>`);
1674
+ else if (rootLock.value) {
1675
+ _push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.value.error }, "fl-unlock-root"])}">`);
1676
+ _push(ssrRenderComponent(_sfc_main$2, {
1677
+ class: "fl-unlock-lock",
1678
+ name: "lock"
1679
+ }, null, _parent));
1680
+ _push(`<label class="fl-unlock-label"${ssrRenderAttr("for", `${unref(uid)}-root`)}>${ssrInterpolate(t("passwordPrompt"))}</label>`);
1681
+ if (rootLock.value.error) _push(`<span${ssrRenderAttr("id", `${unref(uid)}-root-error`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(rootLock.value.error))}</span>`);
1501
1682
  else _push(`<!---->`);
1502
- _push(`<span class="fl-unlock"><input${ssrRenderAttr("id", `fl-root-unlock-${__props.path}`)}${ssrRenderAttr("value", rootLock.input)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", __props.path)}${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-invalid", !!rootLock.error)}${ssrRenderAttr("aria-describedby", rootLock.error ? "fl-root-unlock-error" : void 0)}>`);
1503
- if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", rootLock.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
1504
- else _push(`<!---->`);
1505
- _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(rootLock.busy || !rootLock.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button></span></form>`);
1683
+ _push(`<span class="fl-unlock"><input${ssrRenderAttr("id", `${unref(uid)}-root`)}${ssrRenderAttr("value", rootLock.value.input)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", __props.path)}${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-invalid", !!rootLock.value.error)}${ssrRenderAttr("aria-describedby", rootLock.value.error ? `${unref(uid)}-root-error` : void 0)}>`);
1684
+ if (canRemember.value) {
1685
+ _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", rootLock.value.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}>`);
1686
+ _push(ssrRenderComponent(_sfc_main$2, { name: "key" }, null, _parent));
1687
+ _push(`</button>`);
1688
+ } else _push(`<!---->`);
1689
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(rootLock.value.busy || !rootLock.value.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button></span></form>`);
1506
1690
  } else if (errorMsg.value) {
1507
1691
  _push(`<div class="fl-status fl-error">${ssrInterpolate(t("loadFailed", { message: errorMsg.value }))} `);
1508
1692
  if (/guest/i.test(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("guestHint"))}</p>`);
1509
1693
  else _push(`<!---->`);
1510
- if (/failed to fetch|networkerror/i.test(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("corsHint"))}</p>`);
1694
+ if (unref(isNetworkError)(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("corsHint"))}</p>`);
1511
1695
  else _push(`<!---->`);
1512
1696
  _push(`<button class="fl-btn" type="button">${ssrInterpolate(t("retry"))}</button></div>`);
1513
1697
  } else if (visible.value.length === 0) _push(`<div class="fl-status">${ssrInterpolate(t("empty"))}</div>`);
1514
1698
  else _push(`<!---->`);
1515
1699
  _push(`<!--[-->`);
1516
- ssrRenderList(visible.value, (node) => {
1700
+ ssrRenderList(visible.value, (node, index) => {
1517
1701
  _push(`<div class="${ssrRenderClass([{
1518
1702
  "fl-row-dir": node.is_dir,
1519
1703
  "fl-row-root": node.isRoot,
1520
1704
  "fl-row-selected": !node.is_dir && selected.value.has(node.path),
1521
- "fl-row-locked": node.locked,
1522
- "fl-row-prompting": node.locked && node.prompting,
1523
- "fl-row-error": node.locked && node.prompting && !!node.unlockError
1524
- }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(node.locked) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
1525
- if (node.locked) _push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1526
- else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1527
- else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
1705
+ "fl-row-locked": !!node.lock,
1706
+ "fl-row-prompting": node.lock?.prompting,
1707
+ "fl-row-error": node.lock?.prompting && !!node.lock.error
1708
+ }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
1709
+ if (node.lock) {
1710
+ _push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}>`);
1711
+ _push(ssrRenderComponent(_sfc_main$2, {
1712
+ name: "lock",
1713
+ size: 14
1714
+ }, null, _parent));
1715
+ _push(`</button>`);
1716
+ } else if (node.is_dir) {
1717
+ _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}>`);
1718
+ _push(ssrRenderComponent(_sfc_main$2, {
1719
+ name: "next",
1720
+ size: 14
1721
+ }, null, _parent));
1722
+ _push(`</button>`);
1723
+ } else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
1528
1724
  if (node.is_dir) _push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>`);
1529
1725
  else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
1530
1726
  _push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}">${ssrInterpolate(node.name)}</span><span class="fl-meta">`);
1531
- if (node.is_dir) {
1532
- _push(`<!--[-->`);
1533
- if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(formatTotal(unref(totalSize)(folderFiles(node))))}<!--]-->`);
1534
- else _push(`<!---->`);
1535
- _push(`<!--]-->`);
1536
- } else _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
1537
- _push(`</span></span>`);
1538
- if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))}</span>`);
1727
+ if (!node.is_dir) _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
1728
+ else if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))} · ${ssrInterpolate(unref(formatSize)(statsOf(node).bytes))}<!--]-->`);
1539
1729
  else _push(`<!---->`);
1540
- if (node.locked && node.prompting && node.unlockError) _push(`<span${ssrRenderAttr("id", `fl-unlock-error-${node.path}`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(node.unlockError))}</span>`);
1730
+ _push(`</span></span>`);
1731
+ if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))}</span>`);
1541
1732
  else _push(`<!---->`);
1542
- if (!node.locked && rememberedPaths.value.has(node.path)) _push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg> ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
1733
+ if (node.lock?.prompting && node.lock.error) _push(`<span${ssrRenderAttr("id", `${unref(uid)}-error-${index}`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(node.lock.error))}</span>`);
1543
1734
  else _push(`<!---->`);
1735
+ if (!node.lock && unref(remembered).has(node.path)) {
1736
+ _push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}>`);
1737
+ _push(ssrRenderComponent(_sfc_main$2, {
1738
+ name: "key",
1739
+ size: 11
1740
+ }, null, _parent));
1741
+ _push(` ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
1742
+ } else _push(`<!---->`);
1544
1743
  if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
1545
1744
  else _push(`<!---->`);
1546
1745
  _push(`</span>`);
1547
- if (node.locked && node.prompting) {
1548
- _push(`<form class="fl-unlock fl-unlock-row"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><input${ssrRenderAttr("value", node.unlockInput)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", node.path)}${ssrRenderAttr("placeholder", node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordFor", { name: node.name }))}${ssrRenderAttr("aria-invalid", !!node.unlockError)}${ssrRenderAttr("aria-describedby", node.unlockError ? `fl-unlock-error-${node.path}` : void 0)}>`);
1549
- if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", !!node.unlockRemember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
1550
- else _push(`<!---->`);
1551
- _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(node.unlockBusy || !node.unlockInput) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button><button class="fl-unlock-cancel" type="button"${ssrRenderAttr("aria-label", t("cancel"))}${ssrRenderAttr("title", t("cancel"))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg></button></form>`);
1746
+ if (node.lock?.prompting) {
1747
+ _push(`<form class="fl-unlock fl-unlock-row">`);
1748
+ _push(ssrRenderComponent(_sfc_main$2, {
1749
+ class: "fl-unlock-lock",
1750
+ name: "lock",
1751
+ size: 18
1752
+ }, null, _parent));
1753
+ _push(`<input${ssrRenderAttr("value", node.lock.input)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", node.path)}${ssrRenderAttr("placeholder", node.lock.error ? unlockErrorText(node.lock.error) : t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordFor", { name: node.name }))}${ssrRenderAttr("aria-invalid", !!node.lock.error)}${ssrRenderAttr("aria-describedby", node.lock.error ? `${unref(uid)}-error-${index}` : void 0)}>`);
1754
+ if (canRemember.value) {
1755
+ _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", node.lock.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}>`);
1756
+ _push(ssrRenderComponent(_sfc_main$2, { name: "key" }, null, _parent));
1757
+ _push(`</button>`);
1758
+ } else _push(`<!---->`);
1759
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(node.lock.busy || !node.lock.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button><button class="fl-unlock-cancel" type="button"${ssrRenderAttr("aria-label", t("cancel"))}${ssrRenderAttr("title", t("cancel"))}>`);
1760
+ _push(ssrRenderComponent(_sfc_main$2, {
1761
+ name: "close",
1762
+ size: 14
1763
+ }, null, _parent));
1764
+ _push(`</button></form>`);
1552
1765
  } else {
1553
- _push(`<!--[--><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
1554
- if (node.locked) _push(`<button class="fl-action fl-arrow-end fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1555
- else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1556
- else if (!node.locked) {
1766
+ _push(`<!--[--><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
1767
+ if (node.lock) {
1768
+ _push(`<button class="fl-action fl-arrow-end fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}>`);
1769
+ _push(ssrRenderComponent(_sfc_main$2, {
1770
+ name: "lock",
1771
+ size: 18
1772
+ }, null, _parent));
1773
+ _push(`</button>`);
1774
+ } else if (node.is_dir) {
1775
+ _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}>`);
1776
+ _push(ssrRenderComponent(_sfc_main$2, {
1777
+ name: "next",
1778
+ size: 18
1779
+ }, null, _parent));
1780
+ _push(`</button>`);
1781
+ } else {
1557
1782
  _push(`<!--[-->`);
1558
- if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
1559
- else _push(`<!---->`);
1560
- _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
1561
- } else _push(`<!---->`);
1783
+ if (canPreview(node)) {
1784
+ _push(`<button class="fl-action fl-preview-btn" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}>`);
1785
+ _push(ssrRenderComponent(_sfc_main$2, { name: "eye" }, null, _parent));
1786
+ _push(`</button>`);
1787
+ } else _push(`<!---->`);
1788
+ _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}>`);
1789
+ _push(ssrRenderComponent(_sfc_main$2, { name: "download" }, null, _parent));
1790
+ _push(`</button><!--]-->`);
1791
+ }
1562
1792
  _push(`</span><!--]-->`);
1563
1793
  }
1564
1794
  _push(`</div>`);
1565
1795
  });
1566
- _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: formatTotal(loadedBytes.value) }))}</span></div>`);
1796
+ _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) }))}</span></div>`);
1567
1797
  if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
1568
1798
  else _push(`<!---->`);
1569
1799
  _push(ssrRenderComponent(_sfc_main$1, {