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