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