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.
- package/README.md +17 -3
- package/dist/client/browser/index.d.ts +17 -13
- package/dist/client/browser/index.js +1277 -1464
- package/dist/client/ssr/index.d.ts +17 -13
- package/dist/client/ssr/index.js +1091 -861
- package/package.json +2 -2
|
@@ -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,
|
|
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 (!
|
|
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
|
-
|
|
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].
|
|
80
|
+
return match ? match[1].toLowerCase() : "";
|
|
211
81
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
"
|
|
217
|
-
"odt"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
98
|
+
"none",
|
|
99
|
+
"doc docx odt rtf pages wps"
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"sheet",
|
|
103
|
+
"text",
|
|
104
|
+
"csv tsv"
|
|
223
105
|
],
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"ods"
|
|
228
|
-
"csv",
|
|
229
|
-
"tsv",
|
|
230
|
-
"numbers",
|
|
231
|
-
"et"
|
|
106
|
+
[
|
|
107
|
+
"sheet",
|
|
108
|
+
"none",
|
|
109
|
+
"xls xlsx ods numbers et"
|
|
232
110
|
],
|
|
233
|
-
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"odp"
|
|
237
|
-
"key",
|
|
238
|
-
"dps"
|
|
111
|
+
[
|
|
112
|
+
"slide",
|
|
113
|
+
"none",
|
|
114
|
+
"ppt pptx odp key dps"
|
|
239
115
|
],
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"bmp"
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
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
|
-
|
|
267
|
-
"
|
|
268
|
-
"
|
|
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
|
-
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"json"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
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
|
|
851
|
+
const _hoisted_23$1 = {
|
|
512
852
|
key: 0,
|
|
513
853
|
class: "fl-preview-note"
|
|
514
854
|
};
|
|
515
|
-
const
|
|
516
|
-
const
|
|
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
|
|
521
|
-
const
|
|
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
|
-
*
|
|
892
|
+
* File preview in a native <dialog>, without leaving the page.
|
|
553
893
|
*
|
|
554
|
-
* image / video / audio
|
|
555
|
-
* pdf
|
|
556
|
-
*
|
|
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
|
-
}, [
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
}, [
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
}, [
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
}, [(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
}, [
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
}, [
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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",
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
875
|
-
createElementVNode("p",
|
|
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
|
-
}, [
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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 =
|
|
1013
|
-
const _hoisted_21 =
|
|
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
|
|
1029
|
-
const
|
|
1030
|
-
const
|
|
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
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
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
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
|
1455
|
+
const _hoisted_32 = [
|
|
1056
1456
|
"aria-pressed",
|
|
1057
1457
|
"aria-label",
|
|
1058
1458
|
"title"
|
|
1059
1459
|
];
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1460
|
+
const _hoisted_33 = ["disabled"];
|
|
1461
|
+
const _hoisted_34 = {
|
|
1062
1462
|
key: 2,
|
|
1063
1463
|
class: "fl-status fl-error"
|
|
1064
1464
|
};
|
|
1065
|
-
const
|
|
1465
|
+
const _hoisted_35 = {
|
|
1066
1466
|
key: 0,
|
|
1067
1467
|
class: "fl-hint"
|
|
1068
1468
|
};
|
|
1069
|
-
const
|
|
1469
|
+
const _hoisted_36 = {
|
|
1070
1470
|
key: 1,
|
|
1071
1471
|
class: "fl-hint"
|
|
1072
1472
|
};
|
|
1073
|
-
const
|
|
1473
|
+
const _hoisted_37 = {
|
|
1074
1474
|
key: 3,
|
|
1075
1475
|
class: "fl-status"
|
|
1076
1476
|
};
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
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
|
|
1086
|
-
const
|
|
1485
|
+
const _hoisted_40 = ["title"];
|
|
1486
|
+
const _hoisted_41 = [
|
|
1087
1487
|
"aria-label",
|
|
1088
1488
|
"title",
|
|
1089
1489
|
"onClick"
|
|
1090
1490
|
];
|
|
1091
|
-
const
|
|
1491
|
+
const _hoisted_42 = [
|
|
1092
1492
|
"aria-label",
|
|
1093
1493
|
"aria-expanded",
|
|
1094
1494
|
"onClick"
|
|
1095
1495
|
];
|
|
1096
|
-
const
|
|
1496
|
+
const _hoisted_43 = {
|
|
1097
1497
|
key: 2,
|
|
1098
1498
|
class: "fl-arrow-placeholder",
|
|
1099
1499
|
"aria-hidden": "true"
|
|
1100
1500
|
};
|
|
1101
|
-
const
|
|
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
|
|
1110
|
-
const
|
|
1111
|
-
const
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
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
|
|
1118
|
-
const
|
|
1517
|
+
const _hoisted_50 = ["id"];
|
|
1518
|
+
const _hoisted_51 = [
|
|
1119
1519
|
"aria-label",
|
|
1120
1520
|
"title",
|
|
1121
1521
|
"onClick"
|
|
1122
1522
|
];
|
|
1123
|
-
const
|
|
1523
|
+
const _hoisted_52 = {
|
|
1124
1524
|
key: 8,
|
|
1125
1525
|
class: "fl-loading",
|
|
1126
1526
|
"aria-hidden": "true"
|
|
1127
1527
|
};
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
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
|
|
1537
|
+
const _hoisted_55 = [
|
|
1138
1538
|
"aria-pressed",
|
|
1139
1539
|
"aria-label",
|
|
1140
1540
|
"title",
|
|
1141
1541
|
"onClick"
|
|
1142
1542
|
];
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1543
|
+
const _hoisted_56 = ["disabled"];
|
|
1544
|
+
const _hoisted_57 = [
|
|
1145
1545
|
"aria-label",
|
|
1146
1546
|
"title",
|
|
1147
1547
|
"onClick"
|
|
1148
1548
|
];
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
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
|
|
1558
|
+
const _hoisted_63 = [
|
|
1159
1559
|
"aria-label",
|
|
1160
1560
|
"title",
|
|
1161
1561
|
"onClick"
|
|
1162
1562
|
];
|
|
1163
|
-
const
|
|
1563
|
+
const _hoisted_64 = [
|
|
1164
1564
|
"aria-label",
|
|
1165
1565
|
"title",
|
|
1166
1566
|
"onClick"
|
|
1167
1567
|
];
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
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
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1238
|
-
|
|
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("
|
|
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
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1656
|
+
rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
|
|
1315
1657
|
});
|
|
1316
1658
|
}
|
|
1317
|
-
/** Show
|
|
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.
|
|
1661
|
+
node.lock ??= newLock();
|
|
1662
|
+
node.lock.prompting = true;
|
|
1320
1663
|
if (focus) focusUnlock(node.path);
|
|
1321
1664
|
}
|
|
1322
|
-
function cancelUnlock(
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
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
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
|
1598
|
-
const
|
|
1599
|
-
|
|
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
|
|
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.
|
|
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 } =
|
|
1764
|
+
const { password, rememberedAt } = passwords.resolve(node.path, node.password);
|
|
1613
1765
|
try {
|
|
1614
|
-
|
|
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.
|
|
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
|
|
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
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
|
|
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:
|
|
1744
|
-
configuredPassword: configured,
|
|
1813
|
+
password: own,
|
|
1745
1814
|
isRoot: true,
|
|
1746
|
-
|
|
1747
|
-
unresolved:
|
|
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 } =
|
|
1769
|
-
let items;
|
|
1835
|
+
const { password, rememberedAt } = passwords.resolve(props.path);
|
|
1770
1836
|
try {
|
|
1771
|
-
|
|
1837
|
+
nodes = await listRoot(password);
|
|
1772
1838
|
} catch (error) {
|
|
1773
|
-
|
|
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
|
-
|
|
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.
|
|
1866
|
+
rootLock.value = null;
|
|
1798
1867
|
selected.value = /* @__PURE__ */ new Set();
|
|
1799
|
-
|
|
1868
|
+
api.value.clearCache();
|
|
1800
1869
|
} catch (error) {
|
|
1801
|
-
if (error instanceof RootLockedError)
|
|
1802
|
-
|
|
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.
|
|
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
|
-
|
|
1894
|
+
api.value.clearCache();
|
|
1836
1895
|
} catch (error) {
|
|
1837
1896
|
if (seq !== refreshSeq) return;
|
|
1838
|
-
if (error instanceof RootLockedError)
|
|
1839
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1918
|
-
const hit =
|
|
1919
|
-
return hit > 0 && hit < files
|
|
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.
|
|
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)
|
|
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
|
-
|
|
1959
|
-
|
|
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
|
|
1967
|
-
const
|
|
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
|
|
2002
|
-
|
|
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
|
|
2011
|
-
|
|
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
|
-
|
|
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
|
|
2087
|
+
const url = await fileUrlOf(node);
|
|
2056
2088
|
if (seq !== previewSeq) return;
|
|
2057
2089
|
previewSrc.value = url;
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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
|
-
|
|
2128
|
+
passwords.sync();
|
|
2134
2129
|
loadRoot();
|
|
2135
|
-
|
|
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:
|
|
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
|
-
}, [
|
|
2158
|
+
}, [createVNode(_sfc_main$2, {
|
|
2164
2159
|
class: "fl-clear-icon",
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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
|
-
}, [
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
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
|
-
}, [
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
"
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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",
|
|
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[
|
|
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,
|
|
2268
|
-
}), 128))], 8,
|
|
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",
|
|
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
|
-
|
|
2236
|
+
createVNode(_sfc_main$2, {
|
|
2277
2237
|
class: "fl-unlock-lock",
|
|
2278
|
-
|
|
2279
|
-
|
|
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:
|
|
2297
|
-
}, toDisplayString(t("passwordPrompt")), 9,
|
|
2298
|
-
rootLock.error ? (openBlock(), createElementBlock("span",
|
|
2299
|
-
|
|
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:
|
|
2302
|
-
"onUpdate:modelValue": _cache[
|
|
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 ?
|
|
2309
|
-
}, null, 8,
|
|
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[
|
|
2318
|
-
}, [
|
|
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,
|
|
2273
|
+
disabled: rootLock.value.busy || !rootLock.value.input
|
|
2274
|
+
}, toDisplayString(t("open")), 9, _hoisted_33)
|
|
2342
2275
|
])
|
|
2343
|
-
], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div",
|
|
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",
|
|
2346
|
-
|
|
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",
|
|
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.
|
|
2361
|
-
"fl-row-prompting": node.
|
|
2362
|
-
"fl-row-error": node.
|
|
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",
|
|
2299
|
+
createElementVNode("label", _hoisted_38, [createElementVNode("input", {
|
|
2367
2300
|
type: "checkbox",
|
|
2368
|
-
disabled: node.
|
|
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,
|
|
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[
|
|
2311
|
+
_cache[8] || (_cache[8] = createElementVNode("span", {
|
|
2379
2312
|
class: "fl-indent",
|
|
2380
2313
|
"aria-hidden": "true"
|
|
2381
2314
|
}, null, -1)),
|
|
2382
|
-
node.
|
|
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
|
-
}, [
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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
|
-
}, [
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
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,
|
|
2429
|
-
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.
|
|
2432
|
-
}, toDisplayString(node.name), 11,
|
|
2433
|
-
node.is_dir && node.loaded ? (openBlock(), createElementBlock("span",
|
|
2434
|
-
node.
|
|
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:
|
|
2349
|
+
id: `${unref(uid)}-error-${index}`,
|
|
2437
2350
|
class: "fl-unlock-message",
|
|
2438
2351
|
role: "alert"
|
|
2439
|
-
}, toDisplayString(unlockErrorText(node.
|
|
2440
|
-
!node.
|
|
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
|
-
}, [
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
-
|
|
2372
|
+
createVNode(_sfc_main$2, {
|
|
2475
2373
|
class: "fl-unlock-lock",
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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.
|
|
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.
|
|
2382
|
+
placeholder: node.lock.error ? unlockErrorText(node.lock.error) : t("passwordPlaceholder"),
|
|
2498
2383
|
"aria-label": t("passwordFor", { name: node.name }),
|
|
2499
|
-
"aria-invalid": !!node.
|
|
2500
|
-
"aria-describedby": node.
|
|
2501
|
-
}, null, 8,
|
|
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":
|
|
2391
|
+
"aria-pressed": node.lock.remember,
|
|
2507
2392
|
"aria-label": t("rememberPassword"),
|
|
2508
2393
|
title: t("rememberPassword"),
|
|
2509
|
-
onClick: ($event) => node.
|
|
2510
|
-
}, [
|
|
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.
|
|
2533
|
-
}, toDisplayString(t("open")), 9,
|
|
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
|
-
}, [
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
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
|
-
}, [
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
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
|
-
}, [
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
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-
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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",
|
|
2638
|
-
toast.value ? (openBlock(), createElementBlock("div",
|
|
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[
|
|
2652
|
-
onNext: _cache[
|
|
2653
|
-
onDownload: _cache[
|
|
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",
|