vitepress-plugin-filelist 0.5.0 → 0.6.0
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 +35 -4
- package/dist/client/browser/index.d.ts +56 -13
- package/dist/client/browser/index.js +1701 -1542
- package/dist/client/ssr/index.d.ts +56 -13
- package/dist/client/ssr/index.js +1361 -876
- package/dist/client/style.css +153 -0
- package/package.json +3 -3
package/dist/client/ssr/index.js
CHANGED
|
@@ -1,147 +1,15 @@
|
|
|
1
|
-
import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, reactive, ref, unref, useSSRContext, watch } from "vue";
|
|
1
|
+
import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, reactive, ref, unref, useId, useSSRContext, watch } from "vue";
|
|
2
2
|
import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
|
|
3
|
-
// src/client/locales.ts
|
|
4
|
-
const fileListLocales = {
|
|
5
|
-
"zh-CN": {
|
|
6
|
-
allFiles: "全部文件",
|
|
7
|
-
demoBadge: "演示数据",
|
|
8
|
-
filter: "筛选文件名",
|
|
9
|
-
refresh: "刷新",
|
|
10
|
-
batch: "下载 ZIP",
|
|
11
|
-
batchCount: "批量下载({count})",
|
|
12
|
-
batching: "打包中 {percent}%",
|
|
13
|
-
passwordPrompt: "该目录受密码保护:",
|
|
14
|
-
passwordPlaceholder: "输入密码",
|
|
15
|
-
open: "打开",
|
|
16
|
-
columnName: "文件名",
|
|
17
|
-
columnSize: "文件大小",
|
|
18
|
-
columnDate: "最后修改日期",
|
|
19
|
-
loading: "加载中…",
|
|
20
|
-
loadFailed: "加载失败:{message}",
|
|
21
|
-
guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
|
|
22
|
-
corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
|
|
23
|
-
retry: "重试",
|
|
24
|
-
empty: "没有文件",
|
|
25
|
-
preview: "预览",
|
|
26
|
-
download: "下载",
|
|
27
|
-
loaded: "{count} 个已加载文件",
|
|
28
|
-
selected: "已选 {count} 项",
|
|
29
|
-
unloaded: "{count} 个目录未展开,未计入全选",
|
|
30
|
-
folderFailed: "打开文件夹失败:{message}",
|
|
31
|
-
demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
|
|
32
|
-
demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
|
|
33
|
-
demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
|
|
34
|
-
downloadFailed: "下载失败:{message}",
|
|
35
|
-
previewFailed: "预览失败:{message}",
|
|
36
|
-
batchFailed: "打包失败:{message}",
|
|
37
|
-
defaultZip: "资源",
|
|
38
|
-
clear: "清除",
|
|
39
|
-
clearSelection: "取消选择",
|
|
40
|
-
totalSize: "共 {size}",
|
|
41
|
-
folderItems: "{count} 项",
|
|
42
|
-
sortBy: "排序",
|
|
43
|
-
selectAll: "全选",
|
|
44
|
-
selectItem: "选择 {name}",
|
|
45
|
-
expand: "展开 {name}",
|
|
46
|
-
collapse: "收起 {name}",
|
|
47
|
-
close: "关闭",
|
|
48
|
-
previous: "上一个",
|
|
49
|
-
next: "下一个",
|
|
50
|
-
rotate: "旋转",
|
|
51
|
-
actualSize: "原始大小",
|
|
52
|
-
previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
|
|
53
|
-
previewUnavailable: "该文件无法在页面内预览,可下载后查看",
|
|
54
|
-
previewTooLarge: "文件较大({size}),请下载后查看",
|
|
55
|
-
textTruncated: "仅显示前 {size},完整内容请下载",
|
|
56
|
-
refreshing: "刷新中",
|
|
57
|
-
refreshFailed: "刷新失败:{message}",
|
|
58
|
-
cancel: "取消",
|
|
59
|
-
passwordFor: "{name} 的密码",
|
|
60
|
-
passwordWrong: "密码不正确或无权访问",
|
|
61
|
-
passwordStale: "已保存的密码已失效,请重新输入",
|
|
62
|
-
rememberPassword: "在此标签页中记住(关闭标签页后失效)",
|
|
63
|
-
remembered: "已记住",
|
|
64
|
-
forgetPassword: "忘记",
|
|
65
|
-
unlockFolder: "输入 {name} 的密码",
|
|
66
|
-
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
|
|
67
|
-
},
|
|
68
|
-
"en-US": {
|
|
69
|
-
allFiles: "All files",
|
|
70
|
-
demoBadge: "Demo data",
|
|
71
|
-
filter: "Filter files",
|
|
72
|
-
refresh: "Refresh",
|
|
73
|
-
batch: "Download ZIP",
|
|
74
|
-
batchCount: "Download ZIP ({count})",
|
|
75
|
-
batching: "Zipping {percent}%",
|
|
76
|
-
passwordPrompt: "This folder is password protected:",
|
|
77
|
-
passwordPlaceholder: "Folder password",
|
|
78
|
-
open: "Open",
|
|
79
|
-
columnName: "Name",
|
|
80
|
-
columnSize: "Size",
|
|
81
|
-
columnDate: "Modified",
|
|
82
|
-
loading: "Loading…",
|
|
83
|
-
loadFailed: "Failed to load: {message}",
|
|
84
|
-
guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
|
|
85
|
-
corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
|
|
86
|
-
retry: "Retry",
|
|
87
|
-
empty: "No files",
|
|
88
|
-
preview: "Preview",
|
|
89
|
-
download: "Download",
|
|
90
|
-
loaded: "{count} loaded file(s)",
|
|
91
|
-
selected: "{count} selected",
|
|
92
|
-
unloaded: "{count} folder(s) not expanded, excluded from select-all",
|
|
93
|
-
folderFailed: "Failed to open folder: {message}",
|
|
94
|
-
demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
|
|
95
|
-
demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
|
|
96
|
-
demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
|
|
97
|
-
downloadFailed: "Download failed: {message}",
|
|
98
|
-
previewFailed: "Preview failed: {message}",
|
|
99
|
-
batchFailed: "Failed to create ZIP: {message}",
|
|
100
|
-
defaultZip: "resources",
|
|
101
|
-
clear: "Clear",
|
|
102
|
-
clearSelection: "Clear selection",
|
|
103
|
-
totalSize: "{size} total",
|
|
104
|
-
folderItems: "{count} items",
|
|
105
|
-
sortBy: "Sort",
|
|
106
|
-
selectAll: "Select all",
|
|
107
|
-
selectItem: "Select {name}",
|
|
108
|
-
expand: "Expand {name}",
|
|
109
|
-
collapse: "Collapse {name}",
|
|
110
|
-
close: "Close",
|
|
111
|
-
previous: "Previous",
|
|
112
|
-
next: "Next",
|
|
113
|
-
rotate: "Rotate",
|
|
114
|
-
actualSize: "Actual size",
|
|
115
|
-
previewUnsupported: "This file type cannot be previewed here. Download it to view.",
|
|
116
|
-
previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
|
|
117
|
-
previewTooLarge: "This file is large ({size}). Download it to view.",
|
|
118
|
-
textTruncated: "Showing the first {size} only. Download for the full file.",
|
|
119
|
-
refreshing: "Refreshing",
|
|
120
|
-
refreshFailed: "Refresh failed: {message}",
|
|
121
|
-
cancel: "Cancel",
|
|
122
|
-
passwordFor: "Password for {name}",
|
|
123
|
-
passwordWrong: "Wrong password or no permission",
|
|
124
|
-
passwordStale: "The saved password no longer works. Enter it again.",
|
|
125
|
-
rememberPassword: "Remember in this tab (cleared when the tab closes)",
|
|
126
|
-
remembered: "Remembered",
|
|
127
|
-
forgetPassword: "Forget",
|
|
128
|
-
unlockFolder: "Enter the password for {name}",
|
|
129
|
-
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
function formatLabel(template, values = {}) {
|
|
133
|
-
return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
|
|
134
|
-
}
|
|
135
|
-
function resolveLabels(locale = "zh-CN", overrides) {
|
|
136
|
-
return {
|
|
137
|
-
...fileListLocales[locale] ?? fileListLocales["zh-CN"],
|
|
138
|
-
...overrides
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
3
|
// src/client/utils.ts
|
|
142
4
|
function joinPath(parent, name) {
|
|
143
5
|
return (parent.endsWith("/") ? parent : `${parent}/`) + name;
|
|
144
6
|
}
|
|
7
|
+
/** Whether `path` is `folder` itself or inside it (segment-wise, trailing slashes ignored). */
|
|
8
|
+
function isWithin(path, folder) {
|
|
9
|
+
const p = path.replace(/\/+$/, "") || "/";
|
|
10
|
+
const f = folder.replace(/\/+$/, "") || "/";
|
|
11
|
+
return f === "/" || p === f || p.startsWith(`${f}/`);
|
|
12
|
+
}
|
|
145
13
|
function encodePath(path) {
|
|
146
14
|
return path.split("/").map(encodeURIComponent).join("/");
|
|
147
15
|
}
|
|
@@ -180,8 +48,9 @@ function uniqueEntryName(name, used) {
|
|
|
180
48
|
used.add(candidate);
|
|
181
49
|
return candidate;
|
|
182
50
|
}
|
|
51
|
+
/** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
|
|
183
52
|
function formatSize(size) {
|
|
184
|
-
if (!
|
|
53
|
+
if (!Number.isFinite(size) || size < 0) return "-";
|
|
185
54
|
const units = [
|
|
186
55
|
"B",
|
|
187
56
|
"KB",
|
|
@@ -205,96 +74,106 @@ function formatDate(iso) {
|
|
|
205
74
|
const pad = (value) => String(value).padStart(2, "0");
|
|
206
75
|
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
|
|
207
76
|
}
|
|
208
|
-
|
|
77
|
+
/** Lower-case extension (letters and digits only), or `''`. */
|
|
78
|
+
function extension(name) {
|
|
209
79
|
const match = name.match(/\.([a-z0-9]+)$/i);
|
|
210
|
-
return match ? match[1].
|
|
80
|
+
return match ? match[1].toLowerCase() : "";
|
|
81
|
+
}
|
|
82
|
+
/** Upper-case extension for the type badge, e.g. `PDF`. */
|
|
83
|
+
function extOf(name) {
|
|
84
|
+
return extension(name).toUpperCase();
|
|
211
85
|
}
|
|
212
|
-
|
|
213
|
-
for
|
|
214
|
-
|
|
86
|
+
/**
|
|
87
|
+
* One table for every known extension: the badge family (`FileKind`) and how
|
|
88
|
+
* the preview dialog shows it (`PreviewKind`; Office and the like are `none`).
|
|
89
|
+
*/
|
|
90
|
+
const EXTENSIONS = [
|
|
91
|
+
[
|
|
92
|
+
"doc",
|
|
93
|
+
"text",
|
|
94
|
+
"txt md markdown"
|
|
95
|
+
],
|
|
96
|
+
[
|
|
215
97
|
"doc",
|
|
216
|
-
"
|
|
217
|
-
"odt"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
98
|
+
"none",
|
|
99
|
+
"doc docx odt rtf pages wps"
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"sheet",
|
|
103
|
+
"text",
|
|
104
|
+
"csv tsv"
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
"sheet",
|
|
108
|
+
"none",
|
|
109
|
+
"xls xlsx ods numbers et"
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"slide",
|
|
113
|
+
"none",
|
|
114
|
+
"ppt pptx odp key dps"
|
|
223
115
|
],
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"csv",
|
|
229
|
-
"tsv",
|
|
230
|
-
"numbers",
|
|
231
|
-
"et"
|
|
116
|
+
[
|
|
117
|
+
"pdf",
|
|
118
|
+
"pdf",
|
|
119
|
+
"pdf"
|
|
232
120
|
],
|
|
233
|
-
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"key",
|
|
238
|
-
"dps"
|
|
121
|
+
[
|
|
122
|
+
"image",
|
|
123
|
+
"image",
|
|
124
|
+
"png jpg jpeg gif webp svg bmp ico avif"
|
|
239
125
|
],
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"
|
|
243
|
-
"jpg",
|
|
244
|
-
"jpeg",
|
|
245
|
-
"gif",
|
|
246
|
-
"webp",
|
|
247
|
-
"svg",
|
|
248
|
-
"bmp",
|
|
249
|
-
"ico",
|
|
250
|
-
"avif",
|
|
126
|
+
[
|
|
127
|
+
"image",
|
|
128
|
+
"none",
|
|
251
129
|
"heic"
|
|
252
130
|
],
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
131
|
+
[
|
|
132
|
+
"media",
|
|
133
|
+
"video",
|
|
134
|
+
"mp4 webm m4v mov ogv"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"media",
|
|
138
|
+
"audio",
|
|
139
|
+
"mp3 wav ogg oga m4a aac flac opus"
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"media",
|
|
143
|
+
"none",
|
|
144
|
+
"mkv avi"
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"archive",
|
|
148
|
+
"none",
|
|
149
|
+
"zip rar 7z tar gz tgz bz2 xz iso"
|
|
265
150
|
],
|
|
266
|
-
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"tar",
|
|
271
|
-
"gz",
|
|
272
|
-
"tgz",
|
|
273
|
-
"bz2",
|
|
274
|
-
"xz",
|
|
275
|
-
"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"
|
|
276
155
|
],
|
|
277
|
-
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"html",
|
|
282
|
-
"css",
|
|
283
|
-
"py",
|
|
284
|
-
"java",
|
|
285
|
-
"c",
|
|
286
|
-
"cpp",
|
|
287
|
-
"go",
|
|
288
|
-
"rs",
|
|
289
|
-
"sh",
|
|
290
|
-
"yml",
|
|
291
|
-
"yaml",
|
|
292
|
-
"xml"
|
|
156
|
+
[
|
|
157
|
+
"other",
|
|
158
|
+
"text",
|
|
159
|
+
"log srt vtt ass lrc gitignore env"
|
|
293
160
|
]
|
|
294
|
-
|
|
161
|
+
];
|
|
162
|
+
const BY_EXTENSION = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const [kind, preview, list] of EXTENSIONS) for (const ext of list.split(" ")) BY_EXTENSION.set(ext, {
|
|
164
|
+
kind,
|
|
165
|
+
preview
|
|
166
|
+
});
|
|
295
167
|
/** Rough file family, used to colour the type badge. */
|
|
296
168
|
function fileKind(name) {
|
|
297
|
-
return
|
|
169
|
+
return BY_EXTENSION.get(extension(name))?.kind ?? "other";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* How a file is shown in the preview dialog. Office documents and anything
|
|
173
|
+
* else the browser cannot render are `none` (info + download).
|
|
174
|
+
*/
|
|
175
|
+
function previewKind(name) {
|
|
176
|
+
return BY_EXTENSION.get(extension(name))?.preview ?? "none";
|
|
298
177
|
}
|
|
299
178
|
/** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
|
|
300
179
|
function totalSize(items) {
|
|
@@ -308,100 +187,6 @@ function compareNodes(a, b, key, ascending = true) {
|
|
|
308
187
|
else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
|
|
309
188
|
return ascending ? result : -result;
|
|
310
189
|
}
|
|
311
|
-
const PREVIEW_EXTS = {
|
|
312
|
-
image: [
|
|
313
|
-
"png",
|
|
314
|
-
"jpg",
|
|
315
|
-
"jpeg",
|
|
316
|
-
"gif",
|
|
317
|
-
"webp",
|
|
318
|
-
"svg",
|
|
319
|
-
"bmp",
|
|
320
|
-
"ico",
|
|
321
|
-
"avif"
|
|
322
|
-
],
|
|
323
|
-
video: [
|
|
324
|
-
"mp4",
|
|
325
|
-
"webm",
|
|
326
|
-
"m4v",
|
|
327
|
-
"mov",
|
|
328
|
-
"ogv"
|
|
329
|
-
],
|
|
330
|
-
audio: [
|
|
331
|
-
"mp3",
|
|
332
|
-
"wav",
|
|
333
|
-
"ogg",
|
|
334
|
-
"oga",
|
|
335
|
-
"m4a",
|
|
336
|
-
"aac",
|
|
337
|
-
"flac",
|
|
338
|
-
"opus"
|
|
339
|
-
],
|
|
340
|
-
text: [
|
|
341
|
-
"txt",
|
|
342
|
-
"md",
|
|
343
|
-
"markdown",
|
|
344
|
-
"log",
|
|
345
|
-
"json",
|
|
346
|
-
"csv",
|
|
347
|
-
"tsv",
|
|
348
|
-
"xml",
|
|
349
|
-
"yml",
|
|
350
|
-
"yaml",
|
|
351
|
-
"toml",
|
|
352
|
-
"ini",
|
|
353
|
-
"conf",
|
|
354
|
-
"properties",
|
|
355
|
-
"js",
|
|
356
|
-
"mjs",
|
|
357
|
-
"cjs",
|
|
358
|
-
"ts",
|
|
359
|
-
"tsx",
|
|
360
|
-
"jsx",
|
|
361
|
-
"vue",
|
|
362
|
-
"css",
|
|
363
|
-
"scss",
|
|
364
|
-
"less",
|
|
365
|
-
"html",
|
|
366
|
-
"htm",
|
|
367
|
-
"py",
|
|
368
|
-
"java",
|
|
369
|
-
"kt",
|
|
370
|
-
"c",
|
|
371
|
-
"h",
|
|
372
|
-
"cpp",
|
|
373
|
-
"hpp",
|
|
374
|
-
"cs",
|
|
375
|
-
"go",
|
|
376
|
-
"rs",
|
|
377
|
-
"rb",
|
|
378
|
-
"php",
|
|
379
|
-
"sh",
|
|
380
|
-
"bat",
|
|
381
|
-
"ps1",
|
|
382
|
-
"sql",
|
|
383
|
-
"srt",
|
|
384
|
-
"vtt",
|
|
385
|
-
"ass",
|
|
386
|
-
"lrc",
|
|
387
|
-
"gitignore",
|
|
388
|
-
"env"
|
|
389
|
-
],
|
|
390
|
-
pdf: ["pdf"]
|
|
391
|
-
};
|
|
392
|
-
const PREVIEW_KINDS = {};
|
|
393
|
-
for (const [kind, exts] of Object.entries(PREVIEW_EXTS)) for (const ext of exts) PREVIEW_KINDS[ext] = kind;
|
|
394
|
-
function lowerExt(name) {
|
|
395
|
-
const dot = name.lastIndexOf(".");
|
|
396
|
-
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* How a file is shown in the preview dialog. Office documents and anything
|
|
400
|
-
* else the browser cannot render are `none` (info + download).
|
|
401
|
-
*/
|
|
402
|
-
function previewKind(name) {
|
|
403
|
-
return PREVIEW_KINDS[lowerExt(name)] ?? "none";
|
|
404
|
-
}
|
|
405
190
|
/**
|
|
406
191
|
* Viewers from OpenList's public `iframe_previews` setting that match a file,
|
|
407
192
|
* in the order OpenList lists them. Keys are comma-separated extensions or a
|
|
@@ -416,7 +201,7 @@ function iframePreviewsFor(name, setting) {
|
|
|
416
201
|
return [];
|
|
417
202
|
}
|
|
418
203
|
if (!parsed || typeof parsed !== "object") return [];
|
|
419
|
-
const ext =
|
|
204
|
+
const ext = extension(name);
|
|
420
205
|
const out = [];
|
|
421
206
|
for (const [key, viewers] of Object.entries(parsed)) {
|
|
422
207
|
let hit = false;
|
|
@@ -434,21 +219,709 @@ function iframePreviewsFor(name, setting) {
|
|
|
434
219
|
scheme
|
|
435
220
|
});
|
|
436
221
|
}
|
|
437
|
-
return out;
|
|
438
|
-
}
|
|
439
|
-
function applyUrlOps(placeholder, url) {
|
|
440
|
-
const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
|
|
441
|
-
let value = url;
|
|
442
|
-
for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
|
|
443
|
-
return value;
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function applyUrlOps(placeholder, url) {
|
|
225
|
+
const ops = placeholder.slice(1, placeholder.lastIndexOf("url")).replace(/d$/, "").match(/[eb]/g) ?? [];
|
|
226
|
+
let value = url;
|
|
227
|
+
for (const op of ops.reverse()) value = op === "e" ? encodeURIComponent(value) : btoa(String.fromCharCode(...new TextEncoder().encode(value)));
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
|
|
232
|
+
* (signed download URL), each optionally prefixed with `e` (URI-encode) or
|
|
233
|
+
* `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
|
|
234
|
+
*/
|
|
235
|
+
function fillPreviewUrl(scheme, args) {
|
|
236
|
+
return scheme.replace(/\$name/g, args.name).replace(/\$[eb_]*durl/g, (placeholder) => applyUrlOps(placeholder, args.downloadUrl)).replace(/\$[eb_]*url/g, (placeholder) => applyUrlOps(placeholder, args.rawUrl));
|
|
237
|
+
}
|
|
238
|
+
// src/client/api.ts
|
|
239
|
+
var ApiError = class extends Error {
|
|
240
|
+
code;
|
|
241
|
+
constructor(message, code) {
|
|
242
|
+
super(message);
|
|
243
|
+
this.code = code;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function errorText(error) {
|
|
247
|
+
return error instanceof Error ? error.message : String(error);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* OpenList has no dedicated code for a missing / wrong folder password; its
|
|
251
|
+
* message says "password is incorrect or you have no permission".
|
|
252
|
+
*/
|
|
253
|
+
function isPasswordError(error) {
|
|
254
|
+
return /password|密码/i.test(errorText(error));
|
|
255
|
+
}
|
|
256
|
+
/** Browser-level fetch failures (Chrome, Firefox, Safari wording), usually CORS. */
|
|
257
|
+
function isNetworkError(error) {
|
|
258
|
+
return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
|
|
259
|
+
}
|
|
260
|
+
function createOpenListApi(base, token) {
|
|
261
|
+
const root = base.replace(/\/+$/, "");
|
|
262
|
+
const urls = /* @__PURE__ */ new Map();
|
|
263
|
+
async function request(apiPath, body) {
|
|
264
|
+
const response = await fetch(`${root}${apiPath}`, {
|
|
265
|
+
method: body === void 0 ? "GET" : "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
268
|
+
...token ? { Authorization: token } : {}
|
|
269
|
+
},
|
|
270
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
271
|
+
});
|
|
272
|
+
const payload = await response.json().catch(() => null);
|
|
273
|
+
if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
|
|
274
|
+
if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
|
|
275
|
+
return payload.data;
|
|
276
|
+
}
|
|
277
|
+
function absoluteUrl(raw) {
|
|
278
|
+
if (!raw) return "";
|
|
279
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
|
|
280
|
+
const origin = root || (typeof window !== "undefined" ? window.location.origin : "");
|
|
281
|
+
try {
|
|
282
|
+
return new URL(raw, `${origin}/`).href;
|
|
283
|
+
} catch {
|
|
284
|
+
return raw;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const directUrl = (file) => `${root}/d${encodePath(file.path)}${file.sign ? `?sign=${encodeURIComponent(file.sign)}` : ""}`;
|
|
288
|
+
return {
|
|
289
|
+
async list(path, password) {
|
|
290
|
+
return (await request("/api/fs/list", {
|
|
291
|
+
path,
|
|
292
|
+
password,
|
|
293
|
+
page: 1,
|
|
294
|
+
per_page: 0,
|
|
295
|
+
refresh: false
|
|
296
|
+
}))?.content ?? [];
|
|
297
|
+
},
|
|
298
|
+
get: (path, password) => request("/api/fs/get", {
|
|
299
|
+
path,
|
|
300
|
+
password
|
|
301
|
+
}),
|
|
302
|
+
async settings() {
|
|
303
|
+
try {
|
|
304
|
+
const data = await request("/api/public/settings");
|
|
305
|
+
return {
|
|
306
|
+
title: data?.site_title || data?.title || "",
|
|
307
|
+
iframePreviews: data?.iframe_previews || ""
|
|
308
|
+
};
|
|
309
|
+
} catch {
|
|
310
|
+
return {
|
|
311
|
+
title: "",
|
|
312
|
+
iframePreviews: ""
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
async fileUrl(file, password) {
|
|
317
|
+
if (file.raw_url) return absoluteUrl(file.raw_url);
|
|
318
|
+
const cached = urls.get(file.path);
|
|
319
|
+
if (cached) return cached;
|
|
320
|
+
let url = "";
|
|
321
|
+
try {
|
|
322
|
+
url = absoluteUrl((await request("/api/fs/get", {
|
|
323
|
+
path: file.path,
|
|
324
|
+
password
|
|
325
|
+
}))?.raw_url ?? "");
|
|
326
|
+
} catch {}
|
|
327
|
+
url ||= directUrl(file);
|
|
328
|
+
urls.set(file.path, url);
|
|
329
|
+
return url;
|
|
330
|
+
},
|
|
331
|
+
directUrl,
|
|
332
|
+
clearCache(path) {
|
|
333
|
+
if (!path) urls.clear();
|
|
334
|
+
else for (const key of [...urls.keys()]) if (isWithin(key, path)) urls.delete(key);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// src/client/demo.ts
|
|
339
|
+
/** Built-in sample data, used when no `base` is given. */
|
|
340
|
+
const TREE = {
|
|
341
|
+
"/": [
|
|
342
|
+
{
|
|
343
|
+
name: "assignments",
|
|
344
|
+
size: 0,
|
|
345
|
+
is_dir: true,
|
|
346
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "exams",
|
|
350
|
+
size: 0,
|
|
351
|
+
is_dir: true,
|
|
352
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "materials",
|
|
356
|
+
size: 0,
|
|
357
|
+
is_dir: true,
|
|
358
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "notes",
|
|
362
|
+
size: 0,
|
|
363
|
+
is_dir: true,
|
|
364
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "slides",
|
|
368
|
+
size: 0,
|
|
369
|
+
is_dir: true,
|
|
370
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "加密示例(密码 1234)",
|
|
374
|
+
size: 0,
|
|
375
|
+
is_dir: true,
|
|
376
|
+
modified: "2026-10-20T18:00:00+08:00"
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
"/assignments": [{
|
|
380
|
+
name: "线代作业参考答案",
|
|
381
|
+
size: 0,
|
|
382
|
+
is_dir: true,
|
|
383
|
+
modified: "2026-10-12T21:30:00+08:00"
|
|
384
|
+
}, {
|
|
385
|
+
name: "作业提交说明.pdf",
|
|
386
|
+
size: 186432,
|
|
387
|
+
is_dir: false,
|
|
388
|
+
modified: "2026-09-03T10:15:00+08:00"
|
|
389
|
+
}],
|
|
390
|
+
"/assignments/线代作业参考答案": [
|
|
391
|
+
{
|
|
392
|
+
name: "第1次作业参考答案.doc",
|
|
393
|
+
size: 412672,
|
|
394
|
+
is_dir: false,
|
|
395
|
+
modified: "2026-09-15T22:41:00+08:00"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "第2次作业参考答案.pdf",
|
|
399
|
+
size: 1048576,
|
|
400
|
+
is_dir: false,
|
|
401
|
+
modified: "2026-09-22T22:03:00+08:00"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "第3次作业参考答案.pdf",
|
|
405
|
+
size: 986112,
|
|
406
|
+
is_dir: false,
|
|
407
|
+
modified: "2026-09-29T21:57:00+08:00"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "第4次作业参考答案.pdf",
|
|
411
|
+
size: 1572864,
|
|
412
|
+
is_dir: false,
|
|
413
|
+
modified: "2026-10-13T22:10:00+08:00"
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
"/exams": [
|
|
417
|
+
{
|
|
418
|
+
name: "2023 期末试题和答案.pdf",
|
|
419
|
+
size: 2345678,
|
|
420
|
+
is_dir: false,
|
|
421
|
+
modified: "2025-01-18T09:20:00+08:00"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "2024 期中试题.pdf",
|
|
425
|
+
size: 1234567,
|
|
426
|
+
is_dir: false,
|
|
427
|
+
modified: "2025-11-02T14:08:00+08:00"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "代数与几何-多年合集-试题和答案.pdf",
|
|
431
|
+
size: 18874368,
|
|
432
|
+
is_dir: false,
|
|
433
|
+
modified: "2025-01-18T09:25:00+08:00"
|
|
434
|
+
}
|
|
435
|
+
],
|
|
436
|
+
"/materials": [
|
|
437
|
+
{
|
|
438
|
+
name: "线性代数及其应用(Lay 第5版).pdf",
|
|
439
|
+
size: 48231424,
|
|
440
|
+
is_dir: false,
|
|
441
|
+
modified: "2024-07-29T16:44:00+08:00"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: "Introduction to Linear Algebra(Strang).pdf",
|
|
445
|
+
size: 36700160,
|
|
446
|
+
is_dir: false,
|
|
447
|
+
modified: "2024-07-29T16:50:00+08:00"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "疑难解答与习题指导.zip",
|
|
451
|
+
size: 9437184,
|
|
452
|
+
is_dir: false,
|
|
453
|
+
modified: "2024-08-05T11:12:00+08:00"
|
|
454
|
+
}
|
|
455
|
+
],
|
|
456
|
+
"/notes": [{
|
|
457
|
+
name: "线性代数笔记(106页).pdf",
|
|
458
|
+
size: 12582912,
|
|
459
|
+
is_dir: false,
|
|
460
|
+
modified: "2025-06-30T20:31:00+08:00"
|
|
461
|
+
}, {
|
|
462
|
+
name: "期末复习提纲.docx",
|
|
463
|
+
size: 245760,
|
|
464
|
+
is_dir: false,
|
|
465
|
+
modified: "2026-01-05T19:02:00+08:00"
|
|
466
|
+
}],
|
|
467
|
+
"/加密示例(密码 1234)": [{
|
|
468
|
+
name: "期末答案(教师版).pdf",
|
|
469
|
+
size: 2621440,
|
|
470
|
+
is_dir: false,
|
|
471
|
+
modified: "2026-10-20T18:00:00+08:00"
|
|
472
|
+
}, {
|
|
473
|
+
name: "成绩登记表.xlsx",
|
|
474
|
+
size: 48128,
|
|
475
|
+
is_dir: false,
|
|
476
|
+
modified: "2026-10-21T09:12:00+08:00"
|
|
477
|
+
}],
|
|
478
|
+
"/slides": [{
|
|
479
|
+
name: "第一章 行列式.pptx",
|
|
480
|
+
size: 3145728,
|
|
481
|
+
is_dir: false,
|
|
482
|
+
modified: "2026-09-08T13:26:00+08:00"
|
|
483
|
+
}, {
|
|
484
|
+
name: "第二章 矩阵.pptx",
|
|
485
|
+
size: 4194304,
|
|
486
|
+
is_dir: false,
|
|
487
|
+
modified: "2026-09-15T13:30:00+08:00"
|
|
488
|
+
}]
|
|
489
|
+
};
|
|
490
|
+
/** Demo-only folder passwords, so the demo can show the password flow. */
|
|
491
|
+
const PASSWORDS = { "/加密示例(密码 1234)": "1234" };
|
|
492
|
+
function guard(path, password) {
|
|
493
|
+
const lock = Object.entries(PASSWORDS).find(([folder]) => isWithin(path, folder));
|
|
494
|
+
if (lock && lock[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
|
|
495
|
+
}
|
|
496
|
+
function parentOf(path) {
|
|
497
|
+
return path.slice(0, path.lastIndexOf("/")) || "/";
|
|
498
|
+
}
|
|
499
|
+
/** Demo OpenList: same API, no network; files have no URL. */
|
|
500
|
+
function createDemoApi(delay = 220) {
|
|
501
|
+
return {
|
|
502
|
+
async list(path, password) {
|
|
503
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
504
|
+
guard(path, password);
|
|
505
|
+
return TREE[path] ?? [];
|
|
506
|
+
},
|
|
507
|
+
async get(path, password) {
|
|
508
|
+
guard(path, password);
|
|
509
|
+
const name = path.split("/").filter(Boolean).pop() ?? "";
|
|
510
|
+
const item = (TREE[parentOf(path)] ?? []).find((entry) => entry.name === name);
|
|
511
|
+
if (!item) throw new ApiError("object not found", 404);
|
|
512
|
+
return item;
|
|
513
|
+
},
|
|
514
|
+
settings: async () => ({
|
|
515
|
+
title: "",
|
|
516
|
+
iframePreviews: ""
|
|
517
|
+
}),
|
|
518
|
+
fileUrl: async () => "",
|
|
519
|
+
directUrl: () => "",
|
|
520
|
+
clearCache: () => {}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
// src/client/ConfirmDialog.vue
|
|
524
|
+
const _sfc_main$3 = /*@__PURE__*/ defineComponent({
|
|
525
|
+
__name: "ConfirmDialog",
|
|
526
|
+
__ssrInlineRender: true,
|
|
527
|
+
props: {
|
|
528
|
+
open: { type: Boolean },
|
|
529
|
+
title: {},
|
|
530
|
+
text: {},
|
|
531
|
+
tip: {},
|
|
532
|
+
confirmLabel: {},
|
|
533
|
+
cancelLabel: {}
|
|
534
|
+
},
|
|
535
|
+
emits: ["confirm", "close"],
|
|
536
|
+
setup(__props, { emit: __emit }) {
|
|
537
|
+
/** Small native <dialog> for a yes / no question or a notice (bottom sheet on phones). */
|
|
538
|
+
const props = __props;
|
|
539
|
+
const dialog = ref();
|
|
540
|
+
watch(() => props.open, (open) => {
|
|
541
|
+
const el = dialog.value;
|
|
542
|
+
if (!el) return;
|
|
543
|
+
if (open && !el.open) {
|
|
544
|
+
if (typeof el.showModal === "function") el.showModal();
|
|
545
|
+
else el.setAttribute("open", "");
|
|
546
|
+
} else if (!open && el.open) {
|
|
547
|
+
if (typeof el.close === "function") el.close();
|
|
548
|
+
else el.removeAttribute("open");
|
|
549
|
+
}
|
|
550
|
+
}, { flush: "post" });
|
|
551
|
+
return (_ctx, _push, _parent, _attrs) => {
|
|
552
|
+
_push(`<dialog${ssrRenderAttrs(mergeProps({
|
|
553
|
+
ref_key: "dialog",
|
|
554
|
+
ref: dialog,
|
|
555
|
+
class: "fl-confirm",
|
|
556
|
+
"aria-label": __props.title
|
|
557
|
+
}, _attrs))}>`);
|
|
558
|
+
if (__props.open) {
|
|
559
|
+
_push(`<div class="fl-confirm-panel"><p class="fl-confirm-title">${ssrInterpolate(__props.title)}</p><p class="fl-confirm-text">${ssrInterpolate(__props.text)}</p>`);
|
|
560
|
+
if (__props.tip) _push(`<p class="fl-confirm-text">${ssrInterpolate(__props.tip)}</p>`);
|
|
561
|
+
else _push(`<!---->`);
|
|
562
|
+
_push(`<div class="fl-confirm-actions">`);
|
|
563
|
+
if (__props.confirmLabel) _push(`<button class="fl-btn" type="button">${ssrInterpolate(__props.cancelLabel)}</button>`);
|
|
564
|
+
else _push(`<!---->`);
|
|
565
|
+
_push(`<button class="fl-btn fl-btn-primary" type="button" autofocus>${ssrInterpolate(__props.confirmLabel || __props.cancelLabel)}</button></div></div>`);
|
|
566
|
+
} else _push(`<!---->`);
|
|
567
|
+
_push(`</dialog>`);
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
const _sfc_setup$3 = _sfc_main$3.setup;
|
|
572
|
+
_sfc_main$3.setup = (props, ctx) => {
|
|
573
|
+
const ssrContext = useSSRContext();
|
|
574
|
+
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/ConfirmDialog.vue");
|
|
575
|
+
return _sfc_setup$3 ? _sfc_setup$3(props, ctx) : void 0;
|
|
576
|
+
};
|
|
577
|
+
// src/client/download.ts
|
|
578
|
+
/** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
|
|
579
|
+
function openUrl(url) {
|
|
580
|
+
const anchor = document.createElement("a");
|
|
581
|
+
anchor.href = url;
|
|
582
|
+
anchor.target = "_blank";
|
|
583
|
+
anchor.rel = "noopener";
|
|
584
|
+
document.body.appendChild(anchor);
|
|
585
|
+
anchor.click();
|
|
586
|
+
anchor.remove();
|
|
587
|
+
}
|
|
588
|
+
function saveBlob(blob, name) {
|
|
589
|
+
const href = URL.createObjectURL(blob);
|
|
590
|
+
const anchor = document.createElement("a");
|
|
591
|
+
anchor.href = href;
|
|
592
|
+
anchor.download = name;
|
|
593
|
+
document.body.appendChild(anchor);
|
|
594
|
+
anchor.click();
|
|
595
|
+
anchor.remove();
|
|
596
|
+
setTimeout(() => URL.revokeObjectURL(href), 3e4);
|
|
597
|
+
}
|
|
598
|
+
/** File-system-safe ZIP name from a title. */
|
|
599
|
+
function zipFileName(title) {
|
|
600
|
+
return `${title.replace(/[\\/:*?"<>|]/g, "_")}.zip`;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The ZIP as a stream: files are fetched one after another and passed through
|
|
604
|
+
* as they arrive, stored without compression (most shared files are already
|
|
605
|
+
* compressed). Clashing entry names get ` (2)`, ` (3)`...
|
|
606
|
+
*/
|
|
607
|
+
async function zipStream(files, options) {
|
|
608
|
+
const { makeZip } = await import("client-zip");
|
|
609
|
+
const progress = {
|
|
610
|
+
bytes: 0,
|
|
611
|
+
totalBytes: files.reduce((sum, file) => sum + (Number.isFinite(file.size) ? file.size : 0), 0),
|
|
612
|
+
files: 0,
|
|
613
|
+
totalFiles: files.length
|
|
614
|
+
};
|
|
615
|
+
const report = () => options.onProgress({ ...progress });
|
|
616
|
+
report();
|
|
617
|
+
async function* entries() {
|
|
618
|
+
const used = /* @__PURE__ */ new Set();
|
|
619
|
+
for (const file of files) {
|
|
620
|
+
const response = await fetch(await options.urlOf(file), { signal: options.signal });
|
|
621
|
+
if (!response.ok || !response.body) throw new Error(`${file.name}:HTTP ${response.status}`);
|
|
622
|
+
const counted = response.body.pipeThrough(new TransformStream({
|
|
623
|
+
transform(chunk, controller) {
|
|
624
|
+
progress.bytes += chunk.byteLength;
|
|
625
|
+
report();
|
|
626
|
+
controller.enqueue(chunk);
|
|
627
|
+
},
|
|
628
|
+
flush() {
|
|
629
|
+
progress.files += 1;
|
|
630
|
+
report();
|
|
631
|
+
}
|
|
632
|
+
}));
|
|
633
|
+
const modified = new Date(file.modified);
|
|
634
|
+
yield {
|
|
635
|
+
name: uniqueEntryName(zipEntryName(file.zipPath), used),
|
|
636
|
+
lastModified: Number.isNaN(modified.getTime()) ? void 0 : modified,
|
|
637
|
+
input: counted
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return makeZip(entries());
|
|
642
|
+
}
|
|
643
|
+
/** Build the ZIP in memory (browsers without file streaming). */
|
|
644
|
+
async function zipToBlob(files, options) {
|
|
645
|
+
const reader = (await zipStream(files, options)).getReader();
|
|
646
|
+
const chunks = [];
|
|
647
|
+
const abort = () => void reader.cancel(options.signal.reason).catch(() => {});
|
|
648
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
649
|
+
if (options.signal.aborted) abort();
|
|
650
|
+
try {
|
|
651
|
+
for (;;) {
|
|
652
|
+
const { done, value } = await reader.read();
|
|
653
|
+
if (done) break;
|
|
654
|
+
chunks.push(value);
|
|
655
|
+
}
|
|
656
|
+
} finally {
|
|
657
|
+
options.signal.removeEventListener("abort", abort);
|
|
658
|
+
}
|
|
659
|
+
options.signal.throwIfAborted();
|
|
660
|
+
return new Blob(chunks, { type: "application/zip" });
|
|
661
|
+
}
|
|
662
|
+
// src/client/icons.ts
|
|
663
|
+
/** Stroke icons on a 24×24 grid, drawn by `Icon.vue`. */
|
|
664
|
+
const ICONS = {
|
|
665
|
+
lock: {
|
|
666
|
+
stroke: 2.2,
|
|
667
|
+
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"]
|
|
668
|
+
},
|
|
669
|
+
key: {
|
|
670
|
+
stroke: 2,
|
|
671
|
+
paths: [
|
|
672
|
+
"M4 15a4 4 0 1 0 8 0a4 4 0 1 0-8 0",
|
|
673
|
+
"m11 12 9-9",
|
|
674
|
+
"m17 6 3 3"
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
close: {
|
|
678
|
+
stroke: 2.2,
|
|
679
|
+
paths: ["M6 6l12 12", "M18 6 6 18"]
|
|
680
|
+
},
|
|
681
|
+
download: {
|
|
682
|
+
stroke: 2.2,
|
|
683
|
+
paths: [
|
|
684
|
+
"M12 3v12",
|
|
685
|
+
"m7 10 5 5 5-5",
|
|
686
|
+
"M5 21h14"
|
|
687
|
+
]
|
|
688
|
+
},
|
|
689
|
+
eye: {
|
|
690
|
+
stroke: 2,
|
|
691
|
+
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"]
|
|
692
|
+
},
|
|
693
|
+
refresh: {
|
|
694
|
+
stroke: 2,
|
|
695
|
+
paths: ["M21 12a9 9 0 1 1-3-6.7", "M21 4v5h-5"]
|
|
696
|
+
},
|
|
697
|
+
search: {
|
|
698
|
+
stroke: 2,
|
|
699
|
+
paths: ["M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0", "m20 20-3.5-3.5"]
|
|
700
|
+
},
|
|
701
|
+
next: {
|
|
702
|
+
stroke: 2.2,
|
|
703
|
+
paths: ["m9 6 6 6-6 6"]
|
|
704
|
+
},
|
|
705
|
+
previous: {
|
|
706
|
+
stroke: 2.2,
|
|
707
|
+
paths: ["m15 6-6 6 6 6"]
|
|
708
|
+
},
|
|
709
|
+
zoomIn: {
|
|
710
|
+
stroke: 2,
|
|
711
|
+
paths: [
|
|
712
|
+
"M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
|
|
713
|
+
"m20 20-3.5-3.5",
|
|
714
|
+
"M11 8v6M8 11h6"
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
zoomOut: {
|
|
718
|
+
stroke: 2,
|
|
719
|
+
paths: [
|
|
720
|
+
"M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
|
|
721
|
+
"m20 20-3.5-3.5",
|
|
722
|
+
"M8 11h6"
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
// src/client/Icon.vue
|
|
727
|
+
const _sfc_main$2 = /*@__PURE__*/ defineComponent({
|
|
728
|
+
__name: "Icon",
|
|
729
|
+
__ssrInlineRender: true,
|
|
730
|
+
props: {
|
|
731
|
+
name: {},
|
|
732
|
+
size: { default: 16 }
|
|
733
|
+
},
|
|
734
|
+
setup(__props) {
|
|
735
|
+
return (_ctx, _push, _parent, _attrs) => {
|
|
736
|
+
_push(`<svg${ssrRenderAttrs(mergeProps({
|
|
737
|
+
viewBox: "0 0 24 24",
|
|
738
|
+
width: __props.size,
|
|
739
|
+
height: __props.size,
|
|
740
|
+
fill: "none",
|
|
741
|
+
stroke: "currentColor",
|
|
742
|
+
"stroke-width": unref(ICONS)[__props.name].stroke,
|
|
743
|
+
"stroke-linecap": "round",
|
|
744
|
+
"stroke-linejoin": "round",
|
|
745
|
+
"aria-hidden": "true"
|
|
746
|
+
}, _attrs))}><!--[-->`);
|
|
747
|
+
ssrRenderList(unref(ICONS)[__props.name].paths, (d) => {
|
|
748
|
+
_push(`<path${ssrRenderAttr("d", d)}></path>`);
|
|
749
|
+
});
|
|
750
|
+
_push(`<!--]--></svg>`);
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
const _sfc_setup$2 = _sfc_main$2.setup;
|
|
755
|
+
_sfc_main$2.setup = (props, ctx) => {
|
|
756
|
+
const ssrContext = useSSRContext();
|
|
757
|
+
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/Icon.vue");
|
|
758
|
+
return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
|
|
759
|
+
};
|
|
760
|
+
// src/client/locales.ts
|
|
761
|
+
const fileListLocales = {
|
|
762
|
+
"zh-CN": {
|
|
763
|
+
allFiles: "全部文件",
|
|
764
|
+
demoBadge: "演示数据",
|
|
765
|
+
filter: "筛选文件名",
|
|
766
|
+
refresh: "刷新",
|
|
767
|
+
batch: "下载 ZIP",
|
|
768
|
+
batchCount: "批量下载({count})",
|
|
769
|
+
batching: "打包中 {percent}%",
|
|
770
|
+
passwordPrompt: "该目录受密码保护:",
|
|
771
|
+
passwordPlaceholder: "输入密码",
|
|
772
|
+
open: "打开",
|
|
773
|
+
columnName: "文件名",
|
|
774
|
+
columnSize: "文件大小",
|
|
775
|
+
columnDate: "最后修改日期",
|
|
776
|
+
loading: "加载中…",
|
|
777
|
+
loadFailed: "加载失败:{message}",
|
|
778
|
+
guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
|
|
779
|
+
corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
|
|
780
|
+
retry: "重试",
|
|
781
|
+
empty: "没有文件",
|
|
782
|
+
preview: "预览",
|
|
783
|
+
download: "下载",
|
|
784
|
+
loaded: "{count} 个已加载文件",
|
|
785
|
+
selected: "已选 {count} 项",
|
|
786
|
+
unloaded: "{count} 个目录未展开,未计入全选",
|
|
787
|
+
folderFailed: "打开文件夹失败:{message}",
|
|
788
|
+
demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
|
|
789
|
+
demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
|
|
790
|
+
demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
|
|
791
|
+
downloadFailed: "下载失败:{message}",
|
|
792
|
+
previewFailed: "预览失败:{message}",
|
|
793
|
+
batchFailed: "打包失败:{message}",
|
|
794
|
+
defaultZip: "资源",
|
|
795
|
+
clear: "清除",
|
|
796
|
+
clearSelection: "取消选择",
|
|
797
|
+
totalSize: "共 {size}",
|
|
798
|
+
folderItems: "{count} 项",
|
|
799
|
+
sortBy: "排序",
|
|
800
|
+
selectAll: "全选",
|
|
801
|
+
selectItem: "选择 {name}",
|
|
802
|
+
expand: "展开 {name}",
|
|
803
|
+
collapse: "收起 {name}",
|
|
804
|
+
close: "关闭",
|
|
805
|
+
previous: "上一个",
|
|
806
|
+
next: "下一个",
|
|
807
|
+
rotate: "旋转",
|
|
808
|
+
actualSize: "原始大小",
|
|
809
|
+
previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
|
|
810
|
+
previewUnavailable: "该文件无法在页面内预览,可下载后查看",
|
|
811
|
+
previewTooLarge: "文件较大({size}),请下载后查看",
|
|
812
|
+
textTruncated: "仅显示前 {size},完整内容请下载",
|
|
813
|
+
refreshing: "刷新中",
|
|
814
|
+
refreshFailed: "刷新失败:{message}",
|
|
815
|
+
cancel: "取消",
|
|
816
|
+
passwordFor: "{name} 的密码",
|
|
817
|
+
passwordWrong: "密码不正确或无权访问",
|
|
818
|
+
passwordStale: "已保存的密码已失效,请重新输入",
|
|
819
|
+
rememberPassword: "在此标签页中记住(关闭标签页后失效)",
|
|
820
|
+
remembered: "已记住",
|
|
821
|
+
forgetPassword: "忘记",
|
|
822
|
+
unlockFolder: "输入 {name} 的密码",
|
|
823
|
+
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
824
|
+
batchProgress: "{done} / {total} · {files} / {count} 个文件",
|
|
825
|
+
batchDone: "已打包 {name}({count} 个文件,{size})",
|
|
826
|
+
batchCanceled: "已取消下载",
|
|
827
|
+
zipWarnTitle: "文件较大({size})",
|
|
828
|
+
zipWarnText: "当前浏览器要先把文件全部放进内存再打包,超过 {limit} 可能失败或让页面崩溃。建议分批下载。",
|
|
829
|
+
zipStreamTip: "在 Chrome 或 Edge 中可以边下载边保存,不受大小限制。",
|
|
830
|
+
zipContinue: "仍然下载",
|
|
831
|
+
zipBlockTitle: "无法打包 {size}",
|
|
832
|
+
zipBlockText: "当前浏览器一次最多打包 {limit},请分批勾选下载。",
|
|
833
|
+
gotIt: "知道了",
|
|
834
|
+
moreRemaining: "还有 {count} 项",
|
|
835
|
+
showMore: "显示更多",
|
|
836
|
+
showAll: "全部显示",
|
|
837
|
+
showRest: "显示剩余 {count} 项"
|
|
838
|
+
},
|
|
839
|
+
"en-US": {
|
|
840
|
+
allFiles: "All files",
|
|
841
|
+
demoBadge: "Demo data",
|
|
842
|
+
filter: "Filter files",
|
|
843
|
+
refresh: "Refresh",
|
|
844
|
+
batch: "Download ZIP",
|
|
845
|
+
batchCount: "Download ZIP ({count})",
|
|
846
|
+
batching: "Zipping {percent}%",
|
|
847
|
+
passwordPrompt: "This folder is password protected:",
|
|
848
|
+
passwordPlaceholder: "Folder password",
|
|
849
|
+
open: "Open",
|
|
850
|
+
columnName: "Name",
|
|
851
|
+
columnSize: "Size",
|
|
852
|
+
columnDate: "Modified",
|
|
853
|
+
loading: "Loading…",
|
|
854
|
+
loadFailed: "Failed to load: {message}",
|
|
855
|
+
guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
|
|
856
|
+
corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
|
|
857
|
+
retry: "Retry",
|
|
858
|
+
empty: "No files",
|
|
859
|
+
preview: "Preview",
|
|
860
|
+
download: "Download",
|
|
861
|
+
loaded: "{count} loaded file(s)",
|
|
862
|
+
selected: "{count} selected",
|
|
863
|
+
unloaded: "{count} folder(s) not expanded, excluded from select-all",
|
|
864
|
+
folderFailed: "Failed to open folder: {message}",
|
|
865
|
+
demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
|
|
866
|
+
demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
|
|
867
|
+
demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
|
|
868
|
+
downloadFailed: "Download failed: {message}",
|
|
869
|
+
previewFailed: "Preview failed: {message}",
|
|
870
|
+
batchFailed: "Failed to create ZIP: {message}",
|
|
871
|
+
defaultZip: "resources",
|
|
872
|
+
clear: "Clear",
|
|
873
|
+
clearSelection: "Clear selection",
|
|
874
|
+
totalSize: "{size} total",
|
|
875
|
+
folderItems: "{count} items",
|
|
876
|
+
sortBy: "Sort",
|
|
877
|
+
selectAll: "Select all",
|
|
878
|
+
selectItem: "Select {name}",
|
|
879
|
+
expand: "Expand {name}",
|
|
880
|
+
collapse: "Collapse {name}",
|
|
881
|
+
close: "Close",
|
|
882
|
+
previous: "Previous",
|
|
883
|
+
next: "Next",
|
|
884
|
+
rotate: "Rotate",
|
|
885
|
+
actualSize: "Actual size",
|
|
886
|
+
previewUnsupported: "This file type cannot be previewed here. Download it to view.",
|
|
887
|
+
previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
|
|
888
|
+
previewTooLarge: "This file is large ({size}). Download it to view.",
|
|
889
|
+
textTruncated: "Showing the first {size} only. Download for the full file.",
|
|
890
|
+
refreshing: "Refreshing",
|
|
891
|
+
refreshFailed: "Refresh failed: {message}",
|
|
892
|
+
cancel: "Cancel",
|
|
893
|
+
passwordFor: "Password for {name}",
|
|
894
|
+
passwordWrong: "Wrong password or no permission",
|
|
895
|
+
passwordStale: "The saved password no longer works. Enter it again.",
|
|
896
|
+
rememberPassword: "Remember in this tab (cleared when the tab closes)",
|
|
897
|
+
remembered: "Remembered",
|
|
898
|
+
forgetPassword: "Forget",
|
|
899
|
+
unlockFolder: "Enter the password for {name}",
|
|
900
|
+
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
901
|
+
batchProgress: "{done} / {total} · {files} / {count} files",
|
|
902
|
+
batchDone: "Saved {name} ({count} files, {size})",
|
|
903
|
+
batchCanceled: "Download cancelled",
|
|
904
|
+
zipWarnTitle: "Large download ({size})",
|
|
905
|
+
zipWarnText: "This browser has to hold every file in memory to build the ZIP. Above {limit} it may fail or crash the page. Consider downloading in smaller batches.",
|
|
906
|
+
zipStreamTip: "Chrome and Edge save while downloading, with no size limit.",
|
|
907
|
+
zipContinue: "Download anyway",
|
|
908
|
+
zipBlockTitle: "Cannot zip {size}",
|
|
909
|
+
zipBlockText: "This browser can zip at most {limit} at once. Select fewer files.",
|
|
910
|
+
gotIt: "OK",
|
|
911
|
+
moreRemaining: "{count} more",
|
|
912
|
+
showMore: "Show more",
|
|
913
|
+
showAll: "Show all",
|
|
914
|
+
showRest: "Show the remaining {count}"
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
function formatLabel(template, values = {}) {
|
|
918
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
|
|
444
919
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
function fillPreviewUrl(scheme, args) {
|
|
451
|
-
return scheme.replace(/\$name/g, args.name).replace(/\$[eb_]*durl/g, (placeholder) => applyUrlOps(placeholder, args.downloadUrl)).replace(/\$[eb_]*url/g, (placeholder) => applyUrlOps(placeholder, args.rawUrl));
|
|
920
|
+
function resolveLabels(locale = "zh-CN", overrides) {
|
|
921
|
+
return {
|
|
922
|
+
...fileListLocales[locale] ?? fileListLocales["zh-CN"],
|
|
923
|
+
...overrides
|
|
924
|
+
};
|
|
452
925
|
}
|
|
453
926
|
// src/client/PreviewDialog.vue
|
|
454
927
|
const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
@@ -482,11 +955,12 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
482
955
|
],
|
|
483
956
|
setup(__props, { emit: __emit }) {
|
|
484
957
|
/**
|
|
485
|
-
*
|
|
958
|
+
* File preview in a native <dialog>, without leaving the page.
|
|
486
959
|
*
|
|
487
|
-
* image / video / audio
|
|
488
|
-
* pdf
|
|
489
|
-
*
|
|
960
|
+
* image / video / audio: native elements; text: the first `maxTextBytes`
|
|
961
|
+
* through a Range request; pdf: the first matching OpenList `iframe_previews`
|
|
962
|
+
* viewer, else the browser's reader through a Blob. Anything else (Office
|
|
963
|
+
* included), failures, oversized PDFs and demo mode: file info + download.
|
|
490
964
|
*/
|
|
491
965
|
const props = __props;
|
|
492
966
|
function t(key, values) {
|
|
@@ -636,20 +1110,49 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
636
1110
|
_push(`<div class="fl-preview-panel"><header class="fl-preview-head"><span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><div class="fl-preview-title"><span class="fl-preview-name">${ssrInterpolate(__props.file.name)}</span><span class="fl-preview-meta">${ssrInterpolate(unref(formatSize)(__props.file.size))} · ${ssrInterpolate(unref(formatDate)(__props.file.modified))}</span></div>`);
|
|
637
1111
|
if (__props.total > 1 || mode.value === "image" && !busy.value) {
|
|
638
1112
|
_push(`<div class="fl-preview-tools fl-preview-nav">`);
|
|
639
|
-
if (__props.total > 1)
|
|
640
|
-
|
|
1113
|
+
if (__props.total > 1) {
|
|
1114
|
+
_push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}>`);
|
|
1115
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1116
|
+
name: "previous",
|
|
1117
|
+
size: 18
|
|
1118
|
+
}, null, _parent));
|
|
1119
|
+
_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"))}>`);
|
|
1120
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1121
|
+
name: "next",
|
|
1122
|
+
size: 18
|
|
1123
|
+
}, null, _parent));
|
|
1124
|
+
_push(`</button><!--]-->`);
|
|
1125
|
+
} else _push(`<!---->`);
|
|
641
1126
|
if (mode.value === "image" && !busy.value) {
|
|
642
|
-
_push(`<!--[--><button class="fl-preview-tool fl-preview-rotate" type="button"${ssrRenderAttr("aria-label", t("rotate"))}${ssrRenderAttr("title", t("rotate"))}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1127
|
+
_push(`<!--[--><button class="fl-preview-tool fl-preview-rotate" type="button"${ssrRenderAttr("aria-label", t("rotate"))}${ssrRenderAttr("title", t("rotate"))}>`);
|
|
1128
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1129
|
+
name: "refresh",
|
|
1130
|
+
size: 17
|
|
1131
|
+
}, null, _parent));
|
|
1132
|
+
_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"))}>`);
|
|
1133
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1134
|
+
name: actualSize.value ? "zoomOut" : "zoomIn",
|
|
1135
|
+
size: 17
|
|
1136
|
+
}, null, _parent));
|
|
1137
|
+
_push(`</button><!--]-->`);
|
|
646
1138
|
} else _push(`<!---->`);
|
|
647
1139
|
_push(`</div>`);
|
|
648
1140
|
} else _push(`<!---->`);
|
|
649
1141
|
_push(`<div class="fl-preview-tools fl-preview-actions">`);
|
|
650
|
-
if (!__props.demo)
|
|
651
|
-
|
|
652
|
-
|
|
1142
|
+
if (!__props.demo) {
|
|
1143
|
+
_push(`<button class="fl-preview-tool fl-preview-download" type="button"${ssrRenderAttr("aria-label", t("download"))}${ssrRenderAttr("title", t("download"))}>`);
|
|
1144
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1145
|
+
name: "download",
|
|
1146
|
+
size: 17
|
|
1147
|
+
}, null, _parent));
|
|
1148
|
+
_push(`</button>`);
|
|
1149
|
+
} else _push(`<!---->`);
|
|
1150
|
+
_push(`<button class="fl-preview-tool fl-preview-close" type="button" autofocus${ssrRenderAttr("aria-label", t("close"))}${ssrRenderAttr("title", t("close"))}>`);
|
|
1151
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1152
|
+
name: "close",
|
|
1153
|
+
size: 18
|
|
1154
|
+
}, null, _parent));
|
|
1155
|
+
_push(`</button></div></header><div class="fl-preview-body"${ssrRenderAttr("data-mode", busy.value ? "loading" : mode.value)}>`);
|
|
653
1156
|
if (busy.value) _push(`<div class="fl-preview-status" role="status">${ssrInterpolate(t("loading"))}</div>`);
|
|
654
1157
|
else if (mode.value === "image") _push(`<img class="${ssrRenderClass([{ "fl-preview-actual": actualSize.value }, "fl-preview-image"])}"${ssrRenderAttr("src", __props.src)}${ssrRenderAttr("alt", __props.file.name)} style="${ssrRenderStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 })}">`);
|
|
655
1158
|
else if (mode.value === "video") _push(`<video class="fl-preview-video"${ssrRenderAttr("src", __props.src)} controls playsinline preload="metadata"></video>`);
|
|
@@ -662,8 +1165,14 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
662
1165
|
} else if (mode.value === "pdf") _push(`<iframe class="fl-preview-frame"${ssrRenderAttr("src", __props.viewer || blobUrl.value)}${ssrRenderAttr("title", __props.file.name)}></iframe>`);
|
|
663
1166
|
else {
|
|
664
1167
|
_push(`<div class="fl-preview-info"><span class="fl-type fl-preview-badge"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><p class="fl-preview-message">${ssrInterpolate(infoMessage.value)}</p>`);
|
|
665
|
-
if (!__props.demo)
|
|
666
|
-
|
|
1168
|
+
if (!__props.demo) {
|
|
1169
|
+
_push(`<button class="fl-btn fl-btn-primary" type="button">`);
|
|
1170
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1171
|
+
name: "download",
|
|
1172
|
+
size: 15
|
|
1173
|
+
}, null, _parent));
|
|
1174
|
+
_push(` ${ssrInterpolate(t("download"))}</button>`);
|
|
1175
|
+
} else _push(`<!---->`);
|
|
667
1176
|
_push(`</div>`);
|
|
668
1177
|
}
|
|
669
1178
|
_push(`</div></div>`);
|
|
@@ -678,6 +1187,67 @@ _sfc_main$1.setup = (props, ctx) => {
|
|
|
678
1187
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/PreviewDialog.vue");
|
|
679
1188
|
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
|
|
680
1189
|
};
|
|
1190
|
+
// src/client/tree.ts
|
|
1191
|
+
function newLock(prompting = false, error = "") {
|
|
1192
|
+
return {
|
|
1193
|
+
prompting,
|
|
1194
|
+
input: "",
|
|
1195
|
+
error,
|
|
1196
|
+
remember: false,
|
|
1197
|
+
busy: false
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function toNodes(items, parent, password) {
|
|
1201
|
+
return items.map((item) => ({
|
|
1202
|
+
...item,
|
|
1203
|
+
path: joinPath(parent.path, item.name),
|
|
1204
|
+
depth: parent.depth + 1,
|
|
1205
|
+
expanded: false,
|
|
1206
|
+
loaded: false,
|
|
1207
|
+
loading: false,
|
|
1208
|
+
children: [],
|
|
1209
|
+
password,
|
|
1210
|
+
zipPath: [...parent.zipPath, item.name]
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
function sortTree(nodes, key, ascending) {
|
|
1214
|
+
nodes.sort((a, b) => compareNodes(a, b, key, ascending));
|
|
1215
|
+
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
|
|
1216
|
+
}
|
|
1217
|
+
/** Loaded files at or under a node. */
|
|
1218
|
+
function descendantFiles(node) {
|
|
1219
|
+
return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
|
|
1220
|
+
}
|
|
1221
|
+
/** Last path segment, for labels. */
|
|
1222
|
+
function sourceName(path) {
|
|
1223
|
+
return path.split("/").filter(Boolean).pop() || path;
|
|
1224
|
+
}
|
|
1225
|
+
/** Loaded file count, size and selected count for every folder, in one pass. */
|
|
1226
|
+
function folderStats(roots, selected) {
|
|
1227
|
+
const stats = /* @__PURE__ */ new Map();
|
|
1228
|
+
const walk = (node) => {
|
|
1229
|
+
if (!node.is_dir) return {
|
|
1230
|
+
files: 1,
|
|
1231
|
+
bytes: totalSize([node]),
|
|
1232
|
+
selected: selected.has(node.path) ? 1 : 0
|
|
1233
|
+
};
|
|
1234
|
+
const total = {
|
|
1235
|
+
files: 0,
|
|
1236
|
+
bytes: 0,
|
|
1237
|
+
selected: 0
|
|
1238
|
+
};
|
|
1239
|
+
for (const child of node.children) {
|
|
1240
|
+
const part = walk(child);
|
|
1241
|
+
total.files += part.files;
|
|
1242
|
+
total.bytes += part.bytes;
|
|
1243
|
+
total.selected += part.selected;
|
|
1244
|
+
}
|
|
1245
|
+
stats.set(node.path, total);
|
|
1246
|
+
return total;
|
|
1247
|
+
};
|
|
1248
|
+
roots.forEach(walk);
|
|
1249
|
+
return stats;
|
|
1250
|
+
}
|
|
681
1251
|
// src/client/passwords.ts
|
|
682
1252
|
/**
|
|
683
1253
|
* Folder passwords remembered for the current browser tab.
|
|
@@ -715,12 +1285,6 @@ function write(base, map) {
|
|
|
715
1285
|
function normalise(path) {
|
|
716
1286
|
return path.replace(/\/+$/, "") || "/";
|
|
717
1287
|
}
|
|
718
|
-
/** Whether `path` is `folder` itself or inside it. */
|
|
719
|
-
function isWithin(path, folder) {
|
|
720
|
-
const p = normalise(path);
|
|
721
|
-
const f = normalise(folder);
|
|
722
|
-
return f === "/" || p === f || p.startsWith(`${f}/`);
|
|
723
|
-
}
|
|
724
1288
|
/** The closest remembered folder at or above `path`, if any. */
|
|
725
1289
|
function findRememberedPassword(base, path) {
|
|
726
1290
|
let best = null;
|
|
@@ -733,6 +1297,12 @@ function findRememberedPassword(base, path) {
|
|
|
733
1297
|
}
|
|
734
1298
|
return best;
|
|
735
1299
|
}
|
|
1300
|
+
function rememberFolderPassword(base, path, password) {
|
|
1301
|
+
write(base, {
|
|
1302
|
+
...read(base),
|
|
1303
|
+
[normalise(path)]: password
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
736
1306
|
function forgetFolderPassword(base, path) {
|
|
737
1307
|
const map = read(base);
|
|
738
1308
|
delete map[normalise(path)];
|
|
@@ -742,6 +1312,73 @@ function forgetFolderPassword(base, path) {
|
|
|
742
1312
|
function rememberedFolders(base) {
|
|
743
1313
|
return Object.keys(read(base));
|
|
744
1314
|
}
|
|
1315
|
+
// src/client/usePasswords.ts
|
|
1316
|
+
/**
|
|
1317
|
+
* The one place that decides which folder password to send.
|
|
1318
|
+
*
|
|
1319
|
+
* Order: typed on this page for this exact folder → the item's own password
|
|
1320
|
+
* (site-configured, or inherited from the folder it was listed with) → typed
|
|
1321
|
+
* for an enclosing folder → remembered in this tab → the component default.
|
|
1322
|
+
*/
|
|
1323
|
+
function usePasswords(options) {
|
|
1324
|
+
/** Typed on this page; not necessarily remembered. Survives refreshes. */
|
|
1325
|
+
const typed = /* @__PURE__ */ new Map();
|
|
1326
|
+
/** Remembered folders, mirrored from sessionStorage so templates react. */
|
|
1327
|
+
const remembered = ref(/* @__PURE__ */ new Set());
|
|
1328
|
+
function sync() {
|
|
1329
|
+
remembered.value = new Set(options.enabled() ? rememberedFolders(options.scope()) : []);
|
|
1330
|
+
}
|
|
1331
|
+
function typedAbove(path) {
|
|
1332
|
+
let best = "";
|
|
1333
|
+
let bestFolder = "";
|
|
1334
|
+
for (const [folder, password] of typed) if (isWithin(path, folder) && folder.length > bestFolder.length) {
|
|
1335
|
+
best = password;
|
|
1336
|
+
bestFolder = folder;
|
|
1337
|
+
}
|
|
1338
|
+
return best;
|
|
1339
|
+
}
|
|
1340
|
+
function resolve(path, own = "") {
|
|
1341
|
+
const exact = typed.get(path);
|
|
1342
|
+
if (exact) return { password: exact };
|
|
1343
|
+
if (own) return { password: own };
|
|
1344
|
+
const above = typedAbove(path);
|
|
1345
|
+
if (above) return { password: above };
|
|
1346
|
+
const stored = options.enabled() ? findRememberedPassword(options.scope(), path) : null;
|
|
1347
|
+
if (stored) return {
|
|
1348
|
+
password: stored.password,
|
|
1349
|
+
rememberedAt: stored.path
|
|
1350
|
+
};
|
|
1351
|
+
return { password: options.fallback() };
|
|
1352
|
+
}
|
|
1353
|
+
/** A password worked: keep it for this page, and in this tab when asked. */
|
|
1354
|
+
function accept(path, password, remember) {
|
|
1355
|
+
typed.set(path, password);
|
|
1356
|
+
if (remember && options.enabled()) {
|
|
1357
|
+
rememberFolderPassword(options.scope(), path, password);
|
|
1358
|
+
sync();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function forget(path) {
|
|
1362
|
+
typed.delete(path);
|
|
1363
|
+
forgetFolderPassword(options.scope(), path);
|
|
1364
|
+
sync();
|
|
1365
|
+
}
|
|
1366
|
+
/** A remembered password stopped working: drop it. Returns whether one was dropped. */
|
|
1367
|
+
function dropStale(rememberedAt) {
|
|
1368
|
+
if (!rememberedAt) return false;
|
|
1369
|
+
forgetFolderPassword(options.scope(), rememberedAt);
|
|
1370
|
+
sync();
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
remembered,
|
|
1375
|
+
sync,
|
|
1376
|
+
resolve,
|
|
1377
|
+
accept,
|
|
1378
|
+
forget,
|
|
1379
|
+
dropStale
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
745
1382
|
// src/client/FileList.vue
|
|
746
1383
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
747
1384
|
__name: "FileList",
|
|
@@ -761,24 +1398,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
761
1398
|
type: Boolean,
|
|
762
1399
|
default: true
|
|
763
1400
|
},
|
|
1401
|
+
pageSize: { default: 200 },
|
|
1402
|
+
zipWarnSize: {},
|
|
1403
|
+
zipMaxSize: {},
|
|
764
1404
|
locale: { default: "zh-CN" },
|
|
765
1405
|
labels: {}
|
|
766
1406
|
},
|
|
767
1407
|
setup(__props) {
|
|
768
1408
|
/**
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* <FileList /> // 演示模式
|
|
1409
|
+
* OpenList file browser: folder tree, batch ZIP, folder passwords and an
|
|
1410
|
+
* in-page preview. OpenList access is in api.ts (demo.ts when `base` is empty),
|
|
1411
|
+
* password choice in usePasswords.ts, tree helpers in tree.ts and downloads in
|
|
1412
|
+
* download.ts; this component wires them to the UI.
|
|
774
1413
|
*/
|
|
775
1414
|
const props = __props;
|
|
776
1415
|
const text = computed(() => resolveLabels(props.locale, props.labels));
|
|
777
1416
|
function t(key, values) {
|
|
778
1417
|
return formatLabel(text.value[key], values);
|
|
779
1418
|
}
|
|
1419
|
+
/** Prefix for element ids, unique per component instance. */
|
|
1420
|
+
const uid = useId();
|
|
780
1421
|
const demo = computed(() => !props.base);
|
|
781
|
-
const
|
|
1422
|
+
const api = computed(() => demo.value ? createDemoApi() : createOpenListApi(props.base, props.token));
|
|
1423
|
+
const canRemember = computed(() => props.rememberPassword !== false);
|
|
1424
|
+
const passwords = usePasswords({
|
|
1425
|
+
scope: () => props.base.replace(/\/+$/, "") || "demo",
|
|
1426
|
+
enabled: () => canRemember.value,
|
|
1427
|
+
fallback: () => props.password
|
|
1428
|
+
});
|
|
1429
|
+
const remembered = passwords.remembered;
|
|
782
1430
|
const rootEl = ref();
|
|
783
1431
|
const roots = ref([]);
|
|
784
1432
|
const loadingRoot = ref(false);
|
|
@@ -786,110 +1434,46 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
786
1434
|
const refreshing = ref(false);
|
|
787
1435
|
let refreshSeq = 0;
|
|
788
1436
|
/** Single-path mode: the listed folder itself needs a password. */
|
|
789
|
-
const rootLock =
|
|
790
|
-
active: false,
|
|
791
|
-
input: "",
|
|
792
|
-
error: "",
|
|
793
|
-
remember: false,
|
|
794
|
-
busy: false
|
|
795
|
-
});
|
|
796
|
-
/** Password the visitor typed for the listed folder (single-path mode). */
|
|
797
|
-
const rootPassword = ref("");
|
|
1437
|
+
const rootLock = ref(null);
|
|
798
1438
|
const keyword = ref("");
|
|
799
1439
|
const selected = ref(/* @__PURE__ */ new Set());
|
|
800
1440
|
const toast = ref("");
|
|
801
1441
|
const sortKey = ref("name");
|
|
802
1442
|
const sortAsc = ref(true);
|
|
803
1443
|
const siteTitle = ref("");
|
|
804
|
-
/** OpenList's public `iframe_previews` setting (JSON text). */
|
|
805
1444
|
const iframePreviews = ref("");
|
|
806
1445
|
const batchBusy = ref(false);
|
|
807
|
-
const
|
|
808
|
-
|
|
1446
|
+
const zipProgress = ref(null);
|
|
1447
|
+
let zipAbort = null;
|
|
809
1448
|
let toastTimer;
|
|
810
|
-
|
|
811
|
-
|
|
1449
|
+
/** The folder listed in single-path mode, as the parent of the top rows. */
|
|
1450
|
+
const listedFolder = computed(() => ({
|
|
1451
|
+
path: props.path,
|
|
1452
|
+
depth: -1,
|
|
1453
|
+
zipPath: []
|
|
1454
|
+
}));
|
|
1455
|
+
function showToast(message) {
|
|
1456
|
+
toast.value = message;
|
|
812
1457
|
clearTimeout(toastTimer);
|
|
813
1458
|
toastTimer = setTimeout(() => toast.value = "", 3200);
|
|
814
1459
|
}
|
|
815
|
-
function errorText(error) {
|
|
816
|
-
return error instanceof Error ? error.message : String(error);
|
|
817
|
-
}
|
|
818
|
-
function isPasswordError(error) {
|
|
819
|
-
return /password|密码/i.test(errorText(error));
|
|
820
|
-
}
|
|
821
|
-
function isNetworkError(error) {
|
|
822
|
-
return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
|
|
823
|
-
}
|
|
824
1460
|
/** A failure toast; browser network errors are usually CORS, so say where to look. */
|
|
825
1461
|
function failureMessage(key, error) {
|
|
826
1462
|
const message = t(key, { message: errorText(error) });
|
|
827
|
-
return isNetworkError(error) ? `${message} ${t("
|
|
828
|
-
}
|
|
829
|
-
/** Total size for display: an empty total reads `0 B`, not `-`. */
|
|
830
|
-
function formatTotal(bytes) {
|
|
831
|
-
return bytes > 0 ? formatSize(bytes) : "0 B";
|
|
832
|
-
}
|
|
833
|
-
const canRemember = computed(() => props.rememberPassword !== false);
|
|
834
|
-
const passwordScope = computed(() => baseUrl.value || "demo");
|
|
835
|
-
/** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
|
|
836
|
-
const typedPasswords = /* @__PURE__ */ new Map();
|
|
837
|
-
function typedPasswordFor(path) {
|
|
838
|
-
let best = "";
|
|
839
|
-
let bestPath = "";
|
|
840
|
-
for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
|
|
841
|
-
best = password;
|
|
842
|
-
bestPath = folder;
|
|
843
|
-
}
|
|
844
|
-
return best;
|
|
845
|
-
}
|
|
846
|
-
/** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
|
|
847
|
-
const rememberedPaths = ref(/* @__PURE__ */ new Set());
|
|
848
|
-
function syncRemembered() {
|
|
849
|
-
rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
|
|
850
|
-
}
|
|
851
|
-
function storedPassword(path) {
|
|
852
|
-
return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Password for an item: its own or inherited one first (site-configured or
|
|
856
|
-
* typed), then one remembered in this tab, then the component's `password`.
|
|
857
|
-
*/
|
|
858
|
-
function passwordFor(node) {
|
|
859
|
-
if (node.password) return { password: node.password };
|
|
860
|
-
const typed = typedPasswordFor(node.path);
|
|
861
|
-
if (typed) return { password: typed };
|
|
862
|
-
const stored = storedPassword(node.path);
|
|
863
|
-
if (stored) return {
|
|
864
|
-
password: stored.password,
|
|
865
|
-
rememberedAt: stored.path
|
|
866
|
-
};
|
|
867
|
-
return { password: props.password };
|
|
868
|
-
}
|
|
869
|
-
function rootPasswordFor() {
|
|
870
|
-
if (rootPassword.value) return { password: rootPassword.value };
|
|
871
|
-
const stored = storedPassword(props.path);
|
|
872
|
-
if (stored) return {
|
|
873
|
-
password: stored.password,
|
|
874
|
-
rememberedAt: stored.path
|
|
875
|
-
};
|
|
876
|
-
return { password: props.password };
|
|
1463
|
+
return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
|
|
877
1464
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (!path) return false;
|
|
881
|
-
forgetFolderPassword(passwordScope.value, path);
|
|
882
|
-
syncRemembered();
|
|
883
|
-
return true;
|
|
1465
|
+
function sortNodes(nodes) {
|
|
1466
|
+
sortTree(nodes, sortKey.value, sortAsc.value);
|
|
884
1467
|
}
|
|
885
1468
|
function focusUnlock(path) {
|
|
886
1469
|
nextTick(() => {
|
|
887
|
-
|
|
1470
|
+
rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
|
|
888
1471
|
});
|
|
889
1472
|
}
|
|
890
|
-
/** Show
|
|
1473
|
+
/** Show a folder's same-row input; focus only on user actions, never on page load. */
|
|
891
1474
|
function startPrompt(node, focus) {
|
|
892
|
-
node.
|
|
1475
|
+
node.lock ??= newLock();
|
|
1476
|
+
node.lock.prompting = true;
|
|
893
1477
|
if (focus) focusUnlock(node.path);
|
|
894
1478
|
}
|
|
895
1479
|
function unlockErrorText(error) {
|
|
@@ -897,289 +1481,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
897
1481
|
if (error === "stale") return t("passwordStale");
|
|
898
1482
|
return "";
|
|
899
1483
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
size: 0,
|
|
905
|
-
is_dir: true,
|
|
906
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
907
|
-
},
|
|
908
|
-
{
|
|
909
|
-
name: "exams",
|
|
910
|
-
size: 0,
|
|
911
|
-
is_dir: true,
|
|
912
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
913
|
-
},
|
|
914
|
-
{
|
|
915
|
-
name: "materials",
|
|
916
|
-
size: 0,
|
|
917
|
-
is_dir: true,
|
|
918
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
919
|
-
},
|
|
920
|
-
{
|
|
921
|
-
name: "notes",
|
|
922
|
-
size: 0,
|
|
923
|
-
is_dir: true,
|
|
924
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
name: "slides",
|
|
928
|
-
size: 0,
|
|
929
|
-
is_dir: true,
|
|
930
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
name: "加密示例(密码 1234)",
|
|
934
|
-
size: 0,
|
|
935
|
-
is_dir: true,
|
|
936
|
-
modified: "2026-10-20T18:00:00+08:00"
|
|
937
|
-
}
|
|
938
|
-
],
|
|
939
|
-
"/assignments": [{
|
|
940
|
-
name: "线代作业参考答案",
|
|
941
|
-
size: 0,
|
|
942
|
-
is_dir: true,
|
|
943
|
-
modified: "2026-10-12T21:30:00+08:00"
|
|
944
|
-
}, {
|
|
945
|
-
name: "作业提交说明.pdf",
|
|
946
|
-
size: 186432,
|
|
947
|
-
is_dir: false,
|
|
948
|
-
modified: "2026-09-03T10:15:00+08:00"
|
|
949
|
-
}],
|
|
950
|
-
"/assignments/线代作业参考答案": [
|
|
951
|
-
{
|
|
952
|
-
name: "第1次作业参考答案.doc",
|
|
953
|
-
size: 412672,
|
|
954
|
-
is_dir: false,
|
|
955
|
-
modified: "2026-09-15T22:41:00+08:00"
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
name: "第2次作业参考答案.pdf",
|
|
959
|
-
size: 1048576,
|
|
960
|
-
is_dir: false,
|
|
961
|
-
modified: "2026-09-22T22:03:00+08:00"
|
|
962
|
-
},
|
|
963
|
-
{
|
|
964
|
-
name: "第3次作业参考答案.pdf",
|
|
965
|
-
size: 986112,
|
|
966
|
-
is_dir: false,
|
|
967
|
-
modified: "2026-09-29T21:57:00+08:00"
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
name: "第4次作业参考答案.pdf",
|
|
971
|
-
size: 1572864,
|
|
972
|
-
is_dir: false,
|
|
973
|
-
modified: "2026-10-13T22:10:00+08:00"
|
|
974
|
-
}
|
|
975
|
-
],
|
|
976
|
-
"/exams": [
|
|
977
|
-
{
|
|
978
|
-
name: "2023 期末试题和答案.pdf",
|
|
979
|
-
size: 2345678,
|
|
980
|
-
is_dir: false,
|
|
981
|
-
modified: "2025-01-18T09:20:00+08:00"
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
name: "2024 期中试题.pdf",
|
|
985
|
-
size: 1234567,
|
|
986
|
-
is_dir: false,
|
|
987
|
-
modified: "2025-11-02T14:08:00+08:00"
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
name: "代数与几何-多年合集-试题和答案.pdf",
|
|
991
|
-
size: 18874368,
|
|
992
|
-
is_dir: false,
|
|
993
|
-
modified: "2025-01-18T09:25:00+08:00"
|
|
994
|
-
}
|
|
995
|
-
],
|
|
996
|
-
"/materials": [
|
|
997
|
-
{
|
|
998
|
-
name: "线性代数及其应用(Lay 第5版).pdf",
|
|
999
|
-
size: 48231424,
|
|
1000
|
-
is_dir: false,
|
|
1001
|
-
modified: "2024-07-29T16:44:00+08:00"
|
|
1002
|
-
},
|
|
1003
|
-
{
|
|
1004
|
-
name: "Introduction to Linear Algebra(Strang).pdf",
|
|
1005
|
-
size: 36700160,
|
|
1006
|
-
is_dir: false,
|
|
1007
|
-
modified: "2024-07-29T16:50:00+08:00"
|
|
1008
|
-
},
|
|
1009
|
-
{
|
|
1010
|
-
name: "疑难解答与习题指导.zip",
|
|
1011
|
-
size: 9437184,
|
|
1012
|
-
is_dir: false,
|
|
1013
|
-
modified: "2024-08-05T11:12:00+08:00"
|
|
1014
|
-
}
|
|
1015
|
-
],
|
|
1016
|
-
"/notes": [{
|
|
1017
|
-
name: "线性代数笔记(106页).pdf",
|
|
1018
|
-
size: 12582912,
|
|
1019
|
-
is_dir: false,
|
|
1020
|
-
modified: "2025-06-30T20:31:00+08:00"
|
|
1021
|
-
}, {
|
|
1022
|
-
name: "期末复习提纲.docx",
|
|
1023
|
-
size: 245760,
|
|
1024
|
-
is_dir: false,
|
|
1025
|
-
modified: "2026-01-05T19:02:00+08:00"
|
|
1026
|
-
}],
|
|
1027
|
-
"/加密示例(密码 1234)": [{
|
|
1028
|
-
name: "期末答案(教师版).pdf",
|
|
1029
|
-
size: 2621440,
|
|
1030
|
-
is_dir: false,
|
|
1031
|
-
modified: "2026-10-20T18:00:00+08:00"
|
|
1032
|
-
}, {
|
|
1033
|
-
name: "成绩登记表.xlsx",
|
|
1034
|
-
size: 48128,
|
|
1035
|
-
is_dir: false,
|
|
1036
|
-
modified: "2026-10-21T09:12:00+08:00"
|
|
1037
|
-
}],
|
|
1038
|
-
"/slides": [{
|
|
1039
|
-
name: "第一章 行列式.pptx",
|
|
1040
|
-
size: 3145728,
|
|
1041
|
-
is_dir: false,
|
|
1042
|
-
modified: "2026-09-08T13:26:00+08:00"
|
|
1043
|
-
}, {
|
|
1044
|
-
name: "第二章 矩阵.pptx",
|
|
1045
|
-
size: 4194304,
|
|
1046
|
-
is_dir: false,
|
|
1047
|
-
modified: "2026-09-15T13:30:00+08:00"
|
|
1048
|
-
}]
|
|
1049
|
-
};
|
|
1050
|
-
/** Demo-only folder passwords, so the demo can show the password flow. */
|
|
1051
|
-
const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
|
|
1052
|
-
class ApiError extends Error {
|
|
1053
|
-
code;
|
|
1054
|
-
constructor(message, code) {
|
|
1055
|
-
super(message);
|
|
1056
|
-
this.code = code;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
/** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
|
|
1060
|
-
function authHeaders(json) {
|
|
1061
|
-
return {
|
|
1062
|
-
...json ? { "Content-Type": "application/json" } : {},
|
|
1063
|
-
...props.token ? { Authorization: props.token } : {}
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
async function request(apiPath, body) {
|
|
1067
|
-
const response = await fetch(`${baseUrl.value}${apiPath}`, {
|
|
1068
|
-
method: body === void 0 ? "GET" : "POST",
|
|
1069
|
-
headers: authHeaders(body !== void 0),
|
|
1070
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1071
|
-
});
|
|
1072
|
-
const payload = await response.json().catch(() => null);
|
|
1073
|
-
if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
|
|
1074
|
-
if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
|
|
1075
|
-
return payload.data;
|
|
1076
|
-
}
|
|
1077
|
-
async function listDir(path, password) {
|
|
1078
|
-
if (demo.value) {
|
|
1079
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
1080
|
-
const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
|
|
1081
|
-
if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
|
|
1082
|
-
return DEMO_TREE[path] ?? [];
|
|
1083
|
-
}
|
|
1084
|
-
return (await request("/api/fs/list", {
|
|
1085
|
-
path,
|
|
1086
|
-
password,
|
|
1087
|
-
page: 1,
|
|
1088
|
-
per_page: 0,
|
|
1089
|
-
refresh: false
|
|
1090
|
-
}))?.content ?? [];
|
|
1091
|
-
}
|
|
1092
|
-
function absoluteUrl(raw) {
|
|
1093
|
-
if (!raw) return "";
|
|
1094
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
|
|
1095
|
-
const root = baseUrl.value || (typeof window !== "undefined" ? window.location.origin : "");
|
|
1096
|
-
try {
|
|
1097
|
-
return new URL(raw, `${root}/`).href;
|
|
1098
|
-
} catch {
|
|
1099
|
-
return raw;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
async function resolveUrl(node) {
|
|
1103
|
-
if (demo.value) return "";
|
|
1104
|
-
if (node.raw_url) return absoluteUrl(node.raw_url);
|
|
1105
|
-
const cached = downloadCache.get(node.path);
|
|
1106
|
-
if (cached) return cached;
|
|
1107
|
-
let url = "";
|
|
1108
|
-
try {
|
|
1109
|
-
url = absoluteUrl((await request("/api/fs/get", {
|
|
1110
|
-
path: node.path,
|
|
1111
|
-
password: passwordFor(node).password
|
|
1112
|
-
}))?.raw_url ?? "");
|
|
1113
|
-
} catch {
|
|
1114
|
-
url = "";
|
|
1115
|
-
}
|
|
1116
|
-
if (!url) url = directUrl(node);
|
|
1117
|
-
downloadCache.set(node.path, url);
|
|
1118
|
-
return url;
|
|
1119
|
-
}
|
|
1120
|
-
/** OpenList's signed `/d/` download link for a file. */
|
|
1121
|
-
function directUrl(node) {
|
|
1122
|
-
return `${baseUrl.value}/d${encodePath(node.path)}${node.sign ? `?sign=${encodeURIComponent(node.sign)}` : ""}`;
|
|
1123
|
-
}
|
|
1124
|
-
async function loadSiteTitle() {
|
|
1125
|
-
if (demo.value) return;
|
|
1126
|
-
try {
|
|
1127
|
-
const data = await request("/api/public/settings");
|
|
1128
|
-
siteTitle.value = data?.site_title || data?.title || "";
|
|
1129
|
-
iframePreviews.value = data?.iframe_previews || "";
|
|
1130
|
-
} catch {
|
|
1131
|
-
siteTitle.value = "";
|
|
1132
|
-
iframePreviews.value = "";
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
function sortTree(nodes) {
|
|
1136
|
-
nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
|
|
1137
|
-
for (const node of nodes) if (node.children.length) sortTree(node.children);
|
|
1138
|
-
}
|
|
1139
|
-
function toNodes(items, parent, depth, password = "", zipParent = []) {
|
|
1140
|
-
return items.map((item) => ({
|
|
1141
|
-
...item,
|
|
1142
|
-
path: joinPath(parent, item.name),
|
|
1143
|
-
depth,
|
|
1144
|
-
expanded: false,
|
|
1145
|
-
loaded: false,
|
|
1146
|
-
loading: false,
|
|
1147
|
-
children: [],
|
|
1148
|
-
password,
|
|
1149
|
-
zipPath: [...zipParent, item.name]
|
|
1150
|
-
}));
|
|
1151
|
-
}
|
|
1152
|
-
function resortAll() {
|
|
1153
|
-
if (props.sources.length) {
|
|
1154
|
-
for (const root of roots.value) if (root.children.length) sortTree(root.children);
|
|
1155
|
-
} else sortTree(roots.value);
|
|
1484
|
+
async function fetchChildren(node, password) {
|
|
1485
|
+
node.children = toNodes(await api.value.list(node.path, password), node, password);
|
|
1486
|
+
sortNodes(node.children);
|
|
1487
|
+
node.loaded = true;
|
|
1156
1488
|
}
|
|
1157
|
-
function
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1489
|
+
async function listRoot(password) {
|
|
1490
|
+
const nodes = toNodes(await api.value.list(props.path, password), listedFolder.value, password);
|
|
1491
|
+
sortNodes(nodes);
|
|
1492
|
+
return nodes;
|
|
1160
1493
|
}
|
|
1161
1494
|
/**
|
|
1162
|
-
* Load a folder's children. A password error
|
|
1495
|
+
* Load a folder's children. A password error locks the folder instead of
|
|
1163
1496
|
* toasting; `prompt` also shows its same-row input (and focuses it).
|
|
1164
1497
|
*/
|
|
1165
1498
|
async function loadChildren(node, prompt = false) {
|
|
1166
1499
|
if (node.loaded) return true;
|
|
1167
|
-
if (node.
|
|
1500
|
+
if (node.lock) {
|
|
1168
1501
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1169
1502
|
return false;
|
|
1170
1503
|
}
|
|
1171
1504
|
node.loading = true;
|
|
1172
|
-
const { password, rememberedAt } =
|
|
1505
|
+
const { password, rememberedAt } = passwords.resolve(node.path, node.password);
|
|
1173
1506
|
try {
|
|
1174
|
-
|
|
1175
|
-
sortTree(node.children);
|
|
1176
|
-
node.loaded = true;
|
|
1507
|
+
await fetchChildren(node, password);
|
|
1177
1508
|
return true;
|
|
1178
1509
|
} catch (error) {
|
|
1179
1510
|
if (isPasswordError(error)) {
|
|
1180
|
-
node.locked = true;
|
|
1181
1511
|
node.expanded = false;
|
|
1182
|
-
node.
|
|
1512
|
+
node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1183
1513
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1184
1514
|
} else showToast(failureMessage("folderFailed", error));
|
|
1185
1515
|
return false;
|
|
@@ -1194,44 +1524,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1194
1524
|
}
|
|
1195
1525
|
async function loadSourceRoot(source, expanded) {
|
|
1196
1526
|
const path = source.path;
|
|
1197
|
-
const
|
|
1527
|
+
const own = source.password ?? "";
|
|
1198
1528
|
let item = {
|
|
1199
1529
|
name: sourceName(path),
|
|
1200
1530
|
size: 0,
|
|
1201
1531
|
is_dir: !looksLikeFile(path),
|
|
1202
1532
|
modified: ""
|
|
1203
1533
|
};
|
|
1204
|
-
let
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
password: stored.password,
|
|
1214
|
-
rememberedAt: stored.path
|
|
1215
|
-
} : {
|
|
1216
|
-
password: props.password,
|
|
1217
|
-
rememberedAt: void 0
|
|
1218
|
-
};
|
|
1219
|
-
})();
|
|
1220
|
-
try {
|
|
1221
|
-
const data = await request("/api/fs/get", {
|
|
1222
|
-
path,
|
|
1223
|
-
password
|
|
1224
|
-
});
|
|
1225
|
-
item = {
|
|
1226
|
-
...item,
|
|
1227
|
-
...data
|
|
1228
|
-
};
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
if (isPasswordError(error)) {
|
|
1231
|
-
locked = true;
|
|
1232
|
-
unlockError = dropStale(rememberedAt) ? "stale" : "";
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1534
|
+
let lock;
|
|
1535
|
+
const { password, rememberedAt } = passwords.resolve(path, own);
|
|
1536
|
+
try {
|
|
1537
|
+
item = {
|
|
1538
|
+
...item,
|
|
1539
|
+
...await api.value.get(path, password)
|
|
1540
|
+
};
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (isPasswordError(error)) lock = newLock(expanded, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1235
1543
|
}
|
|
1236
1544
|
const name = source.title || item.name || sourceName(path);
|
|
1237
1545
|
return {
|
|
@@ -1239,17 +1547,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1239
1547
|
name,
|
|
1240
1548
|
path,
|
|
1241
1549
|
depth: 0,
|
|
1242
|
-
expanded,
|
|
1550
|
+
expanded: expanded && !lock,
|
|
1243
1551
|
loaded: false,
|
|
1244
1552
|
loading: false,
|
|
1245
1553
|
children: [],
|
|
1246
|
-
password:
|
|
1247
|
-
configuredPassword: configured,
|
|
1554
|
+
password: own,
|
|
1248
1555
|
isRoot: true,
|
|
1249
|
-
|
|
1250
|
-
unresolved:
|
|
1251
|
-
unlockError,
|
|
1252
|
-
prompting: locked && expanded,
|
|
1556
|
+
lock,
|
|
1557
|
+
unresolved: !!lock,
|
|
1253
1558
|
zipPath: [item.is_dir ? name : item.name || sourceName(path)]
|
|
1254
1559
|
};
|
|
1255
1560
|
}
|
|
@@ -1268,20 +1573,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1268
1573
|
let nodes;
|
|
1269
1574
|
if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
|
|
1270
1575
|
else {
|
|
1271
|
-
const { password, rememberedAt } =
|
|
1272
|
-
let items;
|
|
1576
|
+
const { password, rememberedAt } = passwords.resolve(props.path);
|
|
1273
1577
|
try {
|
|
1274
|
-
|
|
1578
|
+
nodes = await listRoot(password);
|
|
1275
1579
|
} catch (error) {
|
|
1276
|
-
|
|
1277
|
-
throw error;
|
|
1580
|
+
throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
|
|
1278
1581
|
}
|
|
1279
|
-
nodes = toNodes(items, props.path, 0, rootPassword.value);
|
|
1280
|
-
sortTree(nodes);
|
|
1281
1582
|
}
|
|
1282
1583
|
const open = async (list) => {
|
|
1283
1584
|
for (const node of list) {
|
|
1284
|
-
|
|
1585
|
+
const wanted = node.expanded || expandedPaths?.has(node.path);
|
|
1586
|
+
if (!node.is_dir || node.lock || !wanted) continue;
|
|
1285
1587
|
node.expanded = await loadChildren(node, "show");
|
|
1286
1588
|
if (node.expanded && expandedPaths) await open(node.children);
|
|
1287
1589
|
}
|
|
@@ -1289,6 +1591,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1289
1591
|
await open(nodes);
|
|
1290
1592
|
return nodes;
|
|
1291
1593
|
}
|
|
1594
|
+
function lockRoot(error, fallback) {
|
|
1595
|
+
roots.value = [];
|
|
1596
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
1597
|
+
rootLock.value = newLock(true, passwords.dropStale(error.rememberedAt) ? "stale" : fallback);
|
|
1598
|
+
}
|
|
1292
1599
|
/** First load, or a retry after an error: shows the loading state. */
|
|
1293
1600
|
async function loadRoot() {
|
|
1294
1601
|
refreshSeq += 1;
|
|
@@ -1297,54 +1604,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1297
1604
|
errorMsg.value = "";
|
|
1298
1605
|
try {
|
|
1299
1606
|
roots.value = await buildTree();
|
|
1300
|
-
|
|
1607
|
+
shown.clear();
|
|
1608
|
+
rootLock.value = null;
|
|
1301
1609
|
selected.value = /* @__PURE__ */ new Set();
|
|
1302
|
-
|
|
1610
|
+
api.value.clearCache();
|
|
1303
1611
|
} catch (error) {
|
|
1304
|
-
if (error instanceof RootLockedError)
|
|
1305
|
-
|
|
1306
|
-
rootLock.active = true;
|
|
1307
|
-
rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
|
|
1308
|
-
} else errorMsg.value = errorText(error);
|
|
1612
|
+
if (error instanceof RootLockedError) lockRoot(error, "");
|
|
1613
|
+
else errorMsg.value = errorText(error);
|
|
1309
1614
|
} finally {
|
|
1310
1615
|
loadingRoot.value = false;
|
|
1311
1616
|
}
|
|
1312
1617
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
for (const node of nodes) {
|
|
1319
|
-
if (!matches(node)) continue;
|
|
1320
|
-
out.push(node);
|
|
1321
|
-
if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children);
|
|
1322
|
-
}
|
|
1323
|
-
};
|
|
1324
|
-
walk(roots.value);
|
|
1325
|
-
return out;
|
|
1326
|
-
});
|
|
1327
|
-
function descendantFiles(node) {
|
|
1328
|
-
if (!node.is_dir) return [node];
|
|
1329
|
-
return node.children.flatMap(descendantFiles);
|
|
1330
|
-
}
|
|
1331
|
-
function isChecked(node) {
|
|
1332
|
-
const files = descendantFiles(node);
|
|
1333
|
-
return files.length > 0 && files.every((file) => selected.value.has(file.path));
|
|
1334
|
-
}
|
|
1335
|
-
function isIndeterminate(node) {
|
|
1336
|
-
const files = descendantFiles(node);
|
|
1337
|
-
const hit = files.filter((file) => selected.value.has(file.path)).length;
|
|
1338
|
-
return hit > 0 && hit < files.length;
|
|
1339
|
-
}
|
|
1340
|
-
const allFiles = computed(() => roots.value.flatMap(descendantFiles));
|
|
1341
|
-
const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
|
|
1342
|
-
const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
|
|
1343
|
-
const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
|
|
1344
|
-
const selectedBytes = computed(() => totalSize(selectedFiles.value));
|
|
1345
|
-
const loadedBytes = computed(() => totalSize(allFiles.value));
|
|
1346
|
-
function folderFiles(node) {
|
|
1347
|
-
return node.is_dir && node.loaded ? descendantFiles(node) : [];
|
|
1618
|
+
function applySort(key, ascending) {
|
|
1619
|
+
sortKey.value = key;
|
|
1620
|
+
sortAsc.value = ascending;
|
|
1621
|
+
if (props.sources.length) roots.value.forEach((root) => sortNodes(root.children));
|
|
1622
|
+
else sortNodes(roots.value);
|
|
1348
1623
|
}
|
|
1349
1624
|
/** Compact sort control for narrow containers, where header cells are hidden. */
|
|
1350
1625
|
const sortOptions = computed(() => [
|
|
@@ -1365,19 +1640,65 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1365
1640
|
get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
|
|
1366
1641
|
set: (value) => {
|
|
1367
1642
|
const [key, direction] = value.split(":");
|
|
1368
|
-
|
|
1369
|
-
sortAsc.value = direction !== "desc";
|
|
1370
|
-
resortAll();
|
|
1643
|
+
applySort(key, direction !== "desc");
|
|
1371
1644
|
}
|
|
1372
1645
|
});
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1646
|
+
/** A “show more” row closing a folder whose children are cut at `pageSize`. */
|
|
1647
|
+
const shown = reactive(/* @__PURE__ */ new Map());
|
|
1648
|
+
function limitOf(key) {
|
|
1649
|
+
const size = Math.max(0, Math.floor(props.pageSize ?? 200));
|
|
1650
|
+
return size ? shown.get(key) ?? size : Number.POSITIVE_INFINITY;
|
|
1651
|
+
}
|
|
1652
|
+
function isMore(row) {
|
|
1653
|
+
return "more" in row;
|
|
1654
|
+
}
|
|
1655
|
+
const rows = computed(() => {
|
|
1656
|
+
const out = [];
|
|
1657
|
+
const query = keyword.value.trim().toLowerCase();
|
|
1658
|
+
const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
|
|
1659
|
+
const walk = (nodes, key, depth) => {
|
|
1660
|
+
const list = nodes.filter(matches);
|
|
1661
|
+
const limit = query ? list.length : Math.min(list.length, limitOf(key));
|
|
1662
|
+
for (const node of list.slice(0, limit)) {
|
|
1663
|
+
out.push(node);
|
|
1664
|
+
if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children, node.path, node.depth + 1);
|
|
1665
|
+
}
|
|
1666
|
+
if (limit < list.length) out.push({
|
|
1667
|
+
more: true,
|
|
1668
|
+
key,
|
|
1669
|
+
depth,
|
|
1670
|
+
remaining: list.length - limit
|
|
1671
|
+
});
|
|
1672
|
+
};
|
|
1673
|
+
walk(roots.value, props.sources.length ? "" : props.path, 0);
|
|
1674
|
+
return out;
|
|
1675
|
+
});
|
|
1676
|
+
const visible = computed(() => rows.value.filter((row) => !isMore(row)));
|
|
1677
|
+
const stats = computed(() => folderStats(roots.value, selected.value));
|
|
1678
|
+
const noStats = {
|
|
1679
|
+
files: 0,
|
|
1680
|
+
bytes: 0,
|
|
1681
|
+
selected: 0
|
|
1682
|
+
};
|
|
1683
|
+
function statsOf(node) {
|
|
1684
|
+
return stats.value.get(node.path) ?? noStats;
|
|
1685
|
+
}
|
|
1686
|
+
function isChecked(node) {
|
|
1687
|
+
if (!node.is_dir) return selected.value.has(node.path);
|
|
1688
|
+
const { files, selected: hit } = statsOf(node);
|
|
1689
|
+
return files > 0 && hit === files;
|
|
1690
|
+
}
|
|
1691
|
+
function isIndeterminate(node) {
|
|
1692
|
+
if (!node.is_dir) return false;
|
|
1693
|
+
const { files, selected: hit } = statsOf(node);
|
|
1694
|
+
return hit > 0 && hit < files;
|
|
1695
|
+
}
|
|
1696
|
+
const allFiles = computed(() => roots.value.flatMap(descendantFiles));
|
|
1697
|
+
const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
|
|
1698
|
+
const allChecked = computed(() => allFiles.value.length > 0 && selectedFiles.value.length === allFiles.value.length);
|
|
1699
|
+
const someChecked = computed(() => !allChecked.value && selectedFiles.value.length > 0);
|
|
1700
|
+
function fileUrlOf(node) {
|
|
1701
|
+
return api.value.fileUrl(node, passwords.resolve(node.path, node.password).password);
|
|
1381
1702
|
}
|
|
1382
1703
|
async function download(node) {
|
|
1383
1704
|
if (node.is_dir) return;
|
|
@@ -1386,11 +1707,57 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1386
1707
|
return;
|
|
1387
1708
|
}
|
|
1388
1709
|
try {
|
|
1389
|
-
|
|
1710
|
+
openUrl(await fileUrlOf(node));
|
|
1390
1711
|
} catch (error) {
|
|
1391
1712
|
showToast(t("downloadFailed", { message: errorText(error) }));
|
|
1392
1713
|
}
|
|
1393
1714
|
}
|
|
1715
|
+
const zipNotice = ref(null);
|
|
1716
|
+
/** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
|
|
1717
|
+
const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
|
|
1718
|
+
function zipName() {
|
|
1719
|
+
return zipFileName(props.title || t("defaultZip"));
|
|
1720
|
+
}
|
|
1721
|
+
async function zipInMemory(files) {
|
|
1722
|
+
const name = zipName();
|
|
1723
|
+
await runZip(files, async (options) => saveBlob(await zipToBlob(files, options), name), name);
|
|
1724
|
+
}
|
|
1725
|
+
function confirmZip() {
|
|
1726
|
+
const notice = zipNotice.value;
|
|
1727
|
+
zipNotice.value = null;
|
|
1728
|
+
if (notice?.kind === "warn") zipInMemory(notice.files);
|
|
1729
|
+
}
|
|
1730
|
+
async function runZip(files, run, name) {
|
|
1731
|
+
const controller = new AbortController();
|
|
1732
|
+
zipAbort = controller;
|
|
1733
|
+
batchBusy.value = true;
|
|
1734
|
+
zipProgress.value = null;
|
|
1735
|
+
try {
|
|
1736
|
+
await run({
|
|
1737
|
+
urlOf: fileUrlOf,
|
|
1738
|
+
signal: controller.signal,
|
|
1739
|
+
onProgress: (progress) => zipProgress.value = progress
|
|
1740
|
+
});
|
|
1741
|
+
showToast(t("batchDone", {
|
|
1742
|
+
name,
|
|
1743
|
+
count: files.length,
|
|
1744
|
+
size: formatSize(totalSize(files))
|
|
1745
|
+
}));
|
|
1746
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
showToast(controller.signal.aborted ? t("batchCanceled") : t("batchFailed", { message: errorText(error) }));
|
|
1749
|
+
} finally {
|
|
1750
|
+
zipAbort = null;
|
|
1751
|
+
batchBusy.value = false;
|
|
1752
|
+
zipProgress.value = null;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const zipPercent = computed(() => {
|
|
1756
|
+
const progress = zipProgress.value;
|
|
1757
|
+
if (!progress) return 0;
|
|
1758
|
+
const ratio = progress.totalBytes ? progress.bytes / progress.totalBytes : progress.totalFiles ? progress.files / progress.totalFiles : 0;
|
|
1759
|
+
return Math.min(100, Math.floor(ratio * 100));
|
|
1760
|
+
});
|
|
1394
1761
|
/** Only types the dialog can actually render get a preview button. */
|
|
1395
1762
|
function canPreview(node) {
|
|
1396
1763
|
return !node.is_dir && previewKind(node.name) !== "none";
|
|
@@ -1414,17 +1781,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1414
1781
|
if (demo.value || !canPreview(node)) return;
|
|
1415
1782
|
previewResolving.value = true;
|
|
1416
1783
|
try {
|
|
1417
|
-
const url = await
|
|
1784
|
+
const url = await fileUrlOf(node);
|
|
1418
1785
|
if (seq !== previewSeq) return;
|
|
1419
1786
|
previewSrc.value = url;
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1787
|
+
const viewer = previewKind(node.name) === "pdf" ? iframePreviewsFor(node.name, iframePreviews.value)[0] : void 0;
|
|
1788
|
+
if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
|
|
1789
|
+
name: node.name,
|
|
1790
|
+
rawUrl: url,
|
|
1791
|
+
downloadUrl: api.value.directUrl(node)
|
|
1792
|
+
});
|
|
1428
1793
|
} catch (error) {
|
|
1429
1794
|
if (seq === previewSeq) previewError.value = errorText(error);
|
|
1430
1795
|
} finally {
|
|
@@ -1441,9 +1806,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1441
1806
|
previewNode.value = null;
|
|
1442
1807
|
}
|
|
1443
1808
|
const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
|
|
1809
|
+
async function loadSettings() {
|
|
1810
|
+
const settings = await api.value.settings();
|
|
1811
|
+
siteTitle.value = settings.title;
|
|
1812
|
+
iframePreviews.value = settings.iframePreviews;
|
|
1813
|
+
}
|
|
1444
1814
|
const stickyTop = ref(0);
|
|
1445
1815
|
function measureStickyTop() {
|
|
1446
|
-
if (typeof document === "undefined") return;
|
|
1447
1816
|
let top = 0;
|
|
1448
1817
|
for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
|
|
1449
1818
|
const style = getComputedStyle(el);
|
|
@@ -1453,13 +1822,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1453
1822
|
stickyTop.value = top;
|
|
1454
1823
|
}
|
|
1455
1824
|
onMounted(() => {
|
|
1456
|
-
|
|
1825
|
+
passwords.sync();
|
|
1457
1826
|
loadRoot();
|
|
1458
|
-
|
|
1827
|
+
loadSettings();
|
|
1459
1828
|
measureStickyTop();
|
|
1460
1829
|
window.addEventListener("resize", measureStickyTop, { passive: true });
|
|
1461
1830
|
});
|
|
1462
1831
|
onBeforeUnmount(() => {
|
|
1832
|
+
zipAbort?.abort();
|
|
1463
1833
|
window.removeEventListener("resize", measureStickyTop);
|
|
1464
1834
|
clearTimeout(toastTimer);
|
|
1465
1835
|
});
|
|
@@ -1474,98 +1844,213 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1474
1844
|
style: { "--fl-sticky-top-auto": `${stickyTop.value}px` },
|
|
1475
1845
|
"aria-busy": refreshing.value || loadingRoot.value
|
|
1476
1846
|
}, _attrs))}><div class="fl-bar">`);
|
|
1477
|
-
if (
|
|
1847
|
+
if (batchBusy.value) {
|
|
1848
|
+
_push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("batching", { percent: zipPercent.value }))}</span>`);
|
|
1849
|
+
if (zipProgress.value) _push(`<span class="fl-selection-size">${ssrInterpolate(t("batchProgress", {
|
|
1850
|
+
done: unref(formatSize)(zipProgress.value.bytes),
|
|
1851
|
+
total: unref(formatSize)(zipProgress.value.totalBytes),
|
|
1852
|
+
files: zipProgress.value.files,
|
|
1853
|
+
count: zipProgress.value.totalFiles
|
|
1854
|
+
}))}</span>`);
|
|
1855
|
+
else _push(`<!---->`);
|
|
1856
|
+
_push(`<span class="fl-zip-bar" style="${ssrRenderStyle({ width: `${zipPercent.value}%` })}" aria-hidden="true"></span></div>`);
|
|
1857
|
+
} else if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(selectedFiles.value)) }))}</span></div>`);
|
|
1478
1858
|
else {
|
|
1479
1859
|
_push(`<div class="fl-title"><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
|
|
1480
1860
|
if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
|
|
1481
1861
|
else _push(`<!---->`);
|
|
1482
1862
|
_push(`</div>`);
|
|
1483
1863
|
}
|
|
1484
|
-
if (selected.value.size
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1864
|
+
if (selected.value.size && !batchBusy.value) {
|
|
1865
|
+
_push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}>`);
|
|
1866
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1867
|
+
class: "fl-clear-icon",
|
|
1868
|
+
name: "close",
|
|
1869
|
+
size: 18
|
|
1870
|
+
}, null, _parent));
|
|
1871
|
+
_push(`<span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
|
|
1872
|
+
} else _push(`<!---->`);
|
|
1873
|
+
_push(`<div class="fl-tools"><label class="fl-search">`);
|
|
1874
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1875
|
+
name: "search",
|
|
1876
|
+
size: 15
|
|
1877
|
+
}, null, _parent));
|
|
1878
|
+
_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"))}>`);
|
|
1879
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1880
|
+
name: "refresh",
|
|
1881
|
+
size: 15
|
|
1882
|
+
}, null, _parent));
|
|
1883
|
+
_push(`</button></div>`);
|
|
1884
|
+
if (batchBusy.value) _push(`<button class="fl-btn fl-zip-cancel" type="button">${ssrInterpolate(t("cancel"))}</button>`);
|
|
1885
|
+
else if (selected.value.size) {
|
|
1886
|
+
_push(`<button class="fl-btn fl-btn-primary" type="button">`);
|
|
1887
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1888
|
+
name: "download",
|
|
1889
|
+
size: 15
|
|
1890
|
+
}, null, _parent));
|
|
1891
|
+
_push(` ${ssrInterpolate(t("batch"))}</button>`);
|
|
1892
|
+
} else _push(`<!---->`);
|
|
1489
1893
|
_push(`</div><div class="fl-row fl-head">`);
|
|
1490
1894
|
if (refreshing.value) _push(`<span class="fl-progress" aria-hidden="true"></span>`);
|
|
1491
1895
|
else _push(`<!---->`);
|
|
1492
|
-
_push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(
|
|
1896
|
+
_push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><!--[-->`);
|
|
1897
|
+
ssrRenderList([
|
|
1898
|
+
"name",
|
|
1899
|
+
"size",
|
|
1900
|
+
"modified"
|
|
1901
|
+
], (column) => {
|
|
1902
|
+
_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>`);
|
|
1903
|
+
});
|
|
1904
|
+
_push(`<!--]--><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
|
|
1493
1905
|
ssrRenderList(sortOptions.value, (option) => {
|
|
1494
1906
|
_push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
|
|
1495
1907
|
});
|
|
1496
1908
|
_push(`<!--]--></select></label></span></div>`);
|
|
1497
1909
|
if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
|
|
1498
|
-
else if (rootLock.
|
|
1499
|
-
_push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.error }, "fl-unlock-root"])}"
|
|
1500
|
-
|
|
1910
|
+
else if (rootLock.value) {
|
|
1911
|
+
_push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.value.error }, "fl-unlock-root"])}">`);
|
|
1912
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1913
|
+
class: "fl-unlock-lock",
|
|
1914
|
+
name: "lock"
|
|
1915
|
+
}, null, _parent));
|
|
1916
|
+
_push(`<label class="fl-unlock-label"${ssrRenderAttr("for", `${unref(uid)}-root`)}>${ssrInterpolate(t("passwordPrompt"))}</label>`);
|
|
1917
|
+
if (rootLock.value.error) _push(`<span${ssrRenderAttr("id", `${unref(uid)}-root-error`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(rootLock.value.error))}</span>`);
|
|
1501
1918
|
else _push(`<!---->`);
|
|
1502
|
-
_push(`<span class="fl-unlock"><input${ssrRenderAttr("id",
|
|
1503
|
-
if (canRemember.value)
|
|
1504
|
-
|
|
1505
|
-
|
|
1919
|
+
_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)}>`);
|
|
1920
|
+
if (canRemember.value) {
|
|
1921
|
+
_push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", rootLock.value.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}>`);
|
|
1922
|
+
_push(ssrRenderComponent(_sfc_main$2, { name: "key" }, null, _parent));
|
|
1923
|
+
_push(`</button>`);
|
|
1924
|
+
} else _push(`<!---->`);
|
|
1925
|
+
_push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(rootLock.value.busy || !rootLock.value.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button></span></form>`);
|
|
1506
1926
|
} else if (errorMsg.value) {
|
|
1507
1927
|
_push(`<div class="fl-status fl-error">${ssrInterpolate(t("loadFailed", { message: errorMsg.value }))} `);
|
|
1508
1928
|
if (/guest/i.test(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("guestHint"))}</p>`);
|
|
1509
1929
|
else _push(`<!---->`);
|
|
1510
|
-
if (
|
|
1930
|
+
if (unref(isNetworkError)(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("corsHint"))}</p>`);
|
|
1511
1931
|
else _push(`<!---->`);
|
|
1512
1932
|
_push(`<button class="fl-btn" type="button">${ssrInterpolate(t("retry"))}</button></div>`);
|
|
1513
1933
|
} else if (visible.value.length === 0) _push(`<div class="fl-status">${ssrInterpolate(t("empty"))}</div>`);
|
|
1514
1934
|
else _push(`<!---->`);
|
|
1515
1935
|
_push(`<!--[-->`);
|
|
1516
|
-
ssrRenderList(
|
|
1517
|
-
_push(
|
|
1518
|
-
|
|
1519
|
-
"fl-row-
|
|
1520
|
-
"fl-
|
|
1521
|
-
"fl-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1936
|
+
ssrRenderList(rows.value, (node, index) => {
|
|
1937
|
+
_push(`<!--[-->`);
|
|
1938
|
+
if (isMore(node)) {
|
|
1939
|
+
_push(`<div class="fl-row fl-more" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><span class="fl-indent" aria-hidden="true"></span><span class="fl-more-count">${ssrInterpolate(t("moreRemaining", { count: node.remaining }))}</span>`);
|
|
1940
|
+
if (node.remaining > (__props.pageSize || 0)) _push(`<!--[--><button class="fl-more-btn" type="button">${ssrInterpolate(t("showMore"))}<span class="fl-more-page">${ssrInterpolate(` ${t("folderItems", { count: __props.pageSize })}`)}</span></button><button class="fl-more-btn fl-more-all" type="button">${ssrInterpolate(t("showAll"))}</button><!--]-->`);
|
|
1941
|
+
else _push(`<button class="fl-more-btn" type="button">${ssrInterpolate(t("showRest", { count: node.remaining }))}</button>`);
|
|
1942
|
+
_push(`</span></div>`);
|
|
1943
|
+
} else {
|
|
1944
|
+
_push(`<div class="${ssrRenderClass([{
|
|
1945
|
+
"fl-row-dir": node.is_dir,
|
|
1946
|
+
"fl-row-root": node.isRoot,
|
|
1947
|
+
"fl-row-selected": !node.is_dir && selected.value.has(node.path),
|
|
1948
|
+
"fl-row-locked": !!node.lock,
|
|
1949
|
+
"fl-row-prompting": node.lock?.prompting,
|
|
1950
|
+
"fl-row-error": node.lock?.prompting && !!node.lock.error
|
|
1951
|
+
}, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock || batchBusy.value) ? " 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>`);
|
|
1952
|
+
if (node.lock) {
|
|
1953
|
+
_push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}>`);
|
|
1954
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1955
|
+
name: "lock",
|
|
1956
|
+
size: 14
|
|
1957
|
+
}, null, _parent));
|
|
1958
|
+
_push(`</button>`);
|
|
1959
|
+
} else if (node.is_dir) {
|
|
1960
|
+
_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)}>`);
|
|
1961
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1962
|
+
name: "next",
|
|
1963
|
+
size: 14
|
|
1964
|
+
}, null, _parent));
|
|
1965
|
+
_push(`</button>`);
|
|
1966
|
+
} else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
|
|
1967
|
+
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>`);
|
|
1968
|
+
else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
|
|
1969
|
+
_push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}">${ssrInterpolate(node.name)}</span><span class="fl-meta">`);
|
|
1970
|
+
if (!node.is_dir) _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
|
|
1971
|
+
else if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))} · ${ssrInterpolate(unref(formatSize)(statsOf(node).bytes))}<!--]-->`);
|
|
1534
1972
|
else _push(`<!---->`);
|
|
1535
|
-
_push(
|
|
1536
|
-
|
|
1537
|
-
_push(`</span></span>`);
|
|
1538
|
-
if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))}</span>`);
|
|
1539
|
-
else _push(`<!---->`);
|
|
1540
|
-
if (node.locked && node.prompting && node.unlockError) _push(`<span${ssrRenderAttr("id", `fl-unlock-error-${node.path}`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(node.unlockError))}</span>`);
|
|
1541
|
-
else _push(`<!---->`);
|
|
1542
|
-
if (!node.locked && rememberedPaths.value.has(node.path)) _push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg> ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
|
|
1543
|
-
else _push(`<!---->`);
|
|
1544
|
-
if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
|
|
1545
|
-
else _push(`<!---->`);
|
|
1546
|
-
_push(`</span>`);
|
|
1547
|
-
if (node.locked && node.prompting) {
|
|
1548
|
-
_push(`<form class="fl-unlock fl-unlock-row"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><input${ssrRenderAttr("value", node.unlockInput)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", node.path)}${ssrRenderAttr("placeholder", node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordFor", { name: node.name }))}${ssrRenderAttr("aria-invalid", !!node.unlockError)}${ssrRenderAttr("aria-describedby", node.unlockError ? `fl-unlock-error-${node.path}` : void 0)}>`);
|
|
1549
|
-
if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", !!node.unlockRemember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
|
|
1973
|
+
_push(`</span></span>`);
|
|
1974
|
+
if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))}</span>`);
|
|
1550
1975
|
else _push(`<!---->`);
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
_push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
|
|
1976
|
+
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>`);
|
|
1977
|
+
else _push(`<!---->`);
|
|
1978
|
+
if (!node.lock && unref(remembered).has(node.path)) {
|
|
1979
|
+
_push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}>`);
|
|
1980
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1981
|
+
name: "key",
|
|
1982
|
+
size: 11
|
|
1983
|
+
}, null, _parent));
|
|
1984
|
+
_push(` ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
|
|
1561
1985
|
} else _push(`<!---->`);
|
|
1562
|
-
_push(
|
|
1986
|
+
if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
|
|
1987
|
+
else _push(`<!---->`);
|
|
1988
|
+
_push(`</span>`);
|
|
1989
|
+
if (node.lock?.prompting) {
|
|
1990
|
+
_push(`<form class="fl-unlock fl-unlock-row">`);
|
|
1991
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
1992
|
+
class: "fl-unlock-lock",
|
|
1993
|
+
name: "lock",
|
|
1994
|
+
size: 18
|
|
1995
|
+
}, null, _parent));
|
|
1996
|
+
_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)}>`);
|
|
1997
|
+
if (canRemember.value) {
|
|
1998
|
+
_push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", node.lock.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}>`);
|
|
1999
|
+
_push(ssrRenderComponent(_sfc_main$2, { name: "key" }, null, _parent));
|
|
2000
|
+
_push(`</button>`);
|
|
2001
|
+
} else _push(`<!---->`);
|
|
2002
|
+
_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"))}>`);
|
|
2003
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
2004
|
+
name: "close",
|
|
2005
|
+
size: 14
|
|
2006
|
+
}, null, _parent));
|
|
2007
|
+
_push(`</button></form>`);
|
|
2008
|
+
} else {
|
|
2009
|
+
_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">`);
|
|
2010
|
+
if (node.lock) {
|
|
2011
|
+
_push(`<button class="fl-action fl-arrow-end fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}>`);
|
|
2012
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
2013
|
+
name: "lock",
|
|
2014
|
+
size: 18
|
|
2015
|
+
}, null, _parent));
|
|
2016
|
+
_push(`</button>`);
|
|
2017
|
+
} else if (node.is_dir) {
|
|
2018
|
+
_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)}>`);
|
|
2019
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
2020
|
+
name: "next",
|
|
2021
|
+
size: 18
|
|
2022
|
+
}, null, _parent));
|
|
2023
|
+
_push(`</button>`);
|
|
2024
|
+
} else {
|
|
2025
|
+
_push(`<!--[-->`);
|
|
2026
|
+
if (canPreview(node)) {
|
|
2027
|
+
_push(`<button class="fl-action fl-preview-btn" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}>`);
|
|
2028
|
+
_push(ssrRenderComponent(_sfc_main$2, { name: "eye" }, null, _parent));
|
|
2029
|
+
_push(`</button>`);
|
|
2030
|
+
} else _push(`<!---->`);
|
|
2031
|
+
_push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}>`);
|
|
2032
|
+
_push(ssrRenderComponent(_sfc_main$2, { name: "download" }, null, _parent));
|
|
2033
|
+
_push(`</button><!--]-->`);
|
|
2034
|
+
}
|
|
2035
|
+
_push(`</span><!--]-->`);
|
|
2036
|
+
}
|
|
2037
|
+
_push(`</div>`);
|
|
1563
2038
|
}
|
|
1564
|
-
_push(
|
|
2039
|
+
_push(`<!--]-->`);
|
|
1565
2040
|
});
|
|
1566
|
-
_push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size:
|
|
2041
|
+
_push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) }))}</span></div>`);
|
|
1567
2042
|
if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
|
|
1568
2043
|
else _push(`<!---->`);
|
|
2044
|
+
_push(ssrRenderComponent(_sfc_main$3, {
|
|
2045
|
+
open: !!zipNotice.value,
|
|
2046
|
+
title: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnTitle" : "zipBlockTitle", { size: unref(formatSize)(zipNotice.value.size) }) : "",
|
|
2047
|
+
text: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnText" : "zipBlockText", { limit: unref(formatSize)(zipNotice.value.limit) }) : "",
|
|
2048
|
+
tip: zipTip.value,
|
|
2049
|
+
"confirm-label": zipNotice.value?.kind === "warn" ? t("zipContinue") : void 0,
|
|
2050
|
+
"cancel-label": zipNotice.value?.kind === "warn" ? t("cancel") : t("gotIt"),
|
|
2051
|
+
onConfirm: confirmZip,
|
|
2052
|
+
onClose: ($event) => zipNotice.value = null
|
|
2053
|
+
}, null, _parent));
|
|
1569
2054
|
_push(ssrRenderComponent(_sfc_main$1, {
|
|
1570
2055
|
open: !!previewNode.value,
|
|
1571
2056
|
file: previewNode.value,
|