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