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
|
@@ -1,5 +1,809 @@
|
|
|
1
1
|
import "../style.css";
|
|
2
|
-
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
|
|
2
|
+
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, useId, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
|
|
3
|
+
// src/client/utils.ts
|
|
4
|
+
function joinPath(parent, name) {
|
|
5
|
+
return (parent.endsWith("/") ? parent : `${parent}/`) + name;
|
|
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
|
+
}
|
|
13
|
+
function encodePath(path) {
|
|
14
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Make one path segment safe as a ZIP entry segment: no separators,
|
|
18
|
+
* no `.` / `..` traversal, no empty names.
|
|
19
|
+
*/
|
|
20
|
+
function zipSegment(name) {
|
|
21
|
+
const cleaned = name.replace(/[\\/]/g, "_").trim();
|
|
22
|
+
if (!cleaned || cleaned === "." || cleaned === "..") return "_";
|
|
23
|
+
return cleaned;
|
|
24
|
+
}
|
|
25
|
+
/** Join path segments into a ZIP entry name, sanitising each segment. */
|
|
26
|
+
function zipEntryName(segments) {
|
|
27
|
+
return segments.map(zipSegment).join("/");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
|
|
31
|
+
* The suffix goes before the extension of the last segment. Adds the result to `used`.
|
|
32
|
+
*/
|
|
33
|
+
function uniqueEntryName(name, used) {
|
|
34
|
+
let candidate = name;
|
|
35
|
+
if (used.has(candidate)) {
|
|
36
|
+
const slash = name.lastIndexOf("/");
|
|
37
|
+
const dir = name.slice(0, slash + 1);
|
|
38
|
+
const base = name.slice(slash + 1);
|
|
39
|
+
const dot = base.lastIndexOf(".");
|
|
40
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
41
|
+
const ext = dot > 0 ? base.slice(dot) : "";
|
|
42
|
+
let index = 2;
|
|
43
|
+
do {
|
|
44
|
+
candidate = `${dir}${stem} (${index})${ext}`;
|
|
45
|
+
index += 1;
|
|
46
|
+
} while (used.has(candidate));
|
|
47
|
+
}
|
|
48
|
+
used.add(candidate);
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
/** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
|
|
52
|
+
function formatSize(size) {
|
|
53
|
+
if (!Number.isFinite(size) || size < 0) return "-";
|
|
54
|
+
const units = [
|
|
55
|
+
"B",
|
|
56
|
+
"KB",
|
|
57
|
+
"MB",
|
|
58
|
+
"GB",
|
|
59
|
+
"TB"
|
|
60
|
+
];
|
|
61
|
+
let index = 0;
|
|
62
|
+
let value = size;
|
|
63
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
64
|
+
value /= 1024;
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
if (index === 0) return `${Math.round(value)} B`;
|
|
68
|
+
return `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
|
|
69
|
+
}
|
|
70
|
+
function formatDate(iso) {
|
|
71
|
+
if (!iso) return "-";
|
|
72
|
+
const date = new Date(iso);
|
|
73
|
+
if (Number.isNaN(date.getTime())) return iso.slice(0, 10);
|
|
74
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
75
|
+
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
|
|
76
|
+
}
|
|
77
|
+
/** Lower-case extension (letters and digits only), or `''`. */
|
|
78
|
+
function extension(name) {
|
|
79
|
+
const match = name.match(/\.([a-z0-9]+)$/i);
|
|
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();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One table for every known extension: the badge family (`FileKind`) and how
|
|
88
|
+
* the preview dialog shows it (`PreviewKind`; Office and the like are `none`).
|
|
89
|
+
*/
|
|
90
|
+
const EXTENSIONS = [
|
|
91
|
+
[
|
|
92
|
+
"doc",
|
|
93
|
+
"text",
|
|
94
|
+
"txt md markdown"
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"doc",
|
|
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"
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"pdf",
|
|
118
|
+
"pdf",
|
|
119
|
+
"pdf"
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"image",
|
|
123
|
+
"image",
|
|
124
|
+
"png jpg jpeg gif webp svg bmp ico avif"
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"image",
|
|
128
|
+
"none",
|
|
129
|
+
"heic"
|
|
130
|
+
],
|
|
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"
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"code",
|
|
153
|
+
"text",
|
|
154
|
+
"js mjs cjs ts tsx jsx vue json html htm css scss less py java kt c h cpp hpp cs go rs rb php sh bat ps1 sql yml yaml xml toml ini conf properties"
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"other",
|
|
158
|
+
"text",
|
|
159
|
+
"log srt vtt ass lrc gitignore env"
|
|
160
|
+
]
|
|
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
|
+
});
|
|
167
|
+
/** Rough file family, used to colour the type badge. */
|
|
168
|
+
function fileKind(name) {
|
|
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";
|
|
177
|
+
}
|
|
178
|
+
/** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
|
|
179
|
+
function totalSize(items) {
|
|
180
|
+
return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
|
|
181
|
+
}
|
|
182
|
+
function compareNodes(a, b, key, ascending = true) {
|
|
183
|
+
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
184
|
+
let result = 0;
|
|
185
|
+
if (key === "size") result = a.size - b.size;
|
|
186
|
+
else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
|
|
187
|
+
else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
|
|
188
|
+
return ascending ? result : -result;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Viewers from OpenList's public `iframe_previews` setting that match a file,
|
|
192
|
+
* in the order OpenList lists them. Keys are comma-separated extensions or a
|
|
193
|
+
* `/regex/flags` tested against the extension, as in OpenList's frontend.
|
|
194
|
+
*/
|
|
195
|
+
function iframePreviewsFor(name, setting) {
|
|
196
|
+
if (!setting) return [];
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(setting);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
204
|
+
const ext = extension(name);
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const [key, viewers] of Object.entries(parsed)) {
|
|
207
|
+
let hit = false;
|
|
208
|
+
if (key.startsWith("/")) {
|
|
209
|
+
const end = key.lastIndexOf("/");
|
|
210
|
+
try {
|
|
211
|
+
hit = end > 0 && new RegExp(key.slice(1, end), key.slice(end + 1)).test(ext);
|
|
212
|
+
} catch {
|
|
213
|
+
hit = false;
|
|
214
|
+
}
|
|
215
|
+
} else hit = key.split(",").map((item) => item.trim().toLowerCase()).includes(ext);
|
|
216
|
+
if (!hit || !viewers || typeof viewers !== "object") continue;
|
|
217
|
+
for (const [label, scheme] of Object.entries(viewers)) if (typeof scheme === "string" && /^https?:\/\//i.test(scheme)) out.push({
|
|
218
|
+
label,
|
|
219
|
+
scheme
|
|
220
|
+
});
|
|
221
|
+
}
|
|
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 _hoisted_1$3 = ["aria-label"];
|
|
525
|
+
const _hoisted_2$3 = {
|
|
526
|
+
key: 0,
|
|
527
|
+
class: "fl-confirm-panel"
|
|
528
|
+
};
|
|
529
|
+
const _hoisted_3$2 = { class: "fl-confirm-title" };
|
|
530
|
+
const _hoisted_4$2 = { class: "fl-confirm-text" };
|
|
531
|
+
const _hoisted_5$2 = {
|
|
532
|
+
key: 0,
|
|
533
|
+
class: "fl-confirm-text"
|
|
534
|
+
};
|
|
535
|
+
const _hoisted_6$2 = { class: "fl-confirm-actions" };
|
|
536
|
+
const _sfc_main$3 = /*@__PURE__*/ defineComponent({
|
|
537
|
+
__name: "ConfirmDialog",
|
|
538
|
+
props: {
|
|
539
|
+
open: { type: Boolean },
|
|
540
|
+
title: {},
|
|
541
|
+
text: {},
|
|
542
|
+
tip: {},
|
|
543
|
+
confirmLabel: {},
|
|
544
|
+
cancelLabel: {}
|
|
545
|
+
},
|
|
546
|
+
emits: ["confirm", "close"],
|
|
547
|
+
setup(__props, { emit: __emit }) {
|
|
548
|
+
/** Small native <dialog> for a yes / no question or a notice (bottom sheet on phones). */
|
|
549
|
+
const props = __props;
|
|
550
|
+
const emit = __emit;
|
|
551
|
+
const dialog = ref();
|
|
552
|
+
watch(() => props.open, (open) => {
|
|
553
|
+
const el = dialog.value;
|
|
554
|
+
if (!el) return;
|
|
555
|
+
if (open && !el.open) {
|
|
556
|
+
if (typeof el.showModal === "function") el.showModal();
|
|
557
|
+
else el.setAttribute("open", "");
|
|
558
|
+
} else if (!open && el.open) {
|
|
559
|
+
if (typeof el.close === "function") el.close();
|
|
560
|
+
else el.removeAttribute("open");
|
|
561
|
+
}
|
|
562
|
+
}, { flush: "post" });
|
|
563
|
+
function onNativeClose() {
|
|
564
|
+
if (props.open) emit("close");
|
|
565
|
+
}
|
|
566
|
+
function onBackdrop(event) {
|
|
567
|
+
if (event.target === dialog.value) emit("close");
|
|
568
|
+
}
|
|
569
|
+
return (_ctx, _cache) => {
|
|
570
|
+
return openBlock(), createElementBlock("dialog", {
|
|
571
|
+
ref_key: "dialog",
|
|
572
|
+
ref: dialog,
|
|
573
|
+
class: "fl-confirm",
|
|
574
|
+
"aria-label": __props.title,
|
|
575
|
+
onClose: onNativeClose,
|
|
576
|
+
onClick: onBackdrop
|
|
577
|
+
}, [__props.open ? (openBlock(), createElementBlock("div", _hoisted_2$3, [
|
|
578
|
+
createElementVNode("p", _hoisted_3$2, toDisplayString(__props.title), 1),
|
|
579
|
+
createElementVNode("p", _hoisted_4$2, toDisplayString(__props.text), 1),
|
|
580
|
+
__props.tip ? (openBlock(), createElementBlock("p", _hoisted_5$2, toDisplayString(__props.tip), 1)) : createCommentVNode("v-if", true),
|
|
581
|
+
createElementVNode("div", _hoisted_6$2, [__props.confirmLabel ? (openBlock(), createElementBlock("button", {
|
|
582
|
+
key: 0,
|
|
583
|
+
class: "fl-btn",
|
|
584
|
+
type: "button",
|
|
585
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
|
|
586
|
+
}, toDisplayString(__props.cancelLabel), 1)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
587
|
+
class: "fl-btn fl-btn-primary",
|
|
588
|
+
type: "button",
|
|
589
|
+
autofocus: "",
|
|
590
|
+
onClick: _cache[1] || (_cache[1] = ($event) => __props.confirmLabel ? emit("confirm") : emit("close"))
|
|
591
|
+
}, toDisplayString(__props.confirmLabel || __props.cancelLabel), 1)])
|
|
592
|
+
])) : createCommentVNode("v-if", true)], 40, _hoisted_1$3);
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
// src/client/download.ts
|
|
597
|
+
/** Open a file URL; OpenList serves `/p/` and `/d/` as attachments, so this downloads. */
|
|
598
|
+
function openUrl(url) {
|
|
599
|
+
const anchor = document.createElement("a");
|
|
600
|
+
anchor.href = url;
|
|
601
|
+
anchor.target = "_blank";
|
|
602
|
+
anchor.rel = "noopener";
|
|
603
|
+
document.body.appendChild(anchor);
|
|
604
|
+
anchor.click();
|
|
605
|
+
anchor.remove();
|
|
606
|
+
}
|
|
607
|
+
function saveBlob(blob, name) {
|
|
608
|
+
const href = URL.createObjectURL(blob);
|
|
609
|
+
const anchor = document.createElement("a");
|
|
610
|
+
anchor.href = href;
|
|
611
|
+
anchor.download = name;
|
|
612
|
+
document.body.appendChild(anchor);
|
|
613
|
+
anchor.click();
|
|
614
|
+
anchor.remove();
|
|
615
|
+
setTimeout(() => URL.revokeObjectURL(href), 3e4);
|
|
616
|
+
}
|
|
617
|
+
/** File-system-safe ZIP name from a title. */
|
|
618
|
+
function zipFileName(title) {
|
|
619
|
+
return `${title.replace(/[\\/:*?"<>|]/g, "_")}.zip`;
|
|
620
|
+
}
|
|
621
|
+
/** Whether the browser can write a ZIP straight into a file the user picks (Chromium). */
|
|
622
|
+
function canStreamToDisk() {
|
|
623
|
+
return typeof window !== "undefined" && window.isSecureContext && "showSaveFilePicker" in window;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Ask where to save the ZIP. Call it straight from the click, before any other
|
|
627
|
+
* `await`, or the browser refuses. Resolves `null` when the user cancels.
|
|
628
|
+
*/
|
|
629
|
+
async function pickZipFile(name) {
|
|
630
|
+
try {
|
|
631
|
+
return await window.showSaveFilePicker({
|
|
632
|
+
suggestedName: name,
|
|
633
|
+
types: [{
|
|
634
|
+
description: "ZIP",
|
|
635
|
+
accept: { "application/zip": [".zip"] }
|
|
636
|
+
}]
|
|
637
|
+
});
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof DOMException && error.name === "AbortError") return null;
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* The ZIP as a stream: files are fetched one after another and passed through
|
|
645
|
+
* as they arrive, stored without compression (most shared files are already
|
|
646
|
+
* compressed). Clashing entry names get ` (2)`, ` (3)`...
|
|
647
|
+
*/
|
|
648
|
+
async function zipStream(files, options) {
|
|
649
|
+
const { makeZip } = await import("client-zip");
|
|
650
|
+
const progress = {
|
|
651
|
+
bytes: 0,
|
|
652
|
+
totalBytes: files.reduce((sum, file) => sum + (Number.isFinite(file.size) ? file.size : 0), 0),
|
|
653
|
+
files: 0,
|
|
654
|
+
totalFiles: files.length
|
|
655
|
+
};
|
|
656
|
+
const report = () => options.onProgress({ ...progress });
|
|
657
|
+
report();
|
|
658
|
+
async function* entries() {
|
|
659
|
+
const used = /* @__PURE__ */ new Set();
|
|
660
|
+
for (const file of files) {
|
|
661
|
+
const response = await fetch(await options.urlOf(file), { signal: options.signal });
|
|
662
|
+
if (!response.ok || !response.body) throw new Error(`${file.name}:HTTP ${response.status}`);
|
|
663
|
+
const counted = response.body.pipeThrough(new TransformStream({
|
|
664
|
+
transform(chunk, controller) {
|
|
665
|
+
progress.bytes += chunk.byteLength;
|
|
666
|
+
report();
|
|
667
|
+
controller.enqueue(chunk);
|
|
668
|
+
},
|
|
669
|
+
flush() {
|
|
670
|
+
progress.files += 1;
|
|
671
|
+
report();
|
|
672
|
+
}
|
|
673
|
+
}));
|
|
674
|
+
const modified = new Date(file.modified);
|
|
675
|
+
yield {
|
|
676
|
+
name: uniqueEntryName(zipEntryName(file.zipPath), used),
|
|
677
|
+
lastModified: Number.isNaN(modified.getTime()) ? void 0 : modified,
|
|
678
|
+
input: counted
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return makeZip(entries());
|
|
683
|
+
}
|
|
684
|
+
/** Stream the ZIP into a picked file; memory use stays small whatever the size. */
|
|
685
|
+
async function zipToFile(handle, files, options) {
|
|
686
|
+
const stream = await zipStream(files, options);
|
|
687
|
+
const writable = await handle.createWritable();
|
|
688
|
+
await stream.pipeTo(writable, { signal: options.signal });
|
|
689
|
+
}
|
|
690
|
+
/** Build the ZIP in memory (browsers without file streaming). */
|
|
691
|
+
async function zipToBlob(files, options) {
|
|
692
|
+
const reader = (await zipStream(files, options)).getReader();
|
|
693
|
+
const chunks = [];
|
|
694
|
+
const abort = () => void reader.cancel(options.signal.reason).catch(() => {});
|
|
695
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
696
|
+
if (options.signal.aborted) abort();
|
|
697
|
+
try {
|
|
698
|
+
for (;;) {
|
|
699
|
+
const { done, value } = await reader.read();
|
|
700
|
+
if (done) break;
|
|
701
|
+
chunks.push(value);
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
options.signal.removeEventListener("abort", abort);
|
|
705
|
+
}
|
|
706
|
+
options.signal.throwIfAborted();
|
|
707
|
+
return new Blob(chunks, { type: "application/zip" });
|
|
708
|
+
}
|
|
709
|
+
// src/client/icons.ts
|
|
710
|
+
/** Stroke icons on a 24×24 grid, drawn by `Icon.vue`. */
|
|
711
|
+
const ICONS = {
|
|
712
|
+
lock: {
|
|
713
|
+
stroke: 2.2,
|
|
714
|
+
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"]
|
|
715
|
+
},
|
|
716
|
+
key: {
|
|
717
|
+
stroke: 2,
|
|
718
|
+
paths: [
|
|
719
|
+
"M4 15a4 4 0 1 0 8 0a4 4 0 1 0-8 0",
|
|
720
|
+
"m11 12 9-9",
|
|
721
|
+
"m17 6 3 3"
|
|
722
|
+
]
|
|
723
|
+
},
|
|
724
|
+
close: {
|
|
725
|
+
stroke: 2.2,
|
|
726
|
+
paths: ["M6 6l12 12", "M18 6 6 18"]
|
|
727
|
+
},
|
|
728
|
+
download: {
|
|
729
|
+
stroke: 2.2,
|
|
730
|
+
paths: [
|
|
731
|
+
"M12 3v12",
|
|
732
|
+
"m7 10 5 5 5-5",
|
|
733
|
+
"M5 21h14"
|
|
734
|
+
]
|
|
735
|
+
},
|
|
736
|
+
eye: {
|
|
737
|
+
stroke: 2,
|
|
738
|
+
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"]
|
|
739
|
+
},
|
|
740
|
+
refresh: {
|
|
741
|
+
stroke: 2,
|
|
742
|
+
paths: ["M21 12a9 9 0 1 1-3-6.7", "M21 4v5h-5"]
|
|
743
|
+
},
|
|
744
|
+
search: {
|
|
745
|
+
stroke: 2,
|
|
746
|
+
paths: ["M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0", "m20 20-3.5-3.5"]
|
|
747
|
+
},
|
|
748
|
+
next: {
|
|
749
|
+
stroke: 2.2,
|
|
750
|
+
paths: ["m9 6 6 6-6 6"]
|
|
751
|
+
},
|
|
752
|
+
previous: {
|
|
753
|
+
stroke: 2.2,
|
|
754
|
+
paths: ["m15 6-6 6 6 6"]
|
|
755
|
+
},
|
|
756
|
+
zoomIn: {
|
|
757
|
+
stroke: 2,
|
|
758
|
+
paths: [
|
|
759
|
+
"M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
|
|
760
|
+
"m20 20-3.5-3.5",
|
|
761
|
+
"M11 8v6M8 11h6"
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
zoomOut: {
|
|
765
|
+
stroke: 2,
|
|
766
|
+
paths: [
|
|
767
|
+
"M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0",
|
|
768
|
+
"m20 20-3.5-3.5",
|
|
769
|
+
"M8 11h6"
|
|
770
|
+
]
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
// src/client/Icon.vue
|
|
774
|
+
const _hoisted_1$2 = [
|
|
775
|
+
"width",
|
|
776
|
+
"height",
|
|
777
|
+
"stroke-width"
|
|
778
|
+
];
|
|
779
|
+
const _hoisted_2$2 = ["d"];
|
|
780
|
+
const _sfc_main$2 = /*@__PURE__*/ defineComponent({
|
|
781
|
+
__name: "Icon",
|
|
782
|
+
props: {
|
|
783
|
+
name: {},
|
|
784
|
+
size: { default: 16 }
|
|
785
|
+
},
|
|
786
|
+
setup(__props) {
|
|
787
|
+
return (_ctx, _cache) => {
|
|
788
|
+
return openBlock(), createElementBlock("svg", {
|
|
789
|
+
viewBox: "0 0 24 24",
|
|
790
|
+
width: __props.size,
|
|
791
|
+
height: __props.size,
|
|
792
|
+
fill: "none",
|
|
793
|
+
stroke: "currentColor",
|
|
794
|
+
"stroke-width": unref(ICONS)[__props.name].stroke,
|
|
795
|
+
"stroke-linecap": "round",
|
|
796
|
+
"stroke-linejoin": "round",
|
|
797
|
+
"aria-hidden": "true"
|
|
798
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(ICONS)[__props.name].paths, (d) => {
|
|
799
|
+
return openBlock(), createElementBlock("path", {
|
|
800
|
+
key: d,
|
|
801
|
+
d
|
|
802
|
+
}, null, 8, _hoisted_2$2);
|
|
803
|
+
}), 128))], 8, _hoisted_1$2);
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
});
|
|
3
807
|
// src/client/locales.ts
|
|
4
808
|
const fileListLocales = {
|
|
5
809
|
"zh-CN": {
|
|
@@ -63,7 +867,21 @@ const fileListLocales = {
|
|
|
63
867
|
remembered: "已记住",
|
|
64
868
|
forgetPassword: "忘记",
|
|
65
869
|
unlockFolder: "输入 {name} 的密码",
|
|
66
|
-
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
|
|
870
|
+
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
871
|
+
batchProgress: "{done} / {total} · {files} / {count} 个文件",
|
|
872
|
+
batchDone: "已打包 {name}({count} 个文件,{size})",
|
|
873
|
+
batchCanceled: "已取消下载",
|
|
874
|
+
zipWarnTitle: "文件较大({size})",
|
|
875
|
+
zipWarnText: "当前浏览器要先把文件全部放进内存再打包,超过 {limit} 可能失败或让页面崩溃。建议分批下载。",
|
|
876
|
+
zipStreamTip: "在 Chrome 或 Edge 中可以边下载边保存,不受大小限制。",
|
|
877
|
+
zipContinue: "仍然下载",
|
|
878
|
+
zipBlockTitle: "无法打包 {size}",
|
|
879
|
+
zipBlockText: "当前浏览器一次最多打包 {limit},请分批勾选下载。",
|
|
880
|
+
gotIt: "知道了",
|
|
881
|
+
moreRemaining: "还有 {count} 项",
|
|
882
|
+
showMore: "显示更多",
|
|
883
|
+
showAll: "全部显示",
|
|
884
|
+
showRest: "显示剩余 {count} 项"
|
|
67
885
|
},
|
|
68
886
|
"en-US": {
|
|
69
887
|
allFiles: "All files",
|
|
@@ -101,354 +919,56 @@ const fileListLocales = {
|
|
|
101
919
|
clear: "Clear",
|
|
102
920
|
clearSelection: "Clear selection",
|
|
103
921
|
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
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
function encodePath(path) {
|
|
146
|
-
return path.split("/").map(encodeURIComponent).join("/");
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Make one path segment safe as a ZIP entry segment: no separators,
|
|
150
|
-
* no `.` / `..` traversal, no empty names.
|
|
151
|
-
*/
|
|
152
|
-
function zipSegment(name) {
|
|
153
|
-
const cleaned = name.replace(/[\\/]/g, "_").trim();
|
|
154
|
-
if (!cleaned || cleaned === "." || cleaned === "..") return "_";
|
|
155
|
-
return cleaned;
|
|
156
|
-
}
|
|
157
|
-
/** Join path segments into a ZIP entry name, sanitising each segment. */
|
|
158
|
-
function zipEntryName(segments) {
|
|
159
|
-
return segments.map(zipSegment).join("/");
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
|
|
163
|
-
* The suffix goes before the extension of the last segment. Adds the result to `used`.
|
|
164
|
-
*/
|
|
165
|
-
function uniqueEntryName(name, used) {
|
|
166
|
-
let candidate = name;
|
|
167
|
-
if (used.has(candidate)) {
|
|
168
|
-
const slash = name.lastIndexOf("/");
|
|
169
|
-
const dir = name.slice(0, slash + 1);
|
|
170
|
-
const base = name.slice(slash + 1);
|
|
171
|
-
const dot = base.lastIndexOf(".");
|
|
172
|
-
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
173
|
-
const ext = dot > 0 ? base.slice(dot) : "";
|
|
174
|
-
let index = 2;
|
|
175
|
-
do {
|
|
176
|
-
candidate = `${dir}${stem} (${index})${ext}`;
|
|
177
|
-
index += 1;
|
|
178
|
-
} while (used.has(candidate));
|
|
179
|
-
}
|
|
180
|
-
used.add(candidate);
|
|
181
|
-
return candidate;
|
|
182
|
-
}
|
|
183
|
-
function formatSize(size) {
|
|
184
|
-
if (!size || !Number.isFinite(size)) return "-";
|
|
185
|
-
const units = [
|
|
186
|
-
"B",
|
|
187
|
-
"KB",
|
|
188
|
-
"MB",
|
|
189
|
-
"GB",
|
|
190
|
-
"TB"
|
|
191
|
-
];
|
|
192
|
-
let index = 0;
|
|
193
|
-
let value = size;
|
|
194
|
-
while (value >= 1024 && index < units.length - 1) {
|
|
195
|
-
value /= 1024;
|
|
196
|
-
index += 1;
|
|
922
|
+
folderItems: "{count} items",
|
|
923
|
+
sortBy: "Sort",
|
|
924
|
+
selectAll: "Select all",
|
|
925
|
+
selectItem: "Select {name}",
|
|
926
|
+
expand: "Expand {name}",
|
|
927
|
+
collapse: "Collapse {name}",
|
|
928
|
+
close: "Close",
|
|
929
|
+
previous: "Previous",
|
|
930
|
+
next: "Next",
|
|
931
|
+
rotate: "Rotate",
|
|
932
|
+
actualSize: "Actual size",
|
|
933
|
+
previewUnsupported: "This file type cannot be previewed here. Download it to view.",
|
|
934
|
+
previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
|
|
935
|
+
previewTooLarge: "This file is large ({size}). Download it to view.",
|
|
936
|
+
textTruncated: "Showing the first {size} only. Download for the full file.",
|
|
937
|
+
refreshing: "Refreshing",
|
|
938
|
+
refreshFailed: "Refresh failed: {message}",
|
|
939
|
+
cancel: "Cancel",
|
|
940
|
+
passwordFor: "Password for {name}",
|
|
941
|
+
passwordWrong: "Wrong password or no permission",
|
|
942
|
+
passwordStale: "The saved password no longer works. Enter it again.",
|
|
943
|
+
rememberPassword: "Remember in this tab (cleared when the tab closes)",
|
|
944
|
+
remembered: "Remembered",
|
|
945
|
+
forgetPassword: "Forget",
|
|
946
|
+
unlockFolder: "Enter the password for {name}",
|
|
947
|
+
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
948
|
+
batchProgress: "{done} / {total} · {files} / {count} files",
|
|
949
|
+
batchDone: "Saved {name} ({count} files, {size})",
|
|
950
|
+
batchCanceled: "Download cancelled",
|
|
951
|
+
zipWarnTitle: "Large download ({size})",
|
|
952
|
+
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.",
|
|
953
|
+
zipStreamTip: "Chrome and Edge save while downloading, with no size limit.",
|
|
954
|
+
zipContinue: "Download anyway",
|
|
955
|
+
zipBlockTitle: "Cannot zip {size}",
|
|
956
|
+
zipBlockText: "This browser can zip at most {limit} at once. Select fewer files.",
|
|
957
|
+
gotIt: "OK",
|
|
958
|
+
moreRemaining: "{count} more",
|
|
959
|
+
showMore: "Show more",
|
|
960
|
+
showAll: "Show all",
|
|
961
|
+
showRest: "Show the remaining {count}"
|
|
197
962
|
}
|
|
198
|
-
if (index === 0) return `${Math.round(value)} B`;
|
|
199
|
-
return `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
|
|
200
|
-
}
|
|
201
|
-
function formatDate(iso) {
|
|
202
|
-
if (!iso) return "-";
|
|
203
|
-
const date = new Date(iso);
|
|
204
|
-
if (Number.isNaN(date.getTime())) return iso.slice(0, 10);
|
|
205
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
206
|
-
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
|
|
207
|
-
}
|
|
208
|
-
function extOf(name) {
|
|
209
|
-
const match = name.match(/\.([a-z0-9]+)$/i);
|
|
210
|
-
return match ? match[1].toUpperCase() : "";
|
|
211
|
-
}
|
|
212
|
-
const FILE_KINDS = {};
|
|
213
|
-
for (const [kind, exts] of Object.entries({
|
|
214
|
-
doc: [
|
|
215
|
-
"doc",
|
|
216
|
-
"docx",
|
|
217
|
-
"odt",
|
|
218
|
-
"rtf",
|
|
219
|
-
"txt",
|
|
220
|
-
"md",
|
|
221
|
-
"pages",
|
|
222
|
-
"wps"
|
|
223
|
-
],
|
|
224
|
-
sheet: [
|
|
225
|
-
"xls",
|
|
226
|
-
"xlsx",
|
|
227
|
-
"ods",
|
|
228
|
-
"csv",
|
|
229
|
-
"tsv",
|
|
230
|
-
"numbers",
|
|
231
|
-
"et"
|
|
232
|
-
],
|
|
233
|
-
slide: [
|
|
234
|
-
"ppt",
|
|
235
|
-
"pptx",
|
|
236
|
-
"odp",
|
|
237
|
-
"key",
|
|
238
|
-
"dps"
|
|
239
|
-
],
|
|
240
|
-
pdf: ["pdf"],
|
|
241
|
-
image: [
|
|
242
|
-
"png",
|
|
243
|
-
"jpg",
|
|
244
|
-
"jpeg",
|
|
245
|
-
"gif",
|
|
246
|
-
"webp",
|
|
247
|
-
"svg",
|
|
248
|
-
"bmp",
|
|
249
|
-
"ico",
|
|
250
|
-
"avif",
|
|
251
|
-
"heic"
|
|
252
|
-
],
|
|
253
|
-
media: [
|
|
254
|
-
"mp3",
|
|
255
|
-
"wav",
|
|
256
|
-
"flac",
|
|
257
|
-
"aac",
|
|
258
|
-
"ogg",
|
|
259
|
-
"m4a",
|
|
260
|
-
"mp4",
|
|
261
|
-
"webm",
|
|
262
|
-
"mov",
|
|
263
|
-
"mkv",
|
|
264
|
-
"avi"
|
|
265
|
-
],
|
|
266
|
-
archive: [
|
|
267
|
-
"zip",
|
|
268
|
-
"rar",
|
|
269
|
-
"7z",
|
|
270
|
-
"tar",
|
|
271
|
-
"gz",
|
|
272
|
-
"tgz",
|
|
273
|
-
"bz2",
|
|
274
|
-
"xz",
|
|
275
|
-
"iso"
|
|
276
|
-
],
|
|
277
|
-
code: [
|
|
278
|
-
"js",
|
|
279
|
-
"ts",
|
|
280
|
-
"json",
|
|
281
|
-
"html",
|
|
282
|
-
"css",
|
|
283
|
-
"py",
|
|
284
|
-
"java",
|
|
285
|
-
"c",
|
|
286
|
-
"cpp",
|
|
287
|
-
"go",
|
|
288
|
-
"rs",
|
|
289
|
-
"sh",
|
|
290
|
-
"yml",
|
|
291
|
-
"yaml",
|
|
292
|
-
"xml"
|
|
293
|
-
]
|
|
294
|
-
})) for (const ext of exts) FILE_KINDS[ext] = kind;
|
|
295
|
-
/** Rough file family, used to colour the type badge. */
|
|
296
|
-
function fileKind(name) {
|
|
297
|
-
return FILE_KINDS[extOf(name).toLowerCase()] ?? "other";
|
|
298
|
-
}
|
|
299
|
-
/** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
|
|
300
|
-
function totalSize(items) {
|
|
301
|
-
return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
|
|
302
|
-
}
|
|
303
|
-
function compareNodes(a, b, key, ascending = true) {
|
|
304
|
-
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
305
|
-
let result = 0;
|
|
306
|
-
if (key === "size") result = a.size - b.size;
|
|
307
|
-
else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
|
|
308
|
-
else result = a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
|
|
309
|
-
return ascending ? result : -result;
|
|
310
|
-
}
|
|
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
963
|
};
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
/**
|
|
406
|
-
* Viewers from OpenList's public `iframe_previews` setting that match a file,
|
|
407
|
-
* in the order OpenList lists them. Keys are comma-separated extensions or a
|
|
408
|
-
* `/regex/flags` tested against the extension, as in OpenList's frontend.
|
|
409
|
-
*/
|
|
410
|
-
function iframePreviewsFor(name, setting) {
|
|
411
|
-
if (!setting) return [];
|
|
412
|
-
let parsed;
|
|
413
|
-
try {
|
|
414
|
-
parsed = JSON.parse(setting);
|
|
415
|
-
} catch {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
if (!parsed || typeof parsed !== "object") return [];
|
|
419
|
-
const ext = lowerExt(name);
|
|
420
|
-
const out = [];
|
|
421
|
-
for (const [key, viewers] of Object.entries(parsed)) {
|
|
422
|
-
let hit = false;
|
|
423
|
-
if (key.startsWith("/")) {
|
|
424
|
-
const end = key.lastIndexOf("/");
|
|
425
|
-
try {
|
|
426
|
-
hit = end > 0 && new RegExp(key.slice(1, end), key.slice(end + 1)).test(ext);
|
|
427
|
-
} catch {
|
|
428
|
-
hit = false;
|
|
429
|
-
}
|
|
430
|
-
} else hit = key.split(",").map((item) => item.trim().toLowerCase()).includes(ext);
|
|
431
|
-
if (!hit || !viewers || typeof viewers !== "object") continue;
|
|
432
|
-
for (const [label, scheme] of Object.entries(viewers)) if (typeof scheme === "string" && /^https?:\/\//i.test(scheme)) out.push({
|
|
433
|
-
label,
|
|
434
|
-
scheme
|
|
435
|
-
});
|
|
436
|
-
}
|
|
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;
|
|
964
|
+
function formatLabel(template, values = {}) {
|
|
965
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => values[key] === void 0 ? `{${key}}` : String(values[key]));
|
|
444
966
|
}
|
|
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));
|
|
967
|
+
function resolveLabels(locale = "zh-CN", overrides) {
|
|
968
|
+
return {
|
|
969
|
+
...fileListLocales[locale] ?? fileListLocales["zh-CN"],
|
|
970
|
+
...overrides
|
|
971
|
+
};
|
|
452
972
|
}
|
|
453
973
|
// src/client/PreviewDialog.vue
|
|
454
974
|
const _hoisted_1$1 = ["aria-label"];
|
|
@@ -474,51 +994,33 @@ const _hoisted_13$1 = [
|
|
|
474
994
|
"aria-label",
|
|
475
995
|
"title"
|
|
476
996
|
];
|
|
477
|
-
const _hoisted_14$1 = {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
stroke: "currentColor",
|
|
483
|
-
"stroke-width": "2",
|
|
484
|
-
"stroke-linecap": "round",
|
|
485
|
-
"aria-hidden": "true"
|
|
486
|
-
};
|
|
487
|
-
const _hoisted_15$1 = {
|
|
488
|
-
key: 0,
|
|
489
|
-
d: "M11 8v6M8 11h6"
|
|
490
|
-
};
|
|
491
|
-
const _hoisted_16$1 = {
|
|
492
|
-
key: 1,
|
|
493
|
-
d: "M8 11h6"
|
|
494
|
-
};
|
|
495
|
-
const _hoisted_17$1 = { class: "fl-preview-tools fl-preview-actions" };
|
|
496
|
-
const _hoisted_18$1 = ["aria-label", "title"];
|
|
497
|
-
const _hoisted_19$1 = ["aria-label", "title"];
|
|
498
|
-
const _hoisted_20$1 = ["data-mode"];
|
|
499
|
-
const _hoisted_21$1 = {
|
|
997
|
+
const _hoisted_14$1 = { class: "fl-preview-tools fl-preview-actions" };
|
|
998
|
+
const _hoisted_15$1 = ["aria-label", "title"];
|
|
999
|
+
const _hoisted_16$1 = ["aria-label", "title"];
|
|
1000
|
+
const _hoisted_17$1 = ["data-mode"];
|
|
1001
|
+
const _hoisted_18$1 = {
|
|
500
1002
|
key: 0,
|
|
501
1003
|
class: "fl-preview-status",
|
|
502
1004
|
role: "status"
|
|
503
1005
|
};
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
1006
|
+
const _hoisted_19$1 = ["src", "alt"];
|
|
1007
|
+
const _hoisted_20$1 = ["src"];
|
|
1008
|
+
const _hoisted_21$1 = ["src"];
|
|
1009
|
+
const _hoisted_22$1 = {
|
|
508
1010
|
key: 4,
|
|
509
1011
|
class: "fl-preview-text"
|
|
510
1012
|
};
|
|
511
|
-
const
|
|
1013
|
+
const _hoisted_23$1 = {
|
|
512
1014
|
key: 0,
|
|
513
1015
|
class: "fl-preview-note"
|
|
514
1016
|
};
|
|
515
|
-
const
|
|
516
|
-
const
|
|
1017
|
+
const _hoisted_24$1 = ["src", "title"];
|
|
1018
|
+
const _hoisted_25$1 = {
|
|
517
1019
|
key: 6,
|
|
518
1020
|
class: "fl-preview-info"
|
|
519
1021
|
};
|
|
520
|
-
const
|
|
521
|
-
const
|
|
1022
|
+
const _hoisted_26$1 = ["data-kind"];
|
|
1023
|
+
const _hoisted_27$1 = { class: "fl-preview-message" };
|
|
522
1024
|
const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
523
1025
|
__name: "PreviewDialog",
|
|
524
1026
|
props: {
|
|
@@ -549,11 +1051,12 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
549
1051
|
],
|
|
550
1052
|
setup(__props, { emit: __emit }) {
|
|
551
1053
|
/**
|
|
552
|
-
*
|
|
1054
|
+
* File preview in a native <dialog>, without leaving the page.
|
|
553
1055
|
*
|
|
554
|
-
* image / video / audio
|
|
555
|
-
* pdf
|
|
556
|
-
*
|
|
1056
|
+
* image / video / audio: native elements; text: the first `maxTextBytes`
|
|
1057
|
+
* through a Range request; pdf: the first matching OpenList `iframe_previews`
|
|
1058
|
+
* viewer, else the browser's reader through a Blob. Anything else (Office
|
|
1059
|
+
* included), failures, oversized PDFs and demo mode: file info + download.
|
|
557
1060
|
*/
|
|
558
1061
|
const props = __props;
|
|
559
1062
|
const emit = __emit;
|
|
@@ -737,17 +1240,10 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
737
1240
|
"aria-label": t("previous"),
|
|
738
1241
|
title: t("previous"),
|
|
739
1242
|
onClick: _cache[0] || (_cache[0] = ($event) => emit("prev"))
|
|
740
|
-
}, [
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
fill: "none",
|
|
745
|
-
stroke: "currentColor",
|
|
746
|
-
"stroke-width": "2.2",
|
|
747
|
-
"stroke-linecap": "round",
|
|
748
|
-
"stroke-linejoin": "round",
|
|
749
|
-
"aria-hidden": "true"
|
|
750
|
-
}, [createElementVNode("path", { d: "m15 6-6 6 6 6" })], -1)])], 8, _hoisted_9$1),
|
|
1243
|
+
}, [createVNode(_sfc_main$2, {
|
|
1244
|
+
name: "previous",
|
|
1245
|
+
size: 18
|
|
1246
|
+
})], 8, _hoisted_9$1),
|
|
751
1247
|
createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
|
|
752
1248
|
createElementVNode("button", {
|
|
753
1249
|
class: "fl-preview-tool",
|
|
@@ -755,98 +1251,62 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
755
1251
|
"aria-label": t("next"),
|
|
756
1252
|
title: t("next"),
|
|
757
1253
|
onClick: _cache[1] || (_cache[1] = ($event) => emit("next"))
|
|
758
|
-
}, [
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
fill: "none",
|
|
763
|
-
stroke: "currentColor",
|
|
764
|
-
"stroke-width": "2.2",
|
|
765
|
-
"stroke-linecap": "round",
|
|
766
|
-
"stroke-linejoin": "round",
|
|
767
|
-
"aria-hidden": "true"
|
|
768
|
-
}, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 8, _hoisted_11$1)
|
|
1254
|
+
}, [createVNode(_sfc_main$2, {
|
|
1255
|
+
name: "next",
|
|
1256
|
+
size: 18
|
|
1257
|
+
})], 8, _hoisted_11$1)
|
|
769
1258
|
], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
|
|
770
1259
|
class: "fl-preview-tool fl-preview-rotate",
|
|
771
1260
|
type: "button",
|
|
772
1261
|
"aria-label": t("rotate"),
|
|
773
1262
|
title: t("rotate"),
|
|
774
1263
|
onClick: _cache[2] || (_cache[2] = ($event) => rotation.value = (rotation.value + 90) % 360)
|
|
775
|
-
}, [
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
fill: "none",
|
|
780
|
-
stroke: "currentColor",
|
|
781
|
-
"stroke-width": "2",
|
|
782
|
-
"stroke-linecap": "round",
|
|
783
|
-
"stroke-linejoin": "round",
|
|
784
|
-
"aria-hidden": "true"
|
|
785
|
-
}, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 8, _hoisted_12$1), createElementVNode("button", {
|
|
1264
|
+
}, [createVNode(_sfc_main$2, {
|
|
1265
|
+
name: "refresh",
|
|
1266
|
+
size: 17
|
|
1267
|
+
})], 8, _hoisted_12$1), createElementVNode("button", {
|
|
786
1268
|
class: "fl-preview-tool fl-preview-zoom",
|
|
787
1269
|
type: "button",
|
|
788
1270
|
"aria-pressed": actualSize.value,
|
|
789
1271
|
"aria-label": t("actualSize"),
|
|
790
1272
|
title: t("actualSize"),
|
|
791
1273
|
onClick: _cache[3] || (_cache[3] = ($event) => actualSize.value = !actualSize.value)
|
|
792
|
-
}, [(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}, null, -1)),
|
|
798
|
-
_cache[11] || (_cache[11] = createElementVNode("path", { d: "m20 20-3.5-3.5" }, null, -1)),
|
|
799
|
-
!actualSize.value ? (openBlock(), createElementBlock("path", _hoisted_15$1)) : (openBlock(), createElementBlock("path", _hoisted_16$1))
|
|
800
|
-
]))], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
|
|
801
|
-
createElementVNode("div", _hoisted_17$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
|
|
1274
|
+
}, [createVNode(_sfc_main$2, {
|
|
1275
|
+
name: actualSize.value ? "zoomOut" : "zoomIn",
|
|
1276
|
+
size: 17
|
|
1277
|
+
}, null, 8, ["name"])], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
|
|
1278
|
+
createElementVNode("div", _hoisted_14$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
|
|
802
1279
|
key: 0,
|
|
803
1280
|
class: "fl-preview-tool fl-preview-download",
|
|
804
1281
|
type: "button",
|
|
805
1282
|
"aria-label": t("download"),
|
|
806
1283
|
title: t("download"),
|
|
807
1284
|
onClick: _cache[4] || (_cache[4] = ($event) => emit("download"))
|
|
808
|
-
}, [
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
fill: "none",
|
|
813
|
-
stroke: "currentColor",
|
|
814
|
-
"stroke-width": "2",
|
|
815
|
-
"stroke-linecap": "round",
|
|
816
|
-
"stroke-linejoin": "round",
|
|
817
|
-
"aria-hidden": "true"
|
|
818
|
-
}, [
|
|
819
|
-
createElementVNode("path", { d: "M12 3v12" }),
|
|
820
|
-
createElementVNode("path", { d: "m7 10 5 5 5-5" }),
|
|
821
|
-
createElementVNode("path", { d: "M5 21h14" })
|
|
822
|
-
], -1)])], 8, _hoisted_18$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
1285
|
+
}, [createVNode(_sfc_main$2, {
|
|
1286
|
+
name: "download",
|
|
1287
|
+
size: 17
|
|
1288
|
+
})], 8, _hoisted_15$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
823
1289
|
class: "fl-preview-tool fl-preview-close",
|
|
824
1290
|
type: "button",
|
|
825
1291
|
autofocus: "",
|
|
826
1292
|
"aria-label": t("close"),
|
|
827
1293
|
title: t("close"),
|
|
828
1294
|
onClick: _cache[5] || (_cache[5] = ($event) => emit("close"))
|
|
829
|
-
}, [
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
fill: "none",
|
|
834
|
-
stroke: "currentColor",
|
|
835
|
-
"stroke-width": "2.2",
|
|
836
|
-
"stroke-linecap": "round",
|
|
837
|
-
"aria-hidden": "true"
|
|
838
|
-
}, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_19$1)])
|
|
1295
|
+
}, [createVNode(_sfc_main$2, {
|
|
1296
|
+
name: "close",
|
|
1297
|
+
size: 18
|
|
1298
|
+
})], 8, _hoisted_16$1)])
|
|
839
1299
|
]), createElementVNode("div", {
|
|
840
1300
|
class: "fl-preview-body",
|
|
841
1301
|
"data-mode": busy.value ? "loading" : mode.value
|
|
842
|
-
}, [busy.value ? (openBlock(), createElementBlock("div",
|
|
1302
|
+
}, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_18$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
|
|
843
1303
|
key: 1,
|
|
844
1304
|
class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
|
|
845
1305
|
src: __props.src,
|
|
846
1306
|
alt: __props.file.name,
|
|
847
1307
|
style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
|
|
848
1308
|
onError: markFailed
|
|
849
|
-
}, null, 46,
|
|
1309
|
+
}, null, 46, _hoisted_19$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
|
|
850
1310
|
key: 2,
|
|
851
1311
|
class: "fl-preview-video",
|
|
852
1312
|
src: __props.src,
|
|
@@ -854,49 +1314,106 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
854
1314
|
playsinline: "",
|
|
855
1315
|
preload: "metadata",
|
|
856
1316
|
onError: markFailed
|
|
857
|
-
}, null, 40,
|
|
1317
|
+
}, null, 40, _hoisted_20$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
|
|
858
1318
|
key: 3,
|
|
859
1319
|
class: "fl-preview-audio",
|
|
860
1320
|
src: __props.src,
|
|
861
1321
|
controls: "",
|
|
862
1322
|
preload: "metadata",
|
|
863
1323
|
onError: markFailed
|
|
864
|
-
}, null, 40,
|
|
1324
|
+
}, null, 40, _hoisted_21$1)) : mode.value === "text" ? (openBlock(), createElementBlock("div", _hoisted_22$1, [truncated.value ? (openBlock(), createElementBlock("p", _hoisted_23$1, toDisplayString(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) })), 1)) : createCommentVNode("v-if", true), createElementVNode("pre", null, toDisplayString(textContent.value), 1)])) : mode.value === "pdf" ? (openBlock(), createElementBlock("iframe", {
|
|
865
1325
|
key: 5,
|
|
866
1326
|
class: "fl-preview-frame",
|
|
867
1327
|
src: __props.viewer || blobUrl.value,
|
|
868
1328
|
title: __props.file.name
|
|
869
|
-
}, null, 8,
|
|
1329
|
+
}, null, 8, _hoisted_24$1)) : (openBlock(), createElementBlock("div", _hoisted_25$1, [
|
|
870
1330
|
createElementVNode("span", {
|
|
871
1331
|
class: "fl-type fl-preview-badge",
|
|
872
1332
|
"data-kind": unref(fileKind)(__props.file.name),
|
|
873
1333
|
"aria-hidden": "true"
|
|
874
|
-
}, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9,
|
|
875
|
-
createElementVNode("p",
|
|
1334
|
+
}, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_26$1),
|
|
1335
|
+
createElementVNode("p", _hoisted_27$1, toDisplayString(infoMessage.value), 1),
|
|
876
1336
|
!__props.demo ? (openBlock(), createElementBlock("button", {
|
|
877
1337
|
key: 0,
|
|
878
1338
|
class: "fl-btn fl-btn-primary",
|
|
879
1339
|
type: "button",
|
|
880
1340
|
onClick: _cache[6] || (_cache[6] = ($event) => emit("download"))
|
|
881
|
-
}, [
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
stroke: "currentColor",
|
|
887
|
-
"stroke-width": "2.2",
|
|
888
|
-
"stroke-linecap": "round",
|
|
889
|
-
"stroke-linejoin": "round",
|
|
890
|
-
"aria-hidden": "true"
|
|
891
|
-
}, [
|
|
892
|
-
createElementVNode("path", { d: "M12 3v12" }),
|
|
893
|
-
createElementVNode("path", { d: "m7 10 5 5 5-5" }),
|
|
894
|
-
createElementVNode("path", { d: "M5 21h14" })
|
|
895
|
-
], -1)), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
|
|
896
|
-
]))], 8, _hoisted_20$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
|
|
1341
|
+
}, [createVNode(_sfc_main$2, {
|
|
1342
|
+
name: "download",
|
|
1343
|
+
size: 15
|
|
1344
|
+
}), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
|
|
1345
|
+
]))], 8, _hoisted_17$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
|
|
897
1346
|
};
|
|
898
1347
|
}
|
|
899
1348
|
});
|
|
1349
|
+
// src/client/tree.ts
|
|
1350
|
+
function newLock(prompting = false, error = "") {
|
|
1351
|
+
return {
|
|
1352
|
+
prompting,
|
|
1353
|
+
input: "",
|
|
1354
|
+
error,
|
|
1355
|
+
remember: false,
|
|
1356
|
+
busy: false
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function toNodes(items, parent, password) {
|
|
1360
|
+
return items.map((item) => ({
|
|
1361
|
+
...item,
|
|
1362
|
+
path: joinPath(parent.path, item.name),
|
|
1363
|
+
depth: parent.depth + 1,
|
|
1364
|
+
expanded: false,
|
|
1365
|
+
loaded: false,
|
|
1366
|
+
loading: false,
|
|
1367
|
+
children: [],
|
|
1368
|
+
password,
|
|
1369
|
+
zipPath: [...parent.zipPath, item.name]
|
|
1370
|
+
}));
|
|
1371
|
+
}
|
|
1372
|
+
function sortTree(nodes, key, ascending) {
|
|
1373
|
+
nodes.sort((a, b) => compareNodes(a, b, key, ascending));
|
|
1374
|
+
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
|
|
1375
|
+
}
|
|
1376
|
+
/** Loaded files at or under a node. */
|
|
1377
|
+
function descendantFiles(node) {
|
|
1378
|
+
return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
|
|
1379
|
+
}
|
|
1380
|
+
function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
|
|
1381
|
+
for (const node of nodes) if (node.is_dir && node.expanded) {
|
|
1382
|
+
out.add(node.path);
|
|
1383
|
+
expandedPathsOf(node.children, out);
|
|
1384
|
+
}
|
|
1385
|
+
return out;
|
|
1386
|
+
}
|
|
1387
|
+
/** Last path segment, for labels. */
|
|
1388
|
+
function sourceName(path) {
|
|
1389
|
+
return path.split("/").filter(Boolean).pop() || path;
|
|
1390
|
+
}
|
|
1391
|
+
/** Loaded file count, size and selected count for every folder, in one pass. */
|
|
1392
|
+
function folderStats(roots, selected) {
|
|
1393
|
+
const stats = /* @__PURE__ */ new Map();
|
|
1394
|
+
const walk = (node) => {
|
|
1395
|
+
if (!node.is_dir) return {
|
|
1396
|
+
files: 1,
|
|
1397
|
+
bytes: totalSize([node]),
|
|
1398
|
+
selected: selected.has(node.path) ? 1 : 0
|
|
1399
|
+
};
|
|
1400
|
+
const total = {
|
|
1401
|
+
files: 0,
|
|
1402
|
+
bytes: 0,
|
|
1403
|
+
selected: 0
|
|
1404
|
+
};
|
|
1405
|
+
for (const child of node.children) {
|
|
1406
|
+
const part = walk(child);
|
|
1407
|
+
total.files += part.files;
|
|
1408
|
+
total.bytes += part.bytes;
|
|
1409
|
+
total.selected += part.selected;
|
|
1410
|
+
}
|
|
1411
|
+
stats.set(node.path, total);
|
|
1412
|
+
return total;
|
|
1413
|
+
};
|
|
1414
|
+
roots.forEach(walk);
|
|
1415
|
+
return stats;
|
|
1416
|
+
}
|
|
900
1417
|
// src/client/passwords.ts
|
|
901
1418
|
/**
|
|
902
1419
|
* Folder passwords remembered for the current browser tab.
|
|
@@ -934,12 +1451,6 @@ function write(base, map) {
|
|
|
934
1451
|
function normalise(path) {
|
|
935
1452
|
return path.replace(/\/+$/, "") || "/";
|
|
936
1453
|
}
|
|
937
|
-
/** Whether `path` is `folder` itself or inside it. */
|
|
938
|
-
function isWithin(path, folder) {
|
|
939
|
-
const p = normalise(path);
|
|
940
|
-
const f = normalise(folder);
|
|
941
|
-
return f === "/" || p === f || p.startsWith(`${f}/`);
|
|
942
|
-
}
|
|
943
1454
|
/** The closest remembered folder at or above `path`, if any. */
|
|
944
1455
|
function findRememberedPassword(base, path) {
|
|
945
1456
|
let best = null;
|
|
@@ -967,6 +1478,73 @@ function forgetFolderPassword(base, path) {
|
|
|
967
1478
|
function rememberedFolders(base) {
|
|
968
1479
|
return Object.keys(read(base));
|
|
969
1480
|
}
|
|
1481
|
+
// src/client/usePasswords.ts
|
|
1482
|
+
/**
|
|
1483
|
+
* The one place that decides which folder password to send.
|
|
1484
|
+
*
|
|
1485
|
+
* Order: typed on this page for this exact folder → the item's own password
|
|
1486
|
+
* (site-configured, or inherited from the folder it was listed with) → typed
|
|
1487
|
+
* for an enclosing folder → remembered in this tab → the component default.
|
|
1488
|
+
*/
|
|
1489
|
+
function usePasswords(options) {
|
|
1490
|
+
/** Typed on this page; not necessarily remembered. Survives refreshes. */
|
|
1491
|
+
const typed = /* @__PURE__ */ new Map();
|
|
1492
|
+
/** Remembered folders, mirrored from sessionStorage so templates react. */
|
|
1493
|
+
const remembered = ref(/* @__PURE__ */ new Set());
|
|
1494
|
+
function sync() {
|
|
1495
|
+
remembered.value = new Set(options.enabled() ? rememberedFolders(options.scope()) : []);
|
|
1496
|
+
}
|
|
1497
|
+
function typedAbove(path) {
|
|
1498
|
+
let best = "";
|
|
1499
|
+
let bestFolder = "";
|
|
1500
|
+
for (const [folder, password] of typed) if (isWithin(path, folder) && folder.length > bestFolder.length) {
|
|
1501
|
+
best = password;
|
|
1502
|
+
bestFolder = folder;
|
|
1503
|
+
}
|
|
1504
|
+
return best;
|
|
1505
|
+
}
|
|
1506
|
+
function resolve(path, own = "") {
|
|
1507
|
+
const exact = typed.get(path);
|
|
1508
|
+
if (exact) return { password: exact };
|
|
1509
|
+
if (own) return { password: own };
|
|
1510
|
+
const above = typedAbove(path);
|
|
1511
|
+
if (above) return { password: above };
|
|
1512
|
+
const stored = options.enabled() ? findRememberedPassword(options.scope(), path) : null;
|
|
1513
|
+
if (stored) return {
|
|
1514
|
+
password: stored.password,
|
|
1515
|
+
rememberedAt: stored.path
|
|
1516
|
+
};
|
|
1517
|
+
return { password: options.fallback() };
|
|
1518
|
+
}
|
|
1519
|
+
/** A password worked: keep it for this page, and in this tab when asked. */
|
|
1520
|
+
function accept(path, password, remember) {
|
|
1521
|
+
typed.set(path, password);
|
|
1522
|
+
if (remember && options.enabled()) {
|
|
1523
|
+
rememberFolderPassword(options.scope(), path, password);
|
|
1524
|
+
sync();
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function forget(path) {
|
|
1528
|
+
typed.delete(path);
|
|
1529
|
+
forgetFolderPassword(options.scope(), path);
|
|
1530
|
+
sync();
|
|
1531
|
+
}
|
|
1532
|
+
/** A remembered password stopped working: drop it. Returns whether one was dropped. */
|
|
1533
|
+
function dropStale(rememberedAt) {
|
|
1534
|
+
if (!rememberedAt) return false;
|
|
1535
|
+
forgetFolderPassword(options.scope(), rememberedAt);
|
|
1536
|
+
sync();
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
remembered,
|
|
1541
|
+
sync,
|
|
1542
|
+
resolve,
|
|
1543
|
+
accept,
|
|
1544
|
+
forget,
|
|
1545
|
+
dropStale
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
970
1548
|
// src/client/FileList.vue
|
|
971
1549
|
const _hoisted_1 = ["aria-busy"];
|
|
972
1550
|
const _hoisted_2 = { class: "fl-bar" };
|
|
@@ -976,129 +1554,129 @@ const _hoisted_3 = {
|
|
|
976
1554
|
role: "status"
|
|
977
1555
|
};
|
|
978
1556
|
const _hoisted_4 = { class: "fl-selection-count" };
|
|
979
|
-
const _hoisted_5 = {
|
|
1557
|
+
const _hoisted_5 = {
|
|
1558
|
+
key: 0,
|
|
1559
|
+
class: "fl-selection-size"
|
|
1560
|
+
};
|
|
980
1561
|
const _hoisted_6 = {
|
|
981
1562
|
key: 1,
|
|
1563
|
+
class: "fl-selection",
|
|
1564
|
+
role: "status"
|
|
1565
|
+
};
|
|
1566
|
+
const _hoisted_7 = { class: "fl-selection-count" };
|
|
1567
|
+
const _hoisted_8 = { class: "fl-selection-size" };
|
|
1568
|
+
const _hoisted_9 = {
|
|
1569
|
+
key: 2,
|
|
982
1570
|
class: "fl-title"
|
|
983
1571
|
};
|
|
984
|
-
const
|
|
985
|
-
const
|
|
1572
|
+
const _hoisted_10 = { class: "fl-title-text" };
|
|
1573
|
+
const _hoisted_11 = {
|
|
986
1574
|
key: 0,
|
|
987
1575
|
class: "fl-demo-badge"
|
|
988
1576
|
};
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
const
|
|
994
|
-
const
|
|
1577
|
+
const _hoisted_12 = ["aria-label", "title"];
|
|
1578
|
+
const _hoisted_13 = { class: "fl-clear-text" };
|
|
1579
|
+
const _hoisted_14 = { class: "fl-tools" };
|
|
1580
|
+
const _hoisted_15 = { class: "fl-search" };
|
|
1581
|
+
const _hoisted_16 = ["placeholder", "aria-label"];
|
|
1582
|
+
const _hoisted_17 = [
|
|
995
1583
|
"disabled",
|
|
996
1584
|
"aria-label",
|
|
997
1585
|
"title"
|
|
998
1586
|
];
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
const _hoisted_17 = {
|
|
1587
|
+
const _hoisted_18 = { class: "fl-row fl-head" };
|
|
1588
|
+
const _hoisted_19 = {
|
|
1002
1589
|
key: 0,
|
|
1003
1590
|
class: "fl-progress",
|
|
1004
1591
|
"aria-hidden": "true"
|
|
1005
1592
|
};
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1593
|
+
const _hoisted_20 = { class: "fl-cell fl-cell-check" };
|
|
1594
|
+
const _hoisted_21 = [
|
|
1008
1595
|
"aria-label",
|
|
1596
|
+
"disabled",
|
|
1009
1597
|
"checked",
|
|
1010
1598
|
".indeterminate"
|
|
1011
1599
|
];
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
const _hoisted_22 = {
|
|
1015
|
-
class: "fl-sort",
|
|
1016
|
-
"aria-hidden": "true"
|
|
1017
|
-
};
|
|
1018
|
-
const _hoisted_23 = { class: "fl-cell fl-cell-size" };
|
|
1019
|
-
const _hoisted_24 = {
|
|
1600
|
+
const _hoisted_22 = ["aria-sort", "onClick"];
|
|
1601
|
+
const _hoisted_23 = {
|
|
1020
1602
|
class: "fl-sort",
|
|
1021
1603
|
"aria-hidden": "true"
|
|
1022
1604
|
};
|
|
1023
|
-
const
|
|
1605
|
+
const _hoisted_24 = { class: "fl-cell fl-cell-op" };
|
|
1606
|
+
const _hoisted_25 = { class: "fl-sort-select" };
|
|
1024
1607
|
const _hoisted_26 = {
|
|
1025
|
-
class: "fl-sort",
|
|
1026
|
-
"aria-hidden": "true"
|
|
1027
|
-
};
|
|
1028
|
-
const _hoisted_27 = { class: "fl-cell fl-cell-op" };
|
|
1029
|
-
const _hoisted_28 = { class: "fl-sort-select" };
|
|
1030
|
-
const _hoisted_29 = {
|
|
1031
1608
|
class: "fl-sort-current",
|
|
1032
1609
|
"aria-hidden": "true"
|
|
1033
1610
|
};
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1611
|
+
const _hoisted_27 = ["aria-label"];
|
|
1612
|
+
const _hoisted_28 = ["value"];
|
|
1613
|
+
const _hoisted_29 = {
|
|
1037
1614
|
key: 0,
|
|
1038
1615
|
class: "fl-status"
|
|
1039
1616
|
};
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
class: "fl-unlock-message",
|
|
1045
|
-
role: "alert"
|
|
1046
|
-
};
|
|
1047
|
-
const _hoisted_35 = { class: "fl-unlock" };
|
|
1048
|
-
const _hoisted_36 = [
|
|
1617
|
+
const _hoisted_30 = ["for"];
|
|
1618
|
+
const _hoisted_31 = ["id"];
|
|
1619
|
+
const _hoisted_32 = { class: "fl-unlock" };
|
|
1620
|
+
const _hoisted_33 = [
|
|
1049
1621
|
"id",
|
|
1050
1622
|
"data-unlock",
|
|
1051
1623
|
"placeholder",
|
|
1052
1624
|
"aria-invalid",
|
|
1053
1625
|
"aria-describedby"
|
|
1054
1626
|
];
|
|
1055
|
-
const
|
|
1627
|
+
const _hoisted_34 = [
|
|
1056
1628
|
"aria-pressed",
|
|
1057
1629
|
"aria-label",
|
|
1058
1630
|
"title"
|
|
1059
1631
|
];
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1632
|
+
const _hoisted_35 = ["disabled"];
|
|
1633
|
+
const _hoisted_36 = {
|
|
1062
1634
|
key: 2,
|
|
1063
1635
|
class: "fl-status fl-error"
|
|
1064
1636
|
};
|
|
1065
|
-
const
|
|
1637
|
+
const _hoisted_37 = {
|
|
1066
1638
|
key: 0,
|
|
1067
1639
|
class: "fl-hint"
|
|
1068
1640
|
};
|
|
1069
|
-
const
|
|
1641
|
+
const _hoisted_38 = {
|
|
1070
1642
|
key: 1,
|
|
1071
1643
|
class: "fl-hint"
|
|
1072
1644
|
};
|
|
1073
|
-
const
|
|
1645
|
+
const _hoisted_39 = {
|
|
1074
1646
|
key: 3,
|
|
1075
1647
|
class: "fl-status"
|
|
1076
1648
|
};
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1649
|
+
const _hoisted_40 = { class: "fl-cell fl-more-cell" };
|
|
1650
|
+
const _hoisted_41 = { class: "fl-more-count" };
|
|
1651
|
+
const _hoisted_42 = ["onClick"];
|
|
1652
|
+
const _hoisted_43 = { class: "fl-more-page" };
|
|
1653
|
+
const _hoisted_44 = ["onClick"];
|
|
1654
|
+
const _hoisted_45 = ["onClick"];
|
|
1655
|
+
const _hoisted_46 = { class: "fl-cell fl-cell-check" };
|
|
1656
|
+
const _hoisted_47 = [
|
|
1079
1657
|
"disabled",
|
|
1080
1658
|
"aria-label",
|
|
1081
1659
|
"checked",
|
|
1082
1660
|
".indeterminate",
|
|
1083
1661
|
"onChange"
|
|
1084
1662
|
];
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1663
|
+
const _hoisted_48 = ["title"];
|
|
1664
|
+
const _hoisted_49 = [
|
|
1087
1665
|
"aria-label",
|
|
1088
1666
|
"title",
|
|
1089
1667
|
"onClick"
|
|
1090
1668
|
];
|
|
1091
|
-
const
|
|
1669
|
+
const _hoisted_50 = [
|
|
1092
1670
|
"aria-label",
|
|
1093
1671
|
"aria-expanded",
|
|
1094
1672
|
"onClick"
|
|
1095
1673
|
];
|
|
1096
|
-
const
|
|
1674
|
+
const _hoisted_51 = {
|
|
1097
1675
|
key: 2,
|
|
1098
1676
|
class: "fl-arrow-placeholder",
|
|
1099
1677
|
"aria-hidden": "true"
|
|
1100
1678
|
};
|
|
1101
|
-
const
|
|
1679
|
+
const _hoisted_52 = {
|
|
1102
1680
|
key: 3,
|
|
1103
1681
|
class: "fl-folder-icon",
|
|
1104
1682
|
viewBox: "0 0 24 24",
|
|
@@ -1106,27 +1684,27 @@ const _hoisted_49 = {
|
|
|
1106
1684
|
height: "18",
|
|
1107
1685
|
"aria-hidden": "true"
|
|
1108
1686
|
};
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
const
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
1687
|
+
const _hoisted_53 = ["data-kind"];
|
|
1688
|
+
const _hoisted_54 = { class: "fl-label" };
|
|
1689
|
+
const _hoisted_55 = ["onClick"];
|
|
1690
|
+
const _hoisted_56 = { class: "fl-meta" };
|
|
1691
|
+
const _hoisted_57 = {
|
|
1114
1692
|
key: 5,
|
|
1115
1693
|
class: "fl-count"
|
|
1116
1694
|
};
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1695
|
+
const _hoisted_58 = ["id"];
|
|
1696
|
+
const _hoisted_59 = [
|
|
1119
1697
|
"aria-label",
|
|
1120
1698
|
"title",
|
|
1121
1699
|
"onClick"
|
|
1122
1700
|
];
|
|
1123
|
-
const
|
|
1701
|
+
const _hoisted_60 = {
|
|
1124
1702
|
key: 8,
|
|
1125
1703
|
class: "fl-loading",
|
|
1126
1704
|
"aria-hidden": "true"
|
|
1127
1705
|
};
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1706
|
+
const _hoisted_61 = ["onSubmit", "onKeydown"];
|
|
1707
|
+
const _hoisted_62 = [
|
|
1130
1708
|
"onUpdate:modelValue",
|
|
1131
1709
|
"data-unlock",
|
|
1132
1710
|
"placeholder",
|
|
@@ -1134,43 +1712,45 @@ const _hoisted_59 = [
|
|
|
1134
1712
|
"aria-invalid",
|
|
1135
1713
|
"aria-describedby"
|
|
1136
1714
|
];
|
|
1137
|
-
const
|
|
1715
|
+
const _hoisted_63 = [
|
|
1138
1716
|
"aria-pressed",
|
|
1139
1717
|
"aria-label",
|
|
1140
1718
|
"title",
|
|
1141
1719
|
"onClick"
|
|
1142
1720
|
];
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1721
|
+
const _hoisted_64 = ["disabled"];
|
|
1722
|
+
const _hoisted_65 = [
|
|
1145
1723
|
"aria-label",
|
|
1146
1724
|
"title",
|
|
1147
1725
|
"onClick"
|
|
1148
1726
|
];
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1727
|
+
const _hoisted_66 = { class: "fl-cell fl-cell-size" };
|
|
1728
|
+
const _hoisted_67 = { class: "fl-cell fl-cell-date" };
|
|
1729
|
+
const _hoisted_68 = { class: "fl-cell fl-cell-op" };
|
|
1730
|
+
const _hoisted_69 = ["aria-label", "onClick"];
|
|
1731
|
+
const _hoisted_70 = [
|
|
1154
1732
|
"aria-label",
|
|
1155
1733
|
"aria-expanded",
|
|
1156
1734
|
"onClick"
|
|
1157
1735
|
];
|
|
1158
|
-
const
|
|
1736
|
+
const _hoisted_71 = [
|
|
1159
1737
|
"aria-label",
|
|
1160
1738
|
"title",
|
|
1161
1739
|
"onClick"
|
|
1162
1740
|
];
|
|
1163
|
-
const
|
|
1741
|
+
const _hoisted_72 = [
|
|
1164
1742
|
"aria-label",
|
|
1165
1743
|
"title",
|
|
1166
1744
|
"onClick"
|
|
1167
1745
|
];
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1746
|
+
const _hoisted_73 = { class: "fl-foot" };
|
|
1747
|
+
const _hoisted_74 = {
|
|
1170
1748
|
key: 4,
|
|
1171
1749
|
class: "fl-toast",
|
|
1172
1750
|
role: "status"
|
|
1173
1751
|
};
|
|
1752
|
+
/** Rows shown per folder, beyond the first page; kept across refreshes. */
|
|
1753
|
+
const MB = 1048576;
|
|
1174
1754
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
1175
1755
|
__name: "FileList",
|
|
1176
1756
|
props: {
|
|
@@ -1188,24 +1768,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1188
1768
|
type: Boolean,
|
|
1189
1769
|
default: true
|
|
1190
1770
|
},
|
|
1771
|
+
pageSize: { default: 200 },
|
|
1772
|
+
zipWarnSize: {},
|
|
1773
|
+
zipMaxSize: {},
|
|
1191
1774
|
locale: { default: "zh-CN" },
|
|
1192
1775
|
labels: {}
|
|
1193
1776
|
},
|
|
1194
1777
|
setup(__props) {
|
|
1195
1778
|
/**
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* <FileList /> // 演示模式
|
|
1779
|
+
* OpenList file browser: folder tree, batch ZIP, folder passwords and an
|
|
1780
|
+
* in-page preview. OpenList access is in api.ts (demo.ts when `base` is empty),
|
|
1781
|
+
* password choice in usePasswords.ts, tree helpers in tree.ts and downloads in
|
|
1782
|
+
* download.ts; this component wires them to the UI.
|
|
1201
1783
|
*/
|
|
1202
1784
|
const props = __props;
|
|
1203
1785
|
const text = computed(() => resolveLabels(props.locale, props.labels));
|
|
1204
1786
|
function t(key, values) {
|
|
1205
1787
|
return formatLabel(text.value[key], values);
|
|
1206
1788
|
}
|
|
1789
|
+
/** Prefix for element ids, unique per component instance. */
|
|
1790
|
+
const uid = useId();
|
|
1207
1791
|
const demo = computed(() => !props.base);
|
|
1208
|
-
const
|
|
1792
|
+
const api = computed(() => demo.value ? createDemoApi() : createOpenListApi(props.base, props.token));
|
|
1793
|
+
const canRemember = computed(() => props.rememberPassword !== false);
|
|
1794
|
+
const passwords = usePasswords({
|
|
1795
|
+
scope: () => props.base.replace(/\/+$/, "") || "demo",
|
|
1796
|
+
enabled: () => canRemember.value,
|
|
1797
|
+
fallback: () => props.password
|
|
1798
|
+
});
|
|
1799
|
+
const remembered = passwords.remembered;
|
|
1209
1800
|
const rootEl = ref();
|
|
1210
1801
|
const roots = ref([]);
|
|
1211
1802
|
const loadingRoot = ref(false);
|
|
@@ -1213,413 +1804,155 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1213
1804
|
const refreshing = ref(false);
|
|
1214
1805
|
let refreshSeq = 0;
|
|
1215
1806
|
/** Single-path mode: the listed folder itself needs a password. */
|
|
1216
|
-
const rootLock =
|
|
1217
|
-
active: false,
|
|
1218
|
-
input: "",
|
|
1219
|
-
error: "",
|
|
1220
|
-
remember: false,
|
|
1221
|
-
busy: false
|
|
1222
|
-
});
|
|
1223
|
-
/** Password the visitor typed for the listed folder (single-path mode). */
|
|
1224
|
-
const rootPassword = ref("");
|
|
1807
|
+
const rootLock = ref(null);
|
|
1225
1808
|
const keyword = ref("");
|
|
1226
1809
|
const selected = ref(/* @__PURE__ */ new Set());
|
|
1227
1810
|
const toast = ref("");
|
|
1228
1811
|
const sortKey = ref("name");
|
|
1229
1812
|
const sortAsc = ref(true);
|
|
1230
1813
|
const siteTitle = ref("");
|
|
1231
|
-
/** OpenList's public `iframe_previews` setting (JSON text). */
|
|
1232
1814
|
const iframePreviews = ref("");
|
|
1233
1815
|
const batchBusy = ref(false);
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1816
|
+
const zipProgress = ref(null);
|
|
1817
|
+
let zipAbort = null;
|
|
1236
1818
|
let toastTimer;
|
|
1237
|
-
|
|
1238
|
-
|
|
1819
|
+
/** The folder listed in single-path mode, as the parent of the top rows. */
|
|
1820
|
+
const listedFolder = computed(() => ({
|
|
1821
|
+
path: props.path,
|
|
1822
|
+
depth: -1,
|
|
1823
|
+
zipPath: []
|
|
1824
|
+
}));
|
|
1825
|
+
function showToast(message) {
|
|
1826
|
+
toast.value = message;
|
|
1239
1827
|
clearTimeout(toastTimer);
|
|
1240
1828
|
toastTimer = setTimeout(() => toast.value = "", 3200);
|
|
1241
1829
|
}
|
|
1242
|
-
function errorText(error) {
|
|
1243
|
-
return error instanceof Error ? error.message : String(error);
|
|
1244
|
-
}
|
|
1245
|
-
function isPasswordError(error) {
|
|
1246
|
-
return /password|密码/i.test(errorText(error));
|
|
1247
|
-
}
|
|
1248
|
-
function isNetworkError(error) {
|
|
1249
|
-
return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
|
|
1250
|
-
}
|
|
1251
1830
|
/** A failure toast; browser network errors are usually CORS, so say where to look. */
|
|
1252
1831
|
function failureMessage(key, error) {
|
|
1253
1832
|
const message = t(key, { message: errorText(error) });
|
|
1254
|
-
return isNetworkError(error) ? `${message} ${t("
|
|
1255
|
-
}
|
|
1256
|
-
/** Total size for display: an empty total reads `0 B`, not `-`. */
|
|
1257
|
-
function formatTotal(bytes) {
|
|
1258
|
-
return bytes > 0 ? formatSize(bytes) : "0 B";
|
|
1259
|
-
}
|
|
1260
|
-
const canRemember = computed(() => props.rememberPassword !== false);
|
|
1261
|
-
const passwordScope = computed(() => baseUrl.value || "demo");
|
|
1262
|
-
/** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
|
|
1263
|
-
const typedPasswords = /* @__PURE__ */ new Map();
|
|
1264
|
-
function typedPasswordFor(path) {
|
|
1265
|
-
let best = "";
|
|
1266
|
-
let bestPath = "";
|
|
1267
|
-
for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
|
|
1268
|
-
best = password;
|
|
1269
|
-
bestPath = folder;
|
|
1270
|
-
}
|
|
1271
|
-
return best;
|
|
1272
|
-
}
|
|
1273
|
-
/** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
|
|
1274
|
-
const rememberedPaths = ref(/* @__PURE__ */ new Set());
|
|
1275
|
-
function syncRemembered() {
|
|
1276
|
-
rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
|
|
1277
|
-
}
|
|
1278
|
-
function storedPassword(path) {
|
|
1279
|
-
return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Password for an item: its own or inherited one first (site-configured or
|
|
1283
|
-
* typed), then one remembered in this tab, then the component's `password`.
|
|
1284
|
-
*/
|
|
1285
|
-
function passwordFor(node) {
|
|
1286
|
-
if (node.password) return { password: node.password };
|
|
1287
|
-
const typed = typedPasswordFor(node.path);
|
|
1288
|
-
if (typed) return { password: typed };
|
|
1289
|
-
const stored = storedPassword(node.path);
|
|
1290
|
-
if (stored) return {
|
|
1291
|
-
password: stored.password,
|
|
1292
|
-
rememberedAt: stored.path
|
|
1293
|
-
};
|
|
1294
|
-
return { password: props.password };
|
|
1295
|
-
}
|
|
1296
|
-
function rootPasswordFor() {
|
|
1297
|
-
if (rootPassword.value) return { password: rootPassword.value };
|
|
1298
|
-
const stored = storedPassword(props.path);
|
|
1299
|
-
if (stored) return {
|
|
1300
|
-
password: stored.password,
|
|
1301
|
-
rememberedAt: stored.path
|
|
1302
|
-
};
|
|
1303
|
-
return { password: props.password };
|
|
1304
|
-
}
|
|
1305
|
-
/** A remembered password stopped working: drop it so we do not keep retrying it. */
|
|
1306
|
-
function dropStale(path) {
|
|
1307
|
-
if (!path) return false;
|
|
1308
|
-
forgetFolderPassword(passwordScope.value, path);
|
|
1309
|
-
syncRemembered();
|
|
1310
|
-
return true;
|
|
1311
|
-
}
|
|
1312
|
-
function focusUnlock(path) {
|
|
1313
|
-
nextTick(() => {
|
|
1314
|
-
(rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
/** Show the same-row password input; `focus` only for user actions, never on page load. */
|
|
1318
|
-
function startPrompt(node, focus) {
|
|
1319
|
-
node.prompting = true;
|
|
1320
|
-
if (focus) focusUnlock(node.path);
|
|
1321
|
-
}
|
|
1322
|
-
function cancelUnlock(node) {
|
|
1323
|
-
node.prompting = false;
|
|
1324
|
-
node.unlockInput = "";
|
|
1325
|
-
node.unlockError = "";
|
|
1326
|
-
}
|
|
1327
|
-
function unlockErrorText(error) {
|
|
1328
|
-
if (error === "wrong") return t("passwordWrong");
|
|
1329
|
-
if (error === "stale") return t("passwordStale");
|
|
1330
|
-
return "";
|
|
1331
|
-
}
|
|
1332
|
-
const DEMO_TREE = {
|
|
1333
|
-
"/": [
|
|
1334
|
-
{
|
|
1335
|
-
name: "assignments",
|
|
1336
|
-
size: 0,
|
|
1337
|
-
is_dir: true,
|
|
1338
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
1339
|
-
},
|
|
1340
|
-
{
|
|
1341
|
-
name: "exams",
|
|
1342
|
-
size: 0,
|
|
1343
|
-
is_dir: true,
|
|
1344
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
1345
|
-
},
|
|
1346
|
-
{
|
|
1347
|
-
name: "materials",
|
|
1348
|
-
size: 0,
|
|
1349
|
-
is_dir: true,
|
|
1350
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
1351
|
-
},
|
|
1352
|
-
{
|
|
1353
|
-
name: "notes",
|
|
1354
|
-
size: 0,
|
|
1355
|
-
is_dir: true,
|
|
1356
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
1357
|
-
},
|
|
1358
|
-
{
|
|
1359
|
-
name: "slides",
|
|
1360
|
-
size: 0,
|
|
1361
|
-
is_dir: true,
|
|
1362
|
-
modified: "2026-09-01T08:00:00+08:00"
|
|
1363
|
-
},
|
|
1364
|
-
{
|
|
1365
|
-
name: "加密示例(密码 1234)",
|
|
1366
|
-
size: 0,
|
|
1367
|
-
is_dir: true,
|
|
1368
|
-
modified: "2026-10-20T18:00:00+08:00"
|
|
1369
|
-
}
|
|
1370
|
-
],
|
|
1371
|
-
"/assignments": [{
|
|
1372
|
-
name: "线代作业参考答案",
|
|
1373
|
-
size: 0,
|
|
1374
|
-
is_dir: true,
|
|
1375
|
-
modified: "2026-10-12T21:30:00+08:00"
|
|
1376
|
-
}, {
|
|
1377
|
-
name: "作业提交说明.pdf",
|
|
1378
|
-
size: 186432,
|
|
1379
|
-
is_dir: false,
|
|
1380
|
-
modified: "2026-09-03T10:15:00+08:00"
|
|
1381
|
-
}],
|
|
1382
|
-
"/assignments/线代作业参考答案": [
|
|
1383
|
-
{
|
|
1384
|
-
name: "第1次作业参考答案.doc",
|
|
1385
|
-
size: 412672,
|
|
1386
|
-
is_dir: false,
|
|
1387
|
-
modified: "2026-09-15T22:41:00+08:00"
|
|
1388
|
-
},
|
|
1389
|
-
{
|
|
1390
|
-
name: "第2次作业参考答案.pdf",
|
|
1391
|
-
size: 1048576,
|
|
1392
|
-
is_dir: false,
|
|
1393
|
-
modified: "2026-09-22T22:03:00+08:00"
|
|
1394
|
-
},
|
|
1395
|
-
{
|
|
1396
|
-
name: "第3次作业参考答案.pdf",
|
|
1397
|
-
size: 986112,
|
|
1398
|
-
is_dir: false,
|
|
1399
|
-
modified: "2026-09-29T21:57:00+08:00"
|
|
1400
|
-
},
|
|
1401
|
-
{
|
|
1402
|
-
name: "第4次作业参考答案.pdf",
|
|
1403
|
-
size: 1572864,
|
|
1404
|
-
is_dir: false,
|
|
1405
|
-
modified: "2026-10-13T22:10:00+08:00"
|
|
1406
|
-
}
|
|
1407
|
-
],
|
|
1408
|
-
"/exams": [
|
|
1409
|
-
{
|
|
1410
|
-
name: "2023 期末试题和答案.pdf",
|
|
1411
|
-
size: 2345678,
|
|
1412
|
-
is_dir: false,
|
|
1413
|
-
modified: "2025-01-18T09:20:00+08:00"
|
|
1414
|
-
},
|
|
1415
|
-
{
|
|
1416
|
-
name: "2024 期中试题.pdf",
|
|
1417
|
-
size: 1234567,
|
|
1418
|
-
is_dir: false,
|
|
1419
|
-
modified: "2025-11-02T14:08:00+08:00"
|
|
1420
|
-
},
|
|
1421
|
-
{
|
|
1422
|
-
name: "代数与几何-多年合集-试题和答案.pdf",
|
|
1423
|
-
size: 18874368,
|
|
1424
|
-
is_dir: false,
|
|
1425
|
-
modified: "2025-01-18T09:25:00+08:00"
|
|
1426
|
-
}
|
|
1427
|
-
],
|
|
1428
|
-
"/materials": [
|
|
1429
|
-
{
|
|
1430
|
-
name: "线性代数及其应用(Lay 第5版).pdf",
|
|
1431
|
-
size: 48231424,
|
|
1432
|
-
is_dir: false,
|
|
1433
|
-
modified: "2024-07-29T16:44:00+08:00"
|
|
1434
|
-
},
|
|
1435
|
-
{
|
|
1436
|
-
name: "Introduction to Linear Algebra(Strang).pdf",
|
|
1437
|
-
size: 36700160,
|
|
1438
|
-
is_dir: false,
|
|
1439
|
-
modified: "2024-07-29T16:50:00+08:00"
|
|
1440
|
-
},
|
|
1441
|
-
{
|
|
1442
|
-
name: "疑难解答与习题指导.zip",
|
|
1443
|
-
size: 9437184,
|
|
1444
|
-
is_dir: false,
|
|
1445
|
-
modified: "2024-08-05T11:12:00+08:00"
|
|
1446
|
-
}
|
|
1447
|
-
],
|
|
1448
|
-
"/notes": [{
|
|
1449
|
-
name: "线性代数笔记(106页).pdf",
|
|
1450
|
-
size: 12582912,
|
|
1451
|
-
is_dir: false,
|
|
1452
|
-
modified: "2025-06-30T20:31:00+08:00"
|
|
1453
|
-
}, {
|
|
1454
|
-
name: "期末复习提纲.docx",
|
|
1455
|
-
size: 245760,
|
|
1456
|
-
is_dir: false,
|
|
1457
|
-
modified: "2026-01-05T19:02:00+08:00"
|
|
1458
|
-
}],
|
|
1459
|
-
"/加密示例(密码 1234)": [{
|
|
1460
|
-
name: "期末答案(教师版).pdf",
|
|
1461
|
-
size: 2621440,
|
|
1462
|
-
is_dir: false,
|
|
1463
|
-
modified: "2026-10-20T18:00:00+08:00"
|
|
1464
|
-
}, {
|
|
1465
|
-
name: "成绩登记表.xlsx",
|
|
1466
|
-
size: 48128,
|
|
1467
|
-
is_dir: false,
|
|
1468
|
-
modified: "2026-10-21T09:12:00+08:00"
|
|
1469
|
-
}],
|
|
1470
|
-
"/slides": [{
|
|
1471
|
-
name: "第一章 行列式.pptx",
|
|
1472
|
-
size: 3145728,
|
|
1473
|
-
is_dir: false,
|
|
1474
|
-
modified: "2026-09-08T13:26:00+08:00"
|
|
1475
|
-
}, {
|
|
1476
|
-
name: "第二章 矩阵.pptx",
|
|
1477
|
-
size: 4194304,
|
|
1478
|
-
is_dir: false,
|
|
1479
|
-
modified: "2026-09-15T13:30:00+08:00"
|
|
1480
|
-
}]
|
|
1481
|
-
};
|
|
1482
|
-
/** Demo-only folder passwords, so the demo can show the password flow. */
|
|
1483
|
-
const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
|
|
1484
|
-
class ApiError extends Error {
|
|
1485
|
-
code;
|
|
1486
|
-
constructor(message, code) {
|
|
1487
|
-
super(message);
|
|
1488
|
-
this.code = code;
|
|
1489
|
-
}
|
|
1833
|
+
return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
|
|
1490
1834
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
return {
|
|
1494
|
-
...json ? { "Content-Type": "application/json" } : {},
|
|
1495
|
-
...props.token ? { Authorization: props.token } : {}
|
|
1496
|
-
};
|
|
1835
|
+
function sortNodes(nodes) {
|
|
1836
|
+
sortTree(nodes, sortKey.value, sortAsc.value);
|
|
1497
1837
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
headers: authHeaders(body !== void 0),
|
|
1502
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1838
|
+
function focusUnlock(path) {
|
|
1839
|
+
nextTick(() => {
|
|
1840
|
+
rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`)?.focus();
|
|
1503
1841
|
});
|
|
1504
|
-
const payload = await response.json().catch(() => null);
|
|
1505
|
-
if (!payload) throw new ApiError(`HTTP ${response.status}`, response.status);
|
|
1506
|
-
if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
|
|
1507
|
-
return payload.data;
|
|
1508
|
-
}
|
|
1509
|
-
async function listDir(path, password) {
|
|
1510
|
-
if (demo.value) {
|
|
1511
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
1512
|
-
const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
|
|
1513
|
-
if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
|
|
1514
|
-
return DEMO_TREE[path] ?? [];
|
|
1515
|
-
}
|
|
1516
|
-
return (await request("/api/fs/list", {
|
|
1517
|
-
path,
|
|
1518
|
-
password,
|
|
1519
|
-
page: 1,
|
|
1520
|
-
per_page: 0,
|
|
1521
|
-
refresh: false
|
|
1522
|
-
}))?.content ?? [];
|
|
1523
1842
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
return new URL(raw, `${root}/`).href;
|
|
1530
|
-
} catch {
|
|
1531
|
-
return raw;
|
|
1532
|
-
}
|
|
1843
|
+
/** Show a folder's same-row input; focus only on user actions, never on page load. */
|
|
1844
|
+
function startPrompt(node, focus) {
|
|
1845
|
+
node.lock ??= newLock();
|
|
1846
|
+
node.lock.prompting = true;
|
|
1847
|
+
if (focus) focusUnlock(node.path);
|
|
1533
1848
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
try {
|
|
1541
|
-
url = absoluteUrl((await request("/api/fs/get", {
|
|
1542
|
-
path: node.path,
|
|
1543
|
-
password: passwordFor(node).password
|
|
1544
|
-
}))?.raw_url ?? "");
|
|
1545
|
-
} catch {
|
|
1546
|
-
url = "";
|
|
1547
|
-
}
|
|
1548
|
-
if (!url) url = directUrl(node);
|
|
1549
|
-
downloadCache.set(node.path, url);
|
|
1550
|
-
return url;
|
|
1849
|
+
function cancelUnlock(lock) {
|
|
1850
|
+
Object.assign(lock, {
|
|
1851
|
+
prompting: false,
|
|
1852
|
+
input: "",
|
|
1853
|
+
error: ""
|
|
1854
|
+
});
|
|
1551
1855
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1856
|
+
function unlockErrorText(error) {
|
|
1857
|
+
if (error === "wrong") return t("passwordWrong");
|
|
1858
|
+
if (error === "stale") return t("passwordStale");
|
|
1859
|
+
return "";
|
|
1555
1860
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1861
|
+
/**
|
|
1862
|
+
* Try the typed password with `open`. On success keep it (and remember it in
|
|
1863
|
+
* this tab when asked); on a wrong one clear the input and refocus it.
|
|
1864
|
+
*/
|
|
1865
|
+
async function tryUnlock(lock, path, open, failure) {
|
|
1866
|
+
const password = lock.input;
|
|
1867
|
+
if (!password || lock.busy) return false;
|
|
1868
|
+
lock.busy = true;
|
|
1558
1869
|
try {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
} catch {
|
|
1563
|
-
|
|
1564
|
-
|
|
1870
|
+
await open(password);
|
|
1871
|
+
passwords.accept(path, password, lock.remember);
|
|
1872
|
+
return true;
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if (isPasswordError(error)) {
|
|
1875
|
+
Object.assign(lock, {
|
|
1876
|
+
error: "wrong",
|
|
1877
|
+
input: ""
|
|
1878
|
+
});
|
|
1879
|
+
focusUnlock(path);
|
|
1880
|
+
} else showToast(failureMessage(failure, error));
|
|
1881
|
+
return false;
|
|
1882
|
+
} finally {
|
|
1883
|
+
lock.busy = false;
|
|
1565
1884
|
}
|
|
1566
1885
|
}
|
|
1567
|
-
function
|
|
1568
|
-
|
|
1569
|
-
|
|
1886
|
+
async function submitUnlock(node) {
|
|
1887
|
+
if (!node.lock) return;
|
|
1888
|
+
if (await tryUnlock(node.lock, node.path, async (password) => {
|
|
1889
|
+
if (node.unresolved) {
|
|
1890
|
+
const data = await api.value.get(node.path, password);
|
|
1891
|
+
const name = node.name;
|
|
1892
|
+
Object.assign(node, data, {
|
|
1893
|
+
name,
|
|
1894
|
+
unresolved: false
|
|
1895
|
+
});
|
|
1896
|
+
node.zipPath = [node.is_dir ? name : data.name || node.zipPath[0]];
|
|
1897
|
+
}
|
|
1898
|
+
if (node.is_dir) {
|
|
1899
|
+
await fetchChildren(node, password);
|
|
1900
|
+
node.expanded = true;
|
|
1901
|
+
}
|
|
1902
|
+
}, "folderFailed")) node.lock = void 0;
|
|
1903
|
+
}
|
|
1904
|
+
async function submitRootUnlock() {
|
|
1905
|
+
if (!rootLock.value) return;
|
|
1906
|
+
let nodes = [];
|
|
1907
|
+
if (!await tryUnlock(rootLock.value, props.path, async (password) => {
|
|
1908
|
+
nodes = await listRoot(password);
|
|
1909
|
+
}, "loadFailed")) return;
|
|
1910
|
+
roots.value = nodes;
|
|
1911
|
+
rootLock.value = null;
|
|
1912
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
1913
|
+
api.value.clearCache();
|
|
1570
1914
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
depth,
|
|
1915
|
+
/** Forget a remembered password: the folder folds up and asks again next time. */
|
|
1916
|
+
function forgetPassword(node) {
|
|
1917
|
+
passwords.forget(node.path);
|
|
1918
|
+
Object.assign(node, {
|
|
1576
1919
|
expanded: false,
|
|
1577
1920
|
loaded: false,
|
|
1578
|
-
loading: false,
|
|
1579
1921
|
children: [],
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
function setSort(key) {
|
|
1585
|
-
if (sortKey.value === key) sortAsc.value = !sortAsc.value;
|
|
1586
|
-
else {
|
|
1587
|
-
sortKey.value = key;
|
|
1588
|
-
sortAsc.value = true;
|
|
1589
|
-
}
|
|
1590
|
-
resortAll();
|
|
1922
|
+
lock: void 0
|
|
1923
|
+
});
|
|
1924
|
+
selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
|
|
1925
|
+
api.value.clearCache(node.path);
|
|
1591
1926
|
}
|
|
1592
|
-
function
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1927
|
+
async function fetchChildren(node, password) {
|
|
1928
|
+
node.children = toNodes(await api.value.list(node.path, password), node, password);
|
|
1929
|
+
sortNodes(node.children);
|
|
1930
|
+
node.loaded = true;
|
|
1596
1931
|
}
|
|
1597
|
-
function
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1932
|
+
async function listRoot(password) {
|
|
1933
|
+
const nodes = toNodes(await api.value.list(props.path, password), listedFolder.value, password);
|
|
1934
|
+
sortNodes(nodes);
|
|
1935
|
+
return nodes;
|
|
1600
1936
|
}
|
|
1601
1937
|
/**
|
|
1602
|
-
* Load a folder's children. A password error
|
|
1938
|
+
* Load a folder's children. A password error locks the folder instead of
|
|
1603
1939
|
* toasting; `prompt` also shows its same-row input (and focuses it).
|
|
1604
1940
|
*/
|
|
1605
1941
|
async function loadChildren(node, prompt = false) {
|
|
1606
1942
|
if (node.loaded) return true;
|
|
1607
|
-
if (node.
|
|
1943
|
+
if (node.lock) {
|
|
1608
1944
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1609
1945
|
return false;
|
|
1610
1946
|
}
|
|
1611
1947
|
node.loading = true;
|
|
1612
|
-
const { password, rememberedAt } =
|
|
1948
|
+
const { password, rememberedAt } = passwords.resolve(node.path, node.password);
|
|
1613
1949
|
try {
|
|
1614
|
-
|
|
1615
|
-
sortTree(node.children);
|
|
1616
|
-
node.loaded = true;
|
|
1950
|
+
await fetchChildren(node, password);
|
|
1617
1951
|
return true;
|
|
1618
1952
|
} catch (error) {
|
|
1619
1953
|
if (isPasswordError(error)) {
|
|
1620
|
-
node.locked = true;
|
|
1621
1954
|
node.expanded = false;
|
|
1622
|
-
node.
|
|
1955
|
+
node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1623
1956
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1624
1957
|
} else showToast(failureMessage("folderFailed", error));
|
|
1625
1958
|
return false;
|
|
@@ -1627,63 +1960,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1627
1960
|
node.loading = false;
|
|
1628
1961
|
}
|
|
1629
1962
|
}
|
|
1630
|
-
/** Submit the same-row password for a locked folder (or a locked root of unknown type). */
|
|
1631
|
-
async function submitUnlock(node) {
|
|
1632
|
-
const password = node.unlockInput ?? "";
|
|
1633
|
-
if (!password || node.unlockBusy) return;
|
|
1634
|
-
node.unlockBusy = true;
|
|
1635
|
-
try {
|
|
1636
|
-
if (node.unresolved) {
|
|
1637
|
-
const data = await request("/api/fs/get", {
|
|
1638
|
-
path: node.path,
|
|
1639
|
-
password
|
|
1640
|
-
});
|
|
1641
|
-
const name = node.name;
|
|
1642
|
-
Object.assign(node, data, { name });
|
|
1643
|
-
if (node.is_dir) node.zipPath = [name];
|
|
1644
|
-
else node.zipPath = [data.name || node.zipPath[0]];
|
|
1645
|
-
node.unresolved = false;
|
|
1646
|
-
}
|
|
1647
|
-
if (node.is_dir) {
|
|
1648
|
-
node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, password, node.zipPath);
|
|
1649
|
-
sortTree(node.children);
|
|
1650
|
-
node.loaded = true;
|
|
1651
|
-
node.expanded = true;
|
|
1652
|
-
}
|
|
1653
|
-
node.password = password;
|
|
1654
|
-
typedPasswords.set(node.path, password);
|
|
1655
|
-
if (node.unlockRemember && canRemember.value) {
|
|
1656
|
-
rememberFolderPassword(passwordScope.value, node.path, password);
|
|
1657
|
-
syncRemembered();
|
|
1658
|
-
}
|
|
1659
|
-
node.locked = false;
|
|
1660
|
-
node.prompting = false;
|
|
1661
|
-
node.unlockError = "";
|
|
1662
|
-
node.unlockInput = "";
|
|
1663
|
-
node.unlockRemember = false;
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
if (isPasswordError(error)) {
|
|
1666
|
-
node.unlockError = "wrong";
|
|
1667
|
-
node.unlockInput = "";
|
|
1668
|
-
focusUnlock(node.path);
|
|
1669
|
-
} else showToast(failureMessage("folderFailed", error));
|
|
1670
|
-
} finally {
|
|
1671
|
-
node.unlockBusy = false;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
/** Forget a remembered password: the folder folds up and asks again next time. */
|
|
1675
|
-
function forgetPassword(node) {
|
|
1676
|
-
forgetFolderPassword(passwordScope.value, node.path);
|
|
1677
|
-
typedPasswords.delete(node.path);
|
|
1678
|
-
syncRemembered();
|
|
1679
|
-
node.password = node.configuredPassword || "";
|
|
1680
|
-
node.expanded = false;
|
|
1681
|
-
node.loaded = false;
|
|
1682
|
-
node.children = [];
|
|
1683
|
-
node.locked = false;
|
|
1684
|
-
selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
|
|
1685
|
-
for (const key of [...downloadCache.keys()]) if (isWithin(key, node.path)) downloadCache.delete(key);
|
|
1686
|
-
}
|
|
1687
1963
|
/** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
|
|
1688
1964
|
function looksLikeFile(path) {
|
|
1689
1965
|
const name = sourceName(path);
|
|
@@ -1691,44 +1967,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1691
1967
|
}
|
|
1692
1968
|
async function loadSourceRoot(source, expanded) {
|
|
1693
1969
|
const path = source.path;
|
|
1694
|
-
const
|
|
1970
|
+
const own = source.password ?? "";
|
|
1695
1971
|
let item = {
|
|
1696
1972
|
name: sourceName(path),
|
|
1697
1973
|
size: 0,
|
|
1698
1974
|
is_dir: !looksLikeFile(path),
|
|
1699
1975
|
modified: ""
|
|
1700
1976
|
};
|
|
1701
|
-
let
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
password: stored.password,
|
|
1711
|
-
rememberedAt: stored.path
|
|
1712
|
-
} : {
|
|
1713
|
-
password: props.password,
|
|
1714
|
-
rememberedAt: void 0
|
|
1715
|
-
};
|
|
1716
|
-
})();
|
|
1717
|
-
try {
|
|
1718
|
-
const data = await request("/api/fs/get", {
|
|
1719
|
-
path,
|
|
1720
|
-
password
|
|
1721
|
-
});
|
|
1722
|
-
item = {
|
|
1723
|
-
...item,
|
|
1724
|
-
...data
|
|
1725
|
-
};
|
|
1726
|
-
} catch (error) {
|
|
1727
|
-
if (isPasswordError(error)) {
|
|
1728
|
-
locked = true;
|
|
1729
|
-
unlockError = dropStale(rememberedAt) ? "stale" : "";
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1977
|
+
let lock;
|
|
1978
|
+
const { password, rememberedAt } = passwords.resolve(path, own);
|
|
1979
|
+
try {
|
|
1980
|
+
item = {
|
|
1981
|
+
...item,
|
|
1982
|
+
...await api.value.get(path, password)
|
|
1983
|
+
};
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
if (isPasswordError(error)) lock = newLock(expanded, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1732
1986
|
}
|
|
1733
1987
|
const name = source.title || item.name || sourceName(path);
|
|
1734
1988
|
return {
|
|
@@ -1736,17 +1990,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1736
1990
|
name,
|
|
1737
1991
|
path,
|
|
1738
1992
|
depth: 0,
|
|
1739
|
-
expanded,
|
|
1993
|
+
expanded: expanded && !lock,
|
|
1740
1994
|
loaded: false,
|
|
1741
1995
|
loading: false,
|
|
1742
1996
|
children: [],
|
|
1743
|
-
password:
|
|
1744
|
-
configuredPassword: configured,
|
|
1997
|
+
password: own,
|
|
1745
1998
|
isRoot: true,
|
|
1746
|
-
|
|
1747
|
-
unresolved:
|
|
1748
|
-
unlockError,
|
|
1749
|
-
prompting: locked && expanded,
|
|
1999
|
+
lock,
|
|
2000
|
+
unresolved: !!lock,
|
|
1750
2001
|
zipPath: [item.is_dir ? name : item.name || sourceName(path)]
|
|
1751
2002
|
};
|
|
1752
2003
|
}
|
|
@@ -1765,20 +2016,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1765
2016
|
let nodes;
|
|
1766
2017
|
if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
|
|
1767
2018
|
else {
|
|
1768
|
-
const { password, rememberedAt } =
|
|
1769
|
-
let items;
|
|
2019
|
+
const { password, rememberedAt } = passwords.resolve(props.path);
|
|
1770
2020
|
try {
|
|
1771
|
-
|
|
2021
|
+
nodes = await listRoot(password);
|
|
1772
2022
|
} catch (error) {
|
|
1773
|
-
|
|
1774
|
-
throw error;
|
|
2023
|
+
throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
|
|
1775
2024
|
}
|
|
1776
|
-
nodes = toNodes(items, props.path, 0, rootPassword.value);
|
|
1777
|
-
sortTree(nodes);
|
|
1778
2025
|
}
|
|
1779
2026
|
const open = async (list) => {
|
|
1780
2027
|
for (const node of list) {
|
|
1781
|
-
|
|
2028
|
+
const wanted = node.expanded || expandedPaths?.has(node.path);
|
|
2029
|
+
if (!node.is_dir || node.lock || !wanted) continue;
|
|
1782
2030
|
node.expanded = await loadChildren(node, "show");
|
|
1783
2031
|
if (node.expanded && expandedPaths) await open(node.children);
|
|
1784
2032
|
}
|
|
@@ -1786,6 +2034,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1786
2034
|
await open(nodes);
|
|
1787
2035
|
return nodes;
|
|
1788
2036
|
}
|
|
2037
|
+
function lockRoot(error, fallback) {
|
|
2038
|
+
roots.value = [];
|
|
2039
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
2040
|
+
rootLock.value = newLock(true, passwords.dropStale(error.rememberedAt) ? "stale" : fallback);
|
|
2041
|
+
}
|
|
1789
2042
|
/** First load, or a retry after an error: shows the loading state. */
|
|
1790
2043
|
async function loadRoot() {
|
|
1791
2044
|
refreshSeq += 1;
|
|
@@ -1794,33 +2047,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1794
2047
|
errorMsg.value = "";
|
|
1795
2048
|
try {
|
|
1796
2049
|
roots.value = await buildTree();
|
|
1797
|
-
|
|
2050
|
+
shown.clear();
|
|
2051
|
+
rootLock.value = null;
|
|
1798
2052
|
selected.value = /* @__PURE__ */ new Set();
|
|
1799
|
-
|
|
2053
|
+
api.value.clearCache();
|
|
1800
2054
|
} catch (error) {
|
|
1801
|
-
if (error instanceof RootLockedError)
|
|
1802
|
-
|
|
1803
|
-
rootLock.active = true;
|
|
1804
|
-
rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
|
|
1805
|
-
} else errorMsg.value = errorText(error);
|
|
2055
|
+
if (error instanceof RootLockedError) lockRoot(error, "");
|
|
2056
|
+
else errorMsg.value = errorText(error);
|
|
1806
2057
|
} finally {
|
|
1807
2058
|
loadingRoot.value = false;
|
|
1808
2059
|
}
|
|
1809
2060
|
}
|
|
1810
|
-
function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
|
|
1811
|
-
for (const node of nodes) if (node.is_dir && node.expanded) {
|
|
1812
|
-
out.add(node.path);
|
|
1813
|
-
expandedPathsOf(node.children, out);
|
|
1814
|
-
}
|
|
1815
|
-
return out;
|
|
1816
|
-
}
|
|
1817
2061
|
/**
|
|
1818
2062
|
* Refresh keeps the old rows in place (dimmed) until the new tree is ready, then
|
|
1819
2063
|
* swaps it in. Open folders stay open; selected files that still exist stay selected.
|
|
1820
2064
|
*/
|
|
1821
2065
|
async function refresh() {
|
|
1822
2066
|
if (refreshing.value || loadingRoot.value) return;
|
|
1823
|
-
if (!roots.value.length || errorMsg.value || rootLock.
|
|
2067
|
+
if (!roots.value.length || errorMsg.value || rootLock.value) {
|
|
1824
2068
|
await loadRoot();
|
|
1825
2069
|
return;
|
|
1826
2070
|
}
|
|
@@ -1832,54 +2076,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1832
2076
|
roots.value = fresh;
|
|
1833
2077
|
const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
|
|
1834
2078
|
selected.value = new Set([...selected.value].filter((path) => existing.has(path)));
|
|
1835
|
-
|
|
2079
|
+
api.value.clearCache();
|
|
1836
2080
|
} catch (error) {
|
|
1837
2081
|
if (seq !== refreshSeq) return;
|
|
1838
|
-
if (error instanceof RootLockedError)
|
|
1839
|
-
|
|
1840
|
-
rootLock.active = true;
|
|
1841
|
-
rootLock.error = dropStale(error.rememberedAt) ? "stale" : "wrong";
|
|
1842
|
-
selected.value = /* @__PURE__ */ new Set();
|
|
1843
|
-
} else showToast(failureMessage("refreshFailed", error));
|
|
2082
|
+
if (error instanceof RootLockedError) lockRoot(error, "wrong");
|
|
2083
|
+
else showToast(failureMessage("refreshFailed", error));
|
|
1844
2084
|
} finally {
|
|
1845
2085
|
if (seq === refreshSeq) refreshing.value = false;
|
|
1846
2086
|
}
|
|
1847
2087
|
}
|
|
1848
|
-
/** Single-path mode: unlock the listed folder itself. */
|
|
1849
|
-
async function submitRootUnlock() {
|
|
1850
|
-
const password = rootLock.input;
|
|
1851
|
-
if (!password || rootLock.busy) return;
|
|
1852
|
-
rootLock.busy = true;
|
|
1853
|
-
try {
|
|
1854
|
-
const items = await listDir(props.path, password);
|
|
1855
|
-
rootPassword.value = password;
|
|
1856
|
-
if (rootLock.remember && canRemember.value) {
|
|
1857
|
-
rememberFolderPassword(passwordScope.value, props.path, password);
|
|
1858
|
-
syncRemembered();
|
|
1859
|
-
}
|
|
1860
|
-
const nodes = toNodes(items, props.path, 0, password);
|
|
1861
|
-
sortTree(nodes);
|
|
1862
|
-
roots.value = nodes;
|
|
1863
|
-
Object.assign(rootLock, {
|
|
1864
|
-
active: false,
|
|
1865
|
-
input: "",
|
|
1866
|
-
error: "",
|
|
1867
|
-
remember: false
|
|
1868
|
-
});
|
|
1869
|
-
selected.value = /* @__PURE__ */ new Set();
|
|
1870
|
-
downloadCache.clear();
|
|
1871
|
-
} catch (error) {
|
|
1872
|
-
if (isPasswordError(error)) {
|
|
1873
|
-
rootLock.error = "wrong";
|
|
1874
|
-
rootLock.input = "";
|
|
1875
|
-
focusUnlock(props.path);
|
|
1876
|
-
} else showToast(failureMessage("loadFailed", error));
|
|
1877
|
-
} finally {
|
|
1878
|
-
rootLock.busy = false;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
2088
|
async function toggleExpand(node) {
|
|
1882
|
-
if (node.
|
|
2089
|
+
if (node.lock) {
|
|
1883
2090
|
startPrompt(node, true);
|
|
1884
2091
|
return;
|
|
1885
2092
|
}
|
|
@@ -1888,35 +2095,92 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1888
2095
|
node.expanded = false;
|
|
1889
2096
|
return;
|
|
1890
2097
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
2098
|
+
node.expanded = await loadChildren(node, "focus");
|
|
2099
|
+
}
|
|
2100
|
+
function applySort(key, ascending) {
|
|
2101
|
+
sortKey.value = key;
|
|
2102
|
+
sortAsc.value = ascending;
|
|
2103
|
+
if (props.sources.length) roots.value.forEach((root) => sortNodes(root.children));
|
|
2104
|
+
else sortNodes(roots.value);
|
|
2105
|
+
}
|
|
2106
|
+
function setSort(key) {
|
|
2107
|
+
applySort(key, sortKey.value === key ? !sortAsc.value : true);
|
|
2108
|
+
}
|
|
2109
|
+
/** Compact sort control for narrow containers, where header cells are hidden. */
|
|
2110
|
+
const sortOptions = computed(() => [
|
|
2111
|
+
"name",
|
|
2112
|
+
"size",
|
|
2113
|
+
"modified"
|
|
2114
|
+
].flatMap((key) => {
|
|
2115
|
+
const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
|
|
2116
|
+
return [{
|
|
2117
|
+
value: `${key}:asc`,
|
|
2118
|
+
label: `${label} ↑`
|
|
2119
|
+
}, {
|
|
2120
|
+
value: `${key}:desc`,
|
|
2121
|
+
label: `${label} ↓`
|
|
2122
|
+
}];
|
|
2123
|
+
}));
|
|
2124
|
+
const sortValue = computed({
|
|
2125
|
+
get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
|
|
2126
|
+
set: (value) => {
|
|
2127
|
+
const [key, direction] = value.split(":");
|
|
2128
|
+
applySort(key, direction !== "desc");
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
/** A “show more” row closing a folder whose children are cut at `pageSize`. */
|
|
2132
|
+
const shown = reactive(/* @__PURE__ */ new Map());
|
|
2133
|
+
function limitOf(key) {
|
|
2134
|
+
const size = Math.max(0, Math.floor(props.pageSize ?? 200));
|
|
2135
|
+
return size ? shown.get(key) ?? size : Number.POSITIVE_INFINITY;
|
|
2136
|
+
}
|
|
2137
|
+
function showMore(row, all) {
|
|
2138
|
+
const size = Math.max(1, Math.floor(props.pageSize ?? 200));
|
|
2139
|
+
shown.set(row.key, all ? Number.POSITIVE_INFINITY : limitOf(row.key) + size);
|
|
1893
2140
|
}
|
|
1894
|
-
|
|
2141
|
+
function isMore(row) {
|
|
2142
|
+
return "more" in row;
|
|
2143
|
+
}
|
|
2144
|
+
const rows = computed(() => {
|
|
1895
2145
|
const out = [];
|
|
1896
2146
|
const query = keyword.value.trim().toLowerCase();
|
|
1897
2147
|
const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
|
|
1898
|
-
const walk = (nodes) => {
|
|
1899
|
-
|
|
1900
|
-
|
|
2148
|
+
const walk = (nodes, key, depth) => {
|
|
2149
|
+
const list = nodes.filter(matches);
|
|
2150
|
+
const limit = query ? list.length : Math.min(list.length, limitOf(key));
|
|
2151
|
+
for (const node of list.slice(0, limit)) {
|
|
1901
2152
|
out.push(node);
|
|
1902
|
-
if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children);
|
|
2153
|
+
if (node.is_dir && (node.expanded || query) && node.children.length) walk(node.children, node.path, node.depth + 1);
|
|
1903
2154
|
}
|
|
2155
|
+
if (limit < list.length) out.push({
|
|
2156
|
+
more: true,
|
|
2157
|
+
key,
|
|
2158
|
+
depth,
|
|
2159
|
+
remaining: list.length - limit
|
|
2160
|
+
});
|
|
1904
2161
|
};
|
|
1905
|
-
walk(roots.value);
|
|
2162
|
+
walk(roots.value, props.sources.length ? "" : props.path, 0);
|
|
1906
2163
|
return out;
|
|
1907
2164
|
});
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2165
|
+
const visible = computed(() => rows.value.filter((row) => !isMore(row)));
|
|
2166
|
+
const stats = computed(() => folderStats(roots.value, selected.value));
|
|
2167
|
+
const noStats = {
|
|
2168
|
+
files: 0,
|
|
2169
|
+
bytes: 0,
|
|
2170
|
+
selected: 0
|
|
2171
|
+
};
|
|
2172
|
+
function statsOf(node) {
|
|
2173
|
+
return stats.value.get(node.path) ?? noStats;
|
|
1911
2174
|
}
|
|
1912
2175
|
function isChecked(node) {
|
|
1913
|
-
|
|
1914
|
-
|
|
2176
|
+
if (!node.is_dir) return selected.value.has(node.path);
|
|
2177
|
+
const { files, selected: hit } = statsOf(node);
|
|
2178
|
+
return files > 0 && hit === files;
|
|
1915
2179
|
}
|
|
1916
2180
|
function isIndeterminate(node) {
|
|
1917
|
-
|
|
1918
|
-
const hit =
|
|
1919
|
-
return hit > 0 && hit < files
|
|
2181
|
+
if (!node.is_dir) return false;
|
|
2182
|
+
const { files, selected: hit } = statsOf(node);
|
|
2183
|
+
return hit > 0 && hit < files;
|
|
1920
2184
|
}
|
|
1921
2185
|
/**
|
|
1922
2186
|
* Load a directory and all of its sub-directories. Stops at the first folder
|
|
@@ -1924,7 +2188,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1924
2188
|
*/
|
|
1925
2189
|
async function loadTree(node) {
|
|
1926
2190
|
if (!node.is_dir) return { ok: true };
|
|
1927
|
-
if (!await loadChildren(node)) return node.
|
|
2191
|
+
if (!await loadChildren(node)) return node.lock ? {
|
|
1928
2192
|
ok: false,
|
|
1929
2193
|
locked: node
|
|
1930
2194
|
} : { ok: false };
|
|
@@ -1942,12 +2206,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1942
2206
|
const input = event?.target;
|
|
1943
2207
|
if (node.is_dir && !isChecked(node)) {
|
|
1944
2208
|
const result = await loadTree(node);
|
|
1945
|
-
if (result.ok)
|
|
1946
|
-
else {
|
|
2209
|
+
if (!result.ok) {
|
|
1947
2210
|
if (result.locked) startPrompt(result.locked, true);
|
|
1948
2211
|
if (input) input.checked = isChecked(node);
|
|
1949
2212
|
return;
|
|
1950
2213
|
}
|
|
2214
|
+
node.expanded = true;
|
|
1951
2215
|
}
|
|
1952
2216
|
const files = descendantFiles(node);
|
|
1953
2217
|
if (!files.length) {
|
|
@@ -1955,67 +2219,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1955
2219
|
return;
|
|
1956
2220
|
}
|
|
1957
2221
|
const next = new Set(selected.value);
|
|
1958
|
-
|
|
1959
|
-
|
|
2222
|
+
const adding = files.some((file) => !next.has(file.path));
|
|
2223
|
+
for (const file of files) if (adding) next.add(file.path);
|
|
2224
|
+
else next.delete(file.path);
|
|
1960
2225
|
selected.value = next;
|
|
1961
2226
|
}
|
|
1962
2227
|
const allFiles = computed(() => roots.value.flatMap(descendantFiles));
|
|
1963
|
-
const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
|
|
1964
|
-
const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
|
|
1965
2228
|
const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
1968
|
-
function clearSelection() {
|
|
1969
|
-
selected.value = /* @__PURE__ */ new Set();
|
|
1970
|
-
}
|
|
1971
|
-
function folderFiles(node) {
|
|
1972
|
-
return node.is_dir && node.loaded ? descendantFiles(node) : [];
|
|
1973
|
-
}
|
|
1974
|
-
/** Compact sort control for narrow containers, where header cells are hidden. */
|
|
1975
|
-
const sortOptions = computed(() => [
|
|
1976
|
-
"name",
|
|
1977
|
-
"size",
|
|
1978
|
-
"modified"
|
|
1979
|
-
].flatMap((key) => {
|
|
1980
|
-
const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
|
|
1981
|
-
return [{
|
|
1982
|
-
value: `${key}:asc`,
|
|
1983
|
-
label: `${label} ↑`
|
|
1984
|
-
}, {
|
|
1985
|
-
value: `${key}:desc`,
|
|
1986
|
-
label: `${label} ↓`
|
|
1987
|
-
}];
|
|
1988
|
-
}));
|
|
1989
|
-
const sortValue = computed({
|
|
1990
|
-
get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
|
|
1991
|
-
set: (value) => {
|
|
1992
|
-
const [key, direction] = value.split(":");
|
|
1993
|
-
sortKey.value = key;
|
|
1994
|
-
sortAsc.value = direction !== "desc";
|
|
1995
|
-
resortAll();
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
2229
|
+
const allChecked = computed(() => allFiles.value.length > 0 && selectedFiles.value.length === allFiles.value.length);
|
|
2230
|
+
const someChecked = computed(() => !allChecked.value && selectedFiles.value.length > 0);
|
|
1998
2231
|
function toggleCheckAll() {
|
|
1999
2232
|
selected.value = allChecked.value ? /* @__PURE__ */ new Set() : new Set(allFiles.value.map((file) => file.path));
|
|
2000
2233
|
}
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
document.body.appendChild(anchor);
|
|
2007
|
-
anchor.click();
|
|
2008
|
-
anchor.remove();
|
|
2009
|
-
}
|
|
2010
|
-
function triggerBlob(blob, name) {
|
|
2011
|
-
const href = URL.createObjectURL(blob);
|
|
2012
|
-
const anchor = document.createElement("a");
|
|
2013
|
-
anchor.href = href;
|
|
2014
|
-
anchor.download = name;
|
|
2015
|
-
document.body.appendChild(anchor);
|
|
2016
|
-
anchor.click();
|
|
2017
|
-
anchor.remove();
|
|
2018
|
-
URL.revokeObjectURL(href);
|
|
2234
|
+
function clearSelection() {
|
|
2235
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
2236
|
+
}
|
|
2237
|
+
function fileUrlOf(node) {
|
|
2238
|
+
return api.value.fileUrl(node, passwords.resolve(node.path, node.password).password);
|
|
2019
2239
|
}
|
|
2020
2240
|
async function download(node) {
|
|
2021
2241
|
if (node.is_dir) return;
|
|
@@ -2024,11 +2244,109 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2024
2244
|
return;
|
|
2025
2245
|
}
|
|
2026
2246
|
try {
|
|
2027
|
-
|
|
2247
|
+
openUrl(await fileUrlOf(node));
|
|
2028
2248
|
} catch (error) {
|
|
2029
2249
|
showToast(t("downloadFailed", { message: errorText(error) }));
|
|
2030
2250
|
}
|
|
2031
2251
|
}
|
|
2252
|
+
function zipLimits() {
|
|
2253
|
+
const touch = typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches;
|
|
2254
|
+
const bytes = (value, fallback) => {
|
|
2255
|
+
const mb = value ?? fallback;
|
|
2256
|
+
return mb > 0 ? mb * MB : Number.POSITIVE_INFINITY;
|
|
2257
|
+
};
|
|
2258
|
+
return {
|
|
2259
|
+
warn: bytes(props.zipWarnSize, touch ? 200 : 500),
|
|
2260
|
+
max: bytes(props.zipMaxSize, touch ? 1024 : 2048)
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
const zipNotice = ref(null);
|
|
2264
|
+
/** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
|
|
2265
|
+
const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
|
|
2266
|
+
function zipName() {
|
|
2267
|
+
return zipFileName(props.title || t("defaultZip"));
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Chrome / Edge stream into a file the user picks (no size limit). Other
|
|
2271
|
+
* browsers build the ZIP in memory, so large selections ask first or are refused.
|
|
2272
|
+
*/
|
|
2273
|
+
async function batchDownload() {
|
|
2274
|
+
const files = selectedFiles.value;
|
|
2275
|
+
if (!files.length || batchBusy.value) return;
|
|
2276
|
+
if (demo.value) {
|
|
2277
|
+
showToast(t("demoBatch", { count: files.length }));
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (canStreamToDisk()) {
|
|
2281
|
+
let handle;
|
|
2282
|
+
try {
|
|
2283
|
+
handle = await pickZipFile(zipName());
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
showToast(t("batchFailed", { message: errorText(error) }));
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (handle) await runZip(files, (options) => zipToFile(handle, files, options), handle.name);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const size = totalSize(files);
|
|
2292
|
+
const { warn, max } = zipLimits();
|
|
2293
|
+
if (size > max) zipNotice.value = {
|
|
2294
|
+
kind: "block",
|
|
2295
|
+
size,
|
|
2296
|
+
limit: max,
|
|
2297
|
+
files
|
|
2298
|
+
};
|
|
2299
|
+
else if (size > warn) zipNotice.value = {
|
|
2300
|
+
kind: "warn",
|
|
2301
|
+
size,
|
|
2302
|
+
limit: warn,
|
|
2303
|
+
files
|
|
2304
|
+
};
|
|
2305
|
+
else await zipInMemory(files);
|
|
2306
|
+
}
|
|
2307
|
+
async function zipInMemory(files) {
|
|
2308
|
+
const name = zipName();
|
|
2309
|
+
await runZip(files, async (options) => saveBlob(await zipToBlob(files, options), name), name);
|
|
2310
|
+
}
|
|
2311
|
+
function confirmZip() {
|
|
2312
|
+
const notice = zipNotice.value;
|
|
2313
|
+
zipNotice.value = null;
|
|
2314
|
+
if (notice?.kind === "warn") zipInMemory(notice.files);
|
|
2315
|
+
}
|
|
2316
|
+
async function runZip(files, run, name) {
|
|
2317
|
+
const controller = new AbortController();
|
|
2318
|
+
zipAbort = controller;
|
|
2319
|
+
batchBusy.value = true;
|
|
2320
|
+
zipProgress.value = null;
|
|
2321
|
+
try {
|
|
2322
|
+
await run({
|
|
2323
|
+
urlOf: fileUrlOf,
|
|
2324
|
+
signal: controller.signal,
|
|
2325
|
+
onProgress: (progress) => zipProgress.value = progress
|
|
2326
|
+
});
|
|
2327
|
+
showToast(t("batchDone", {
|
|
2328
|
+
name,
|
|
2329
|
+
count: files.length,
|
|
2330
|
+
size: formatSize(totalSize(files))
|
|
2331
|
+
}));
|
|
2332
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
showToast(controller.signal.aborted ? t("batchCanceled") : t("batchFailed", { message: errorText(error) }));
|
|
2335
|
+
} finally {
|
|
2336
|
+
zipAbort = null;
|
|
2337
|
+
batchBusy.value = false;
|
|
2338
|
+
zipProgress.value = null;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function cancelZip() {
|
|
2342
|
+
zipAbort?.abort();
|
|
2343
|
+
}
|
|
2344
|
+
const zipPercent = computed(() => {
|
|
2345
|
+
const progress = zipProgress.value;
|
|
2346
|
+
if (!progress) return 0;
|
|
2347
|
+
const ratio = progress.totalBytes ? progress.bytes / progress.totalBytes : progress.totalFiles ? progress.files / progress.totalFiles : 0;
|
|
2348
|
+
return Math.min(100, Math.floor(ratio * 100));
|
|
2349
|
+
});
|
|
2032
2350
|
/** Only types the dialog can actually render get a preview button. */
|
|
2033
2351
|
function canPreview(node) {
|
|
2034
2352
|
return !node.is_dir && previewKind(node.name) !== "none";
|
|
@@ -2052,17 +2370,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2052
2370
|
if (demo.value || !canPreview(node)) return;
|
|
2053
2371
|
previewResolving.value = true;
|
|
2054
2372
|
try {
|
|
2055
|
-
const url = await
|
|
2373
|
+
const url = await fileUrlOf(node);
|
|
2056
2374
|
if (seq !== previewSeq) return;
|
|
2057
2375
|
previewSrc.value = url;
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
});
|
|
2065
|
-
}
|
|
2376
|
+
const viewer = previewKind(node.name) === "pdf" ? iframePreviewsFor(node.name, iframePreviews.value)[0] : void 0;
|
|
2377
|
+
if (viewer) previewViewer.value = fillPreviewUrl(viewer.scheme, {
|
|
2378
|
+
name: node.name,
|
|
2379
|
+
rawUrl: url,
|
|
2380
|
+
downloadUrl: api.value.directUrl(node)
|
|
2381
|
+
});
|
|
2066
2382
|
} catch (error) {
|
|
2067
2383
|
if (seq === previewSeq) previewError.value = errorText(error);
|
|
2068
2384
|
} finally {
|
|
@@ -2078,49 +2394,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2078
2394
|
previewSeq += 1;
|
|
2079
2395
|
previewNode.value = null;
|
|
2080
2396
|
}
|
|
2081
|
-
async function batchDownload() {
|
|
2082
|
-
const files = allFiles.value.filter((file) => selected.value.has(file.path));
|
|
2083
|
-
if (!files.length) return;
|
|
2084
|
-
if (demo.value) {
|
|
2085
|
-
showToast(t("demoBatch", { count: files.length }));
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
batchBusy.value = true;
|
|
2089
|
-
batchProgress.value = 0;
|
|
2090
|
-
try {
|
|
2091
|
-
const entries = {};
|
|
2092
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
2093
|
-
const queue = [...files];
|
|
2094
|
-
let done = 0;
|
|
2095
|
-
const worker = async () => {
|
|
2096
|
-
while (queue.length) {
|
|
2097
|
-
const file = queue.shift();
|
|
2098
|
-
if (!file) break;
|
|
2099
|
-
const response = await fetch(await resolveUrl(file));
|
|
2100
|
-
if (!response.ok) throw new Error(`${file.name}:HTTP ${response.status}`);
|
|
2101
|
-
entries[uniqueEntryName(zipEntryName(file.zipPath), usedNames)] = new Uint8Array(await response.arrayBuffer());
|
|
2102
|
-
done += 1;
|
|
2103
|
-
batchProgress.value = Math.round(done / files.length * 90);
|
|
2104
|
-
}
|
|
2105
|
-
};
|
|
2106
|
-
await Promise.all(Array.from({ length: Math.min(3, files.length) }, () => worker()));
|
|
2107
|
-
const { zip } = await import("fflate");
|
|
2108
|
-
const data = await new Promise((resolve, reject) => {
|
|
2109
|
-
zip(entries, (error, output) => error ? reject(error) : resolve(output));
|
|
2110
|
-
});
|
|
2111
|
-
batchProgress.value = 100;
|
|
2112
|
-
triggerBlob(new Blob([data], { type: "application/zip" }), `${(props.title || t("defaultZip")).replace(/[\\/:*?"<>|]/g, "_")}.zip`);
|
|
2113
|
-
selected.value = /* @__PURE__ */ new Set();
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
showToast(t("batchFailed", { message: errorText(error) }));
|
|
2116
|
-
} finally {
|
|
2117
|
-
batchBusy.value = false;
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
2397
|
const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
|
|
2398
|
+
async function loadSettings() {
|
|
2399
|
+
const settings = await api.value.settings();
|
|
2400
|
+
siteTitle.value = settings.title;
|
|
2401
|
+
iframePreviews.value = settings.iframePreviews;
|
|
2402
|
+
}
|
|
2121
2403
|
const stickyTop = ref(0);
|
|
2122
2404
|
function measureStickyTop() {
|
|
2123
|
-
if (typeof document === "undefined") return;
|
|
2124
2405
|
let top = 0;
|
|
2125
2406
|
for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
|
|
2126
2407
|
const style = getComputedStyle(el);
|
|
@@ -2130,13 +2411,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2130
2411
|
stickyTop.value = top;
|
|
2131
2412
|
}
|
|
2132
2413
|
onMounted(() => {
|
|
2133
|
-
|
|
2414
|
+
passwords.sync();
|
|
2134
2415
|
loadRoot();
|
|
2135
|
-
|
|
2416
|
+
loadSettings();
|
|
2136
2417
|
measureStickyTop();
|
|
2137
2418
|
window.addEventListener("resize", measureStickyTop, { passive: true });
|
|
2138
2419
|
});
|
|
2139
2420
|
onBeforeUnmount(() => {
|
|
2421
|
+
zipAbort?.abort();
|
|
2140
2422
|
window.removeEventListener("resize", measureStickyTop);
|
|
2141
2423
|
clearTimeout(toastTimer);
|
|
2142
2424
|
});
|
|
@@ -2152,490 +2434,367 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2152
2434
|
"aria-busy": refreshing.value || loadingRoot.value
|
|
2153
2435
|
}, [
|
|
2154
2436
|
createElementVNode("div", _hoisted_2, [
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2437
|
+
batchBusy.value ? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
2438
|
+
createElementVNode("span", _hoisted_4, toDisplayString(t("batching", { percent: zipPercent.value })), 1),
|
|
2439
|
+
zipProgress.value ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(t("batchProgress", {
|
|
2440
|
+
done: unref(formatSize)(zipProgress.value.bytes),
|
|
2441
|
+
total: unref(formatSize)(zipProgress.value.totalBytes),
|
|
2442
|
+
files: zipProgress.value.files,
|
|
2443
|
+
count: zipProgress.value.totalFiles
|
|
2444
|
+
})), 1)) : createCommentVNode("v-if", true),
|
|
2445
|
+
createElementVNode("span", {
|
|
2446
|
+
class: "fl-zip-bar",
|
|
2447
|
+
style: normalizeStyle({ width: `${zipPercent.value}%` }),
|
|
2448
|
+
"aria-hidden": "true"
|
|
2449
|
+
}, null, 4)
|
|
2450
|
+
])) : selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("span", _hoisted_7, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_8, toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(selectedFiles.value)) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_9, [createElementVNode("span", _hoisted_10, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_11, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
|
|
2451
|
+
selected.value.size && !batchBusy.value ? (openBlock(), createElementBlock("button", {
|
|
2452
|
+
key: 3,
|
|
2158
2453
|
class: "fl-clear",
|
|
2159
2454
|
type: "button",
|
|
2160
2455
|
"aria-label": t("clearSelection"),
|
|
2161
2456
|
title: t("clearSelection"),
|
|
2162
2457
|
onClick: clearSelection
|
|
2163
|
-
}, [
|
|
2458
|
+
}, [createVNode(_sfc_main$2, {
|
|
2164
2459
|
class: "fl-clear-icon",
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
"aria-hidden": "true"
|
|
2173
|
-
}, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)), createElementVNode("span", _hoisted_10, toDisplayString(t("clear")), 1)], 8, _hoisted_9)) : createCommentVNode("v-if", true),
|
|
2174
|
-
createElementVNode("div", _hoisted_11, [createElementVNode("label", _hoisted_12, [_cache[11] || (_cache[11] = createElementVNode("svg", {
|
|
2175
|
-
viewBox: "0 0 24 24",
|
|
2176
|
-
width: "15",
|
|
2177
|
-
height: "15",
|
|
2178
|
-
fill: "none",
|
|
2179
|
-
stroke: "currentColor",
|
|
2180
|
-
"stroke-width": "2",
|
|
2181
|
-
"stroke-linecap": "round",
|
|
2182
|
-
"aria-hidden": "true"
|
|
2183
|
-
}, [createElementVNode("circle", {
|
|
2184
|
-
cx: "11",
|
|
2185
|
-
cy: "11",
|
|
2186
|
-
r: "7"
|
|
2187
|
-
}), createElementVNode("path", { d: "m20 20-3.5-3.5" })], -1)), withDirectives(createElementVNode("input", {
|
|
2460
|
+
name: "close",
|
|
2461
|
+
size: 18
|
|
2462
|
+
}), createElementVNode("span", _hoisted_13, toDisplayString(t("clear")), 1)], 8, _hoisted_12)) : createCommentVNode("v-if", true),
|
|
2463
|
+
createElementVNode("div", _hoisted_14, [createElementVNode("label", _hoisted_15, [createVNode(_sfc_main$2, {
|
|
2464
|
+
name: "search",
|
|
2465
|
+
size: 15
|
|
2466
|
+
}), withDirectives(createElementVNode("input", {
|
|
2188
2467
|
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
|
|
2189
2468
|
type: "text",
|
|
2190
2469
|
placeholder: t("filter"),
|
|
2191
2470
|
"aria-label": t("filter")
|
|
2192
|
-
}, null, 8,
|
|
2471
|
+
}, null, 8, _hoisted_16), [[vModelText, keyword.value]])]), createElementVNode("button", {
|
|
2193
2472
|
class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
|
|
2194
2473
|
type: "button",
|
|
2195
2474
|
disabled: refreshing.value || loadingRoot.value,
|
|
2196
2475
|
"aria-label": refreshing.value ? t("refreshing") : t("refresh"),
|
|
2197
2476
|
title: refreshing.value ? t("refreshing") : t("refresh"),
|
|
2198
2477
|
onClick: refresh
|
|
2199
|
-
}, [
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
"
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
selected.value.size ? (openBlock(), createElementBlock("button", {
|
|
2211
|
-
key: 3,
|
|
2478
|
+
}, [createVNode(_sfc_main$2, {
|
|
2479
|
+
name: "refresh",
|
|
2480
|
+
size: 15
|
|
2481
|
+
})], 10, _hoisted_17)]),
|
|
2482
|
+
batchBusy.value ? (openBlock(), createElementBlock("button", {
|
|
2483
|
+
key: 4,
|
|
2484
|
+
class: "fl-btn fl-zip-cancel",
|
|
2485
|
+
type: "button",
|
|
2486
|
+
onClick: cancelZip
|
|
2487
|
+
}, toDisplayString(t("cancel")), 1)) : selected.value.size ? (openBlock(), createElementBlock("button", {
|
|
2488
|
+
key: 5,
|
|
2212
2489
|
class: "fl-btn fl-btn-primary",
|
|
2213
2490
|
type: "button",
|
|
2214
|
-
disabled: batchBusy.value,
|
|
2215
2491
|
onClick: batchDownload
|
|
2216
|
-
}, [
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
fill: "none",
|
|
2221
|
-
stroke: "currentColor",
|
|
2222
|
-
"stroke-width": "2.2",
|
|
2223
|
-
"stroke-linecap": "round",
|
|
2224
|
-
"stroke-linejoin": "round",
|
|
2225
|
-
"aria-hidden": "true"
|
|
2226
|
-
}, [
|
|
2227
|
-
createElementVNode("path", { d: "M12 3v12" }),
|
|
2228
|
-
createElementVNode("path", { d: "m7 10 5 5 5-5" }),
|
|
2229
|
-
createElementVNode("path", { d: "M5 21h14" })
|
|
2230
|
-
], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_15)) : createCommentVNode("v-if", true)
|
|
2492
|
+
}, [createVNode(_sfc_main$2, {
|
|
2493
|
+
name: "download",
|
|
2494
|
+
size: 15
|
|
2495
|
+
}), createTextVNode(" " + toDisplayString(t("batch")), 1)])) : createCommentVNode("v-if", true)
|
|
2231
2496
|
]),
|
|
2232
|
-
createElementVNode("div",
|
|
2233
|
-
refreshing.value ? (openBlock(), createElementBlock("span",
|
|
2234
|
-
createElementVNode("label",
|
|
2497
|
+
createElementVNode("div", _hoisted_18, [
|
|
2498
|
+
refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_19)) : createCommentVNode("v-if", true),
|
|
2499
|
+
createElementVNode("label", _hoisted_20, [createElementVNode("input", {
|
|
2235
2500
|
type: "checkbox",
|
|
2236
2501
|
"aria-label": t("selectAll"),
|
|
2502
|
+
disabled: batchBusy.value,
|
|
2237
2503
|
checked: allChecked.value,
|
|
2238
2504
|
".indeterminate": someChecked.value,
|
|
2239
2505
|
onChange: toggleCheckAll
|
|
2240
|
-
}, null, 40,
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
"
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
createElementVNode("span", _hoisted_27, [createElementVNode("label", _hoisted_28, [
|
|
2506
|
+
}, null, 40, _hoisted_21)]),
|
|
2507
|
+
(openBlock(), createElementBlock(Fragment, null, renderList([
|
|
2508
|
+
"name",
|
|
2509
|
+
"size",
|
|
2510
|
+
"modified"
|
|
2511
|
+
], (column) => {
|
|
2512
|
+
return createElementVNode("span", {
|
|
2513
|
+
key: column,
|
|
2514
|
+
class: normalizeClass(["fl-cell", `fl-cell-${column === "modified" ? "date" : column}`])
|
|
2515
|
+
}, [createElementVNode("button", {
|
|
2516
|
+
class: "fl-sort-btn",
|
|
2517
|
+
type: "button",
|
|
2518
|
+
"aria-sort": sortKey.value === column ? sortAsc.value ? "ascending" : "descending" : void 0,
|
|
2519
|
+
onClick: ($event) => setSort(column)
|
|
2520
|
+
}, [createTextVNode(toDisplayString(t(column === "name" ? "columnName" : column === "size" ? "columnSize" : "columnDate")), 1), createElementVNode("span", _hoisted_23, toDisplayString(sortKey.value === column ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_22)], 2);
|
|
2521
|
+
}), 64)),
|
|
2522
|
+
createElementVNode("span", _hoisted_24, [createElementVNode("label", _hoisted_25, [
|
|
2258
2523
|
createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
|
|
2259
|
-
createElementVNode("span",
|
|
2524
|
+
createElementVNode("span", _hoisted_26, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
|
|
2260
2525
|
withDirectives(createElementVNode("select", {
|
|
2261
|
-
"onUpdate:modelValue": _cache[
|
|
2526
|
+
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => sortValue.value = $event),
|
|
2262
2527
|
"aria-label": t("sortBy")
|
|
2263
2528
|
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
|
|
2264
2529
|
return openBlock(), createElementBlock("option", {
|
|
2265
2530
|
key: option.value,
|
|
2266
2531
|
value: option.value
|
|
2267
|
-
}, toDisplayString(option.label), 9,
|
|
2268
|
-
}), 128))], 8,
|
|
2532
|
+
}, toDisplayString(option.label), 9, _hoisted_28);
|
|
2533
|
+
}), 128))], 8, _hoisted_27), [[vModelSelect, sortValue.value]])
|
|
2269
2534
|
])])
|
|
2270
2535
|
]),
|
|
2271
|
-
loadingRoot.value ? (openBlock(), createElementBlock("div",
|
|
2536
|
+
loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_29, toDisplayString(t("loading")), 1)) : rootLock.value ? (openBlock(), createElementBlock("form", {
|
|
2272
2537
|
key: 1,
|
|
2273
|
-
class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.error }]),
|
|
2538
|
+
class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.value.error }]),
|
|
2274
2539
|
onSubmit: withModifiers(submitRootUnlock, ["prevent"])
|
|
2275
2540
|
}, [
|
|
2276
|
-
|
|
2541
|
+
createVNode(_sfc_main$2, {
|
|
2277
2542
|
class: "fl-unlock-lock",
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
height: "16",
|
|
2281
|
-
fill: "none",
|
|
2282
|
-
stroke: "currentColor",
|
|
2283
|
-
"stroke-width": "2.2",
|
|
2284
|
-
"stroke-linecap": "round",
|
|
2285
|
-
"stroke-linejoin": "round",
|
|
2286
|
-
"aria-hidden": "true"
|
|
2287
|
-
}, [createElementVNode("rect", {
|
|
2288
|
-
x: "5",
|
|
2289
|
-
y: "11",
|
|
2290
|
-
width: "14",
|
|
2291
|
-
height: "10",
|
|
2292
|
-
rx: "2"
|
|
2293
|
-
}), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
|
|
2543
|
+
name: "lock"
|
|
2544
|
+
}),
|
|
2294
2545
|
createElementVNode("label", {
|
|
2295
2546
|
class: "fl-unlock-label",
|
|
2296
|
-
for:
|
|
2297
|
-
}, toDisplayString(t("passwordPrompt")), 9,
|
|
2298
|
-
rootLock.error ? (openBlock(), createElementBlock("span",
|
|
2299
|
-
|
|
2547
|
+
for: `${unref(uid)}-root`
|
|
2548
|
+
}, toDisplayString(t("passwordPrompt")), 9, _hoisted_30),
|
|
2549
|
+
rootLock.value.error ? (openBlock(), createElementBlock("span", {
|
|
2550
|
+
key: 0,
|
|
2551
|
+
id: `${unref(uid)}-root-error`,
|
|
2552
|
+
class: "fl-unlock-message",
|
|
2553
|
+
role: "alert"
|
|
2554
|
+
}, toDisplayString(unlockErrorText(rootLock.value.error)), 9, _hoisted_31)) : createCommentVNode("v-if", true),
|
|
2555
|
+
createElementVNode("span", _hoisted_32, [
|
|
2300
2556
|
withDirectives(createElementVNode("input", {
|
|
2301
|
-
id:
|
|
2302
|
-
"onUpdate:modelValue": _cache[
|
|
2557
|
+
id: `${unref(uid)}-root`,
|
|
2558
|
+
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => rootLock.value.input = $event),
|
|
2303
2559
|
type: "password",
|
|
2304
2560
|
autocomplete: "off",
|
|
2305
2561
|
"data-unlock": __props.path,
|
|
2306
2562
|
placeholder: t("passwordPlaceholder"),
|
|
2307
|
-
"aria-invalid": !!rootLock.error,
|
|
2308
|
-
"aria-describedby": rootLock.error ?
|
|
2309
|
-
}, null, 8,
|
|
2563
|
+
"aria-invalid": !!rootLock.value.error,
|
|
2564
|
+
"aria-describedby": rootLock.value.error ? `${unref(uid)}-root-error` : void 0
|
|
2565
|
+
}, null, 8, _hoisted_33), [[vModelText, rootLock.value.input]]),
|
|
2310
2566
|
canRemember.value ? (openBlock(), createElementBlock("button", {
|
|
2311
2567
|
key: 0,
|
|
2312
2568
|
class: "fl-unlock-remember",
|
|
2313
2569
|
type: "button",
|
|
2314
|
-
"aria-pressed": rootLock.remember,
|
|
2570
|
+
"aria-pressed": rootLock.value.remember,
|
|
2315
2571
|
"aria-label": t("rememberPassword"),
|
|
2316
2572
|
title: t("rememberPassword"),
|
|
2317
|
-
onClick: _cache[
|
|
2318
|
-
}, [
|
|
2319
|
-
viewBox: "0 0 24 24",
|
|
2320
|
-
width: "16",
|
|
2321
|
-
height: "16",
|
|
2322
|
-
fill: "none",
|
|
2323
|
-
stroke: "currentColor",
|
|
2324
|
-
"stroke-width": "2",
|
|
2325
|
-
"stroke-linecap": "round",
|
|
2326
|
-
"stroke-linejoin": "round",
|
|
2327
|
-
"aria-hidden": "true"
|
|
2328
|
-
}, [
|
|
2329
|
-
createElementVNode("circle", {
|
|
2330
|
-
cx: "8",
|
|
2331
|
-
cy: "15",
|
|
2332
|
-
r: "4"
|
|
2333
|
-
}),
|
|
2334
|
-
createElementVNode("path", { d: "m11 12 9-9" }),
|
|
2335
|
-
createElementVNode("path", { d: "m17 6 3 3" })
|
|
2336
|
-
], -1)])], 8, _hoisted_37)) : createCommentVNode("v-if", true),
|
|
2573
|
+
onClick: _cache[3] || (_cache[3] = ($event) => rootLock.value.remember = !rootLock.value.remember)
|
|
2574
|
+
}, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_34)) : createCommentVNode("v-if", true),
|
|
2337
2575
|
createElementVNode("button", {
|
|
2338
2576
|
class: "fl-unlock-submit",
|
|
2339
2577
|
type: "submit",
|
|
2340
|
-
disabled: rootLock.busy || !rootLock.input
|
|
2341
|
-
}, toDisplayString(t("open")), 9,
|
|
2578
|
+
disabled: rootLock.value.busy || !rootLock.value.input
|
|
2579
|
+
}, toDisplayString(t("open")), 9, _hoisted_35)
|
|
2342
2580
|
])
|
|
2343
|
-
], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div",
|
|
2581
|
+
], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_36, [
|
|
2344
2582
|
createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
|
|
2345
|
-
/guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p",
|
|
2346
|
-
|
|
2583
|
+
/guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_37, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
|
|
2584
|
+
unref(isNetworkError)(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_38, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
|
|
2347
2585
|
createElementVNode("button", {
|
|
2348
2586
|
class: "fl-btn",
|
|
2349
2587
|
type: "button",
|
|
2350
2588
|
onClick: loadRoot
|
|
2351
2589
|
}, toDisplayString(t("retry")), 1)
|
|
2352
|
-
])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div",
|
|
2353
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(
|
|
2354
|
-
return openBlock(), createElementBlock("div", {
|
|
2355
|
-
key:
|
|
2590
|
+
])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_39, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
|
|
2591
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (node, index) => {
|
|
2592
|
+
return openBlock(), createElementBlock(Fragment, { key: isMore(node) ? `more:${node.key}` : node.path }, [isMore(node) ? (openBlock(), createElementBlock("div", {
|
|
2593
|
+
key: 0,
|
|
2594
|
+
class: "fl-row fl-more",
|
|
2595
|
+
style: normalizeStyle({ "--fl-depth": node.depth })
|
|
2596
|
+
}, [_cache[9] || (_cache[9] = createElementVNode("span", { class: "fl-cell fl-cell-check" }, null, -1)), createElementVNode("span", _hoisted_40, [
|
|
2597
|
+
_cache[8] || (_cache[8] = createElementVNode("span", {
|
|
2598
|
+
class: "fl-indent",
|
|
2599
|
+
"aria-hidden": "true"
|
|
2600
|
+
}, null, -1)),
|
|
2601
|
+
createElementVNode("span", _hoisted_41, toDisplayString(t("moreRemaining", { count: node.remaining })), 1),
|
|
2602
|
+
node.remaining > (__props.pageSize || 0) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("button", {
|
|
2603
|
+
class: "fl-more-btn",
|
|
2604
|
+
type: "button",
|
|
2605
|
+
onClick: ($event) => showMore(node, false)
|
|
2606
|
+
}, [createTextVNode(toDisplayString(t("showMore")), 1), createElementVNode("span", _hoisted_43, toDisplayString(` ${t("folderItems", { count: __props.pageSize })}`), 1)], 8, _hoisted_42), createElementVNode("button", {
|
|
2607
|
+
class: "fl-more-btn fl-more-all",
|
|
2608
|
+
type: "button",
|
|
2609
|
+
onClick: ($event) => showMore(node, true)
|
|
2610
|
+
}, toDisplayString(t("showAll")), 9, _hoisted_44)], 64)) : (openBlock(), createElementBlock("button", {
|
|
2611
|
+
key: 1,
|
|
2612
|
+
class: "fl-more-btn",
|
|
2613
|
+
type: "button",
|
|
2614
|
+
onClick: ($event) => showMore(node, true)
|
|
2615
|
+
}, toDisplayString(t("showRest", { count: node.remaining })), 9, _hoisted_45))
|
|
2616
|
+
])], 4)) : (openBlock(), createElementBlock("div", {
|
|
2617
|
+
key: 1,
|
|
2356
2618
|
class: normalizeClass(["fl-row", {
|
|
2357
2619
|
"fl-row-dir": node.is_dir,
|
|
2358
2620
|
"fl-row-root": node.isRoot,
|
|
2359
2621
|
"fl-row-selected": !node.is_dir && selected.value.has(node.path),
|
|
2360
|
-
"fl-row-locked": node.
|
|
2361
|
-
"fl-row-prompting": node.
|
|
2362
|
-
"fl-row-error": node.
|
|
2622
|
+
"fl-row-locked": !!node.lock,
|
|
2623
|
+
"fl-row-prompting": node.lock?.prompting,
|
|
2624
|
+
"fl-row-error": node.lock?.prompting && !!node.lock.error
|
|
2363
2625
|
}]),
|
|
2364
2626
|
style: normalizeStyle({ "--fl-depth": node.depth })
|
|
2365
2627
|
}, [
|
|
2366
|
-
createElementVNode("label",
|
|
2628
|
+
createElementVNode("label", _hoisted_46, [createElementVNode("input", {
|
|
2367
2629
|
type: "checkbox",
|
|
2368
|
-
disabled: node.
|
|
2630
|
+
disabled: !!node.lock || batchBusy.value,
|
|
2369
2631
|
"aria-label": t("selectItem", { name: node.name }),
|
|
2370
2632
|
checked: isChecked(node),
|
|
2371
2633
|
".indeterminate": isIndeterminate(node),
|
|
2372
2634
|
onChange: ($event) => toggleCheck(node, $event)
|
|
2373
|
-
}, null, 40,
|
|
2635
|
+
}, null, 40, _hoisted_47)]),
|
|
2374
2636
|
createElementVNode("span", {
|
|
2375
2637
|
class: "fl-cell fl-cell-name",
|
|
2376
2638
|
title: node.isRoot ? node.path : void 0
|
|
2377
2639
|
}, [
|
|
2378
|
-
_cache[
|
|
2640
|
+
_cache[11] || (_cache[11] = createElementVNode("span", {
|
|
2379
2641
|
class: "fl-indent",
|
|
2380
2642
|
"aria-hidden": "true"
|
|
2381
2643
|
}, null, -1)),
|
|
2382
|
-
node.
|
|
2644
|
+
node.lock ? (openBlock(), createElementBlock("button", {
|
|
2383
2645
|
key: 0,
|
|
2384
2646
|
class: "fl-arrow fl-lock",
|
|
2385
2647
|
type: "button",
|
|
2386
2648
|
"aria-label": t("unlockFolder", { name: node.name }),
|
|
2387
2649
|
title: t("unlockFolder", { name: node.name }),
|
|
2388
2650
|
onClick: ($event) => toggleExpand(node)
|
|
2389
|
-
}, [
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
fill: "none",
|
|
2394
|
-
stroke: "currentColor",
|
|
2395
|
-
"stroke-width": "2.2",
|
|
2396
|
-
"stroke-linecap": "round",
|
|
2397
|
-
"stroke-linejoin": "round",
|
|
2398
|
-
"aria-hidden": "true"
|
|
2399
|
-
}, [createElementVNode("rect", {
|
|
2400
|
-
x: "5",
|
|
2401
|
-
y: "11",
|
|
2402
|
-
width: "14",
|
|
2403
|
-
height: "10",
|
|
2404
|
-
rx: "2"
|
|
2405
|
-
}), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_46)) : node.is_dir ? (openBlock(), createElementBlock("button", {
|
|
2651
|
+
}, [createVNode(_sfc_main$2, {
|
|
2652
|
+
name: "lock",
|
|
2653
|
+
size: 14
|
|
2654
|
+
})], 8, _hoisted_49)) : node.is_dir ? (openBlock(), createElementBlock("button", {
|
|
2406
2655
|
key: 1,
|
|
2407
2656
|
class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
|
|
2408
2657
|
type: "button",
|
|
2409
2658
|
"aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
|
|
2410
2659
|
"aria-expanded": node.expanded,
|
|
2411
2660
|
onClick: ($event) => toggleExpand(node)
|
|
2412
|
-
}, [
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
stroke: "currentColor",
|
|
2418
|
-
"stroke-width": "2.4",
|
|
2419
|
-
"stroke-linecap": "round",
|
|
2420
|
-
"stroke-linejoin": "round",
|
|
2421
|
-
"aria-hidden": "true"
|
|
2422
|
-
}, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_47)) : (openBlock(), createElementBlock("span", _hoisted_48)),
|
|
2423
|
-
node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_49, [..._cache[18] || (_cache[18] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
|
|
2661
|
+
}, [createVNode(_sfc_main$2, {
|
|
2662
|
+
name: "next",
|
|
2663
|
+
size: 14
|
|
2664
|
+
})], 10, _hoisted_50)) : (openBlock(), createElementBlock("span", _hoisted_51)),
|
|
2665
|
+
node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_52, [..._cache[10] || (_cache[10] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
|
|
2424
2666
|
key: 4,
|
|
2425
2667
|
class: "fl-type",
|
|
2426
2668
|
"data-kind": unref(fileKind)(node.name),
|
|
2427
2669
|
"aria-hidden": "true"
|
|
2428
|
-
}, toDisplayString(unref(extOf)(node.name) || "FILE"), 9,
|
|
2429
|
-
createElementVNode("span",
|
|
2670
|
+
}, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_53)),
|
|
2671
|
+
createElementVNode("span", _hoisted_54, [createElementVNode("span", {
|
|
2430
2672
|
class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
|
|
2431
|
-
onClick: ($event) => node.
|
|
2432
|
-
}, toDisplayString(node.name), 11,
|
|
2433
|
-
node.is_dir && node.loaded ? (openBlock(), createElementBlock("span",
|
|
2434
|
-
node.
|
|
2673
|
+
onClick: ($event) => node.lock || node.is_dir ? toggleExpand(node) : download(node)
|
|
2674
|
+
}, toDisplayString(node.name), 11, _hoisted_55), createElementVNode("span", _hoisted_56, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64)) : node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(t("folderItems", { count: statsOf(node).files })) + " · " + toDisplayString(unref(formatSize)(statsOf(node).bytes)), 1)], 64)) : createCommentVNode("v-if", true)])]),
|
|
2675
|
+
node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_57, toDisplayString(t("folderItems", { count: statsOf(node).files })), 1)) : createCommentVNode("v-if", true),
|
|
2676
|
+
node.lock?.prompting && node.lock.error ? (openBlock(), createElementBlock("span", {
|
|
2435
2677
|
key: 6,
|
|
2436
|
-
id:
|
|
2678
|
+
id: `${unref(uid)}-error-${index}`,
|
|
2437
2679
|
class: "fl-unlock-message",
|
|
2438
2680
|
role: "alert"
|
|
2439
|
-
}, toDisplayString(unlockErrorText(node.
|
|
2440
|
-
!node.
|
|
2681
|
+
}, toDisplayString(unlockErrorText(node.lock.error)), 9, _hoisted_58)) : createCommentVNode("v-if", true),
|
|
2682
|
+
!node.lock && unref(remembered).has(node.path) ? (openBlock(), createElementBlock("button", {
|
|
2441
2683
|
key: 7,
|
|
2442
2684
|
class: "fl-remembered",
|
|
2443
2685
|
type: "button",
|
|
2444
2686
|
"aria-label": `${t("forgetPassword")} ${node.name}`,
|
|
2445
2687
|
title: t("forgetPassword"),
|
|
2446
2688
|
onClick: ($event) => forgetPassword(node)
|
|
2447
|
-
}, [
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
"stroke-linecap": "round",
|
|
2455
|
-
"stroke-linejoin": "round",
|
|
2456
|
-
"aria-hidden": "true"
|
|
2457
|
-
}, [
|
|
2458
|
-
createElementVNode("circle", {
|
|
2459
|
-
cx: "8",
|
|
2460
|
-
cy: "15",
|
|
2461
|
-
r: "4"
|
|
2462
|
-
}),
|
|
2463
|
-
createElementVNode("path", { d: "m11 12 9-9" }),
|
|
2464
|
-
createElementVNode("path", { d: "m17 6 3 3" })
|
|
2465
|
-
], -1)), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_56)) : createCommentVNode("v-if", true),
|
|
2466
|
-
node.loading ? (openBlock(), createElementBlock("span", _hoisted_57, "…")) : createCommentVNode("v-if", true)
|
|
2467
|
-
], 8, _hoisted_45),
|
|
2468
|
-
node.locked && node.prompting ? (openBlock(), createElementBlock("form", {
|
|
2689
|
+
}, [createVNode(_sfc_main$2, {
|
|
2690
|
+
name: "key",
|
|
2691
|
+
size: 11
|
|
2692
|
+
}), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_59)) : createCommentVNode("v-if", true),
|
|
2693
|
+
node.loading ? (openBlock(), createElementBlock("span", _hoisted_60, "…")) : createCommentVNode("v-if", true)
|
|
2694
|
+
], 8, _hoisted_48),
|
|
2695
|
+
node.lock?.prompting ? (openBlock(), createElementBlock("form", {
|
|
2469
2696
|
key: 0,
|
|
2470
2697
|
class: "fl-unlock fl-unlock-row",
|
|
2471
2698
|
onSubmit: withModifiers(($event) => submitUnlock(node), ["prevent"]),
|
|
2472
|
-
onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node), ["prevent"]), ["esc"])
|
|
2699
|
+
onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node.lock), ["prevent"]), ["esc"])
|
|
2473
2700
|
}, [
|
|
2474
|
-
|
|
2701
|
+
createVNode(_sfc_main$2, {
|
|
2475
2702
|
class: "fl-unlock-lock",
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
fill: "none",
|
|
2480
|
-
stroke: "currentColor",
|
|
2481
|
-
"stroke-width": "2.2",
|
|
2482
|
-
"stroke-linecap": "round",
|
|
2483
|
-
"stroke-linejoin": "round",
|
|
2484
|
-
"aria-hidden": "true"
|
|
2485
|
-
}, [createElementVNode("rect", {
|
|
2486
|
-
x: "5",
|
|
2487
|
-
y: "11",
|
|
2488
|
-
width: "14",
|
|
2489
|
-
height: "10",
|
|
2490
|
-
rx: "2"
|
|
2491
|
-
}), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
|
|
2703
|
+
name: "lock",
|
|
2704
|
+
size: 18
|
|
2705
|
+
}),
|
|
2492
2706
|
withDirectives(createElementVNode("input", {
|
|
2493
|
-
"onUpdate:modelValue": ($event) => node.
|
|
2707
|
+
"onUpdate:modelValue": ($event) => node.lock.input = $event,
|
|
2494
2708
|
type: "password",
|
|
2495
2709
|
autocomplete: "off",
|
|
2496
2710
|
"data-unlock": node.path,
|
|
2497
|
-
placeholder: node.
|
|
2711
|
+
placeholder: node.lock.error ? unlockErrorText(node.lock.error) : t("passwordPlaceholder"),
|
|
2498
2712
|
"aria-label": t("passwordFor", { name: node.name }),
|
|
2499
|
-
"aria-invalid": !!node.
|
|
2500
|
-
"aria-describedby": node.
|
|
2501
|
-
}, null, 8,
|
|
2713
|
+
"aria-invalid": !!node.lock.error,
|
|
2714
|
+
"aria-describedby": node.lock.error ? `${unref(uid)}-error-${index}` : void 0
|
|
2715
|
+
}, null, 8, _hoisted_62), [[vModelText, node.lock.input]]),
|
|
2502
2716
|
canRemember.value ? (openBlock(), createElementBlock("button", {
|
|
2503
2717
|
key: 0,
|
|
2504
2718
|
class: "fl-unlock-remember",
|
|
2505
2719
|
type: "button",
|
|
2506
|
-
"aria-pressed":
|
|
2720
|
+
"aria-pressed": node.lock.remember,
|
|
2507
2721
|
"aria-label": t("rememberPassword"),
|
|
2508
2722
|
title: t("rememberPassword"),
|
|
2509
|
-
onClick: ($event) => node.
|
|
2510
|
-
}, [
|
|
2511
|
-
viewBox: "0 0 24 24",
|
|
2512
|
-
width: "16",
|
|
2513
|
-
height: "16",
|
|
2514
|
-
fill: "none",
|
|
2515
|
-
stroke: "currentColor",
|
|
2516
|
-
"stroke-width": "2",
|
|
2517
|
-
"stroke-linecap": "round",
|
|
2518
|
-
"stroke-linejoin": "round",
|
|
2519
|
-
"aria-hidden": "true"
|
|
2520
|
-
}, [
|
|
2521
|
-
createElementVNode("circle", {
|
|
2522
|
-
cx: "8",
|
|
2523
|
-
cy: "15",
|
|
2524
|
-
r: "4"
|
|
2525
|
-
}),
|
|
2526
|
-
createElementVNode("path", { d: "m11 12 9-9" }),
|
|
2527
|
-
createElementVNode("path", { d: "m17 6 3 3" })
|
|
2528
|
-
], -1)])], 8, _hoisted_60)) : createCommentVNode("v-if", true),
|
|
2723
|
+
onClick: ($event) => node.lock.remember = !node.lock.remember
|
|
2724
|
+
}, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_63)) : createCommentVNode("v-if", true),
|
|
2529
2725
|
createElementVNode("button", {
|
|
2530
2726
|
class: "fl-unlock-submit",
|
|
2531
2727
|
type: "submit",
|
|
2532
|
-
disabled: node.
|
|
2533
|
-
}, toDisplayString(t("open")), 9,
|
|
2728
|
+
disabled: node.lock.busy || !node.lock.input
|
|
2729
|
+
}, toDisplayString(t("open")), 9, _hoisted_64),
|
|
2534
2730
|
createElementVNode("button", {
|
|
2535
2731
|
class: "fl-unlock-cancel",
|
|
2536
2732
|
type: "button",
|
|
2537
2733
|
"aria-label": t("cancel"),
|
|
2538
2734
|
title: t("cancel"),
|
|
2539
|
-
onClick: ($event) => cancelUnlock(node)
|
|
2540
|
-
}, [
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
"aria-hidden": "true"
|
|
2549
|
-
}, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_62)
|
|
2550
|
-
], 40, _hoisted_58)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
2551
|
-
createElementVNode("span", _hoisted_63, toDisplayString(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size)), 1),
|
|
2552
|
-
createElementVNode("span", _hoisted_64, toDisplayString(unref(formatDate)(node.modified)), 1),
|
|
2553
|
-
createElementVNode("span", _hoisted_65, [node.locked ? (openBlock(), createElementBlock("button", {
|
|
2735
|
+
onClick: ($event) => cancelUnlock(node.lock)
|
|
2736
|
+
}, [createVNode(_sfc_main$2, {
|
|
2737
|
+
name: "close",
|
|
2738
|
+
size: 14
|
|
2739
|
+
})], 8, _hoisted_65)
|
|
2740
|
+
], 40, _hoisted_61)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
2741
|
+
createElementVNode("span", _hoisted_66, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
|
|
2742
|
+
createElementVNode("span", _hoisted_67, toDisplayString(unref(formatDate)(node.modified)), 1),
|
|
2743
|
+
createElementVNode("span", _hoisted_68, [node.lock ? (openBlock(), createElementBlock("button", {
|
|
2554
2744
|
key: 0,
|
|
2555
2745
|
class: "fl-action fl-arrow-end fl-lock",
|
|
2556
2746
|
type: "button",
|
|
2557
2747
|
"aria-label": t("unlockFolder", { name: node.name }),
|
|
2558
2748
|
onClick: ($event) => toggleExpand(node)
|
|
2559
|
-
}, [
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
fill: "none",
|
|
2564
|
-
stroke: "currentColor",
|
|
2565
|
-
"stroke-width": "2.2",
|
|
2566
|
-
"stroke-linecap": "round",
|
|
2567
|
-
"stroke-linejoin": "round",
|
|
2568
|
-
"aria-hidden": "true"
|
|
2569
|
-
}, [createElementVNode("rect", {
|
|
2570
|
-
x: "5",
|
|
2571
|
-
y: "11",
|
|
2572
|
-
width: "14",
|
|
2573
|
-
height: "10",
|
|
2574
|
-
rx: "2"
|
|
2575
|
-
}), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_66)) : node.is_dir ? (openBlock(), createElementBlock("button", {
|
|
2749
|
+
}, [createVNode(_sfc_main$2, {
|
|
2750
|
+
name: "lock",
|
|
2751
|
+
size: 18
|
|
2752
|
+
})], 8, _hoisted_69)) : node.is_dir ? (openBlock(), createElementBlock("button", {
|
|
2576
2753
|
key: 1,
|
|
2577
2754
|
class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
|
|
2578
2755
|
type: "button",
|
|
2579
2756
|
"aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
|
|
2580
2757
|
"aria-expanded": node.expanded,
|
|
2581
2758
|
onClick: ($event) => toggleExpand(node)
|
|
2582
|
-
}, [
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
fill: "none",
|
|
2587
|
-
stroke: "currentColor",
|
|
2588
|
-
"stroke-width": "2.2",
|
|
2589
|
-
"stroke-linecap": "round",
|
|
2590
|
-
"stroke-linejoin": "round",
|
|
2591
|
-
"aria-hidden": "true"
|
|
2592
|
-
}, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_67)) : !node.locked ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
|
|
2759
|
+
}, [createVNode(_sfc_main$2, {
|
|
2760
|
+
name: "next",
|
|
2761
|
+
size: 18
|
|
2762
|
+
})], 10, _hoisted_70)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
|
|
2593
2763
|
key: 0,
|
|
2594
|
-
class: "fl-action fl-
|
|
2764
|
+
class: "fl-action fl-preview-btn",
|
|
2595
2765
|
type: "button",
|
|
2596
2766
|
"aria-label": `${t("preview")} ${node.name}`,
|
|
2597
2767
|
title: t("preview"),
|
|
2598
2768
|
onClick: ($event) => preview(node)
|
|
2599
|
-
}, [
|
|
2600
|
-
viewBox: "0 0 24 24",
|
|
2601
|
-
width: "16",
|
|
2602
|
-
height: "16",
|
|
2603
|
-
fill: "none",
|
|
2604
|
-
stroke: "currentColor",
|
|
2605
|
-
"stroke-width": "2",
|
|
2606
|
-
"stroke-linecap": "round",
|
|
2607
|
-
"stroke-linejoin": "round",
|
|
2608
|
-
"aria-hidden": "true"
|
|
2609
|
-
}, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
|
|
2610
|
-
cx: "12",
|
|
2611
|
-
cy: "12",
|
|
2612
|
-
r: "3"
|
|
2613
|
-
})], -1)])], 8, _hoisted_68)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
2769
|
+
}, [createVNode(_sfc_main$2, { name: "eye" })], 8, _hoisted_71)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
2614
2770
|
class: "fl-action fl-download",
|
|
2615
2771
|
type: "button",
|
|
2616
2772
|
"aria-label": `${t("download")} ${node.name}`,
|
|
2617
2773
|
title: t("download"),
|
|
2618
2774
|
onClick: ($event) => download(node)
|
|
2619
|
-
}, [
|
|
2620
|
-
viewBox: "0 0 24 24",
|
|
2621
|
-
width: "16",
|
|
2622
|
-
height: "16",
|
|
2623
|
-
fill: "none",
|
|
2624
|
-
stroke: "currentColor",
|
|
2625
|
-
"stroke-width": "2",
|
|
2626
|
-
"stroke-linecap": "round",
|
|
2627
|
-
"stroke-linejoin": "round",
|
|
2628
|
-
"aria-hidden": "true"
|
|
2629
|
-
}, [
|
|
2630
|
-
createElementVNode("path", { d: "M12 3v12" }),
|
|
2631
|
-
createElementVNode("path", { d: "m7 10 5 5 5-5" }),
|
|
2632
|
-
createElementVNode("path", { d: "M5 21h14" })
|
|
2633
|
-
], -1)])], 8, _hoisted_69)], 64)) : createCommentVNode("v-if", true)])
|
|
2775
|
+
}, [createVNode(_sfc_main$2, { name: "download" })], 8, _hoisted_72)], 64))])
|
|
2634
2776
|
], 64))
|
|
2635
|
-
], 6);
|
|
2777
|
+
], 6))], 64);
|
|
2636
2778
|
}), 128)),
|
|
2637
|
-
createElementVNode("div",
|
|
2638
|
-
toast.value ? (openBlock(), createElementBlock("div",
|
|
2779
|
+
createElementVNode("div", _hoisted_73, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) })), 1)]),
|
|
2780
|
+
toast.value ? (openBlock(), createElementBlock("div", _hoisted_74, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
|
|
2781
|
+
createVNode(_sfc_main$3, {
|
|
2782
|
+
open: !!zipNotice.value,
|
|
2783
|
+
title: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnTitle" : "zipBlockTitle", { size: unref(formatSize)(zipNotice.value.size) }) : "",
|
|
2784
|
+
text: zipNotice.value ? t(zipNotice.value.kind === "warn" ? "zipWarnText" : "zipBlockText", { limit: unref(formatSize)(zipNotice.value.limit) }) : "",
|
|
2785
|
+
tip: zipTip.value,
|
|
2786
|
+
"confirm-label": zipNotice.value?.kind === "warn" ? t("zipContinue") : void 0,
|
|
2787
|
+
"cancel-label": zipNotice.value?.kind === "warn" ? t("cancel") : t("gotIt"),
|
|
2788
|
+
onConfirm: confirmZip,
|
|
2789
|
+
onClose: _cache[4] || (_cache[4] = ($event) => zipNotice.value = null)
|
|
2790
|
+
}, null, 8, [
|
|
2791
|
+
"open",
|
|
2792
|
+
"title",
|
|
2793
|
+
"text",
|
|
2794
|
+
"tip",
|
|
2795
|
+
"confirm-label",
|
|
2796
|
+
"cancel-label"
|
|
2797
|
+
]),
|
|
2639
2798
|
createVNode(_sfc_main$1, {
|
|
2640
2799
|
open: !!previewNode.value,
|
|
2641
2800
|
file: previewNode.value,
|
|
@@ -2648,9 +2807,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2648
2807
|
position: previewPosition.value,
|
|
2649
2808
|
total: previewPosition.value ? previewList.value.length : 0,
|
|
2650
2809
|
onClose: closePreview,
|
|
2651
|
-
onPrev: _cache[
|
|
2652
|
-
onNext: _cache[
|
|
2653
|
-
onDownload: _cache[
|
|
2810
|
+
onPrev: _cache[5] || (_cache[5] = ($event) => stepPreview(-1)),
|
|
2811
|
+
onNext: _cache[6] || (_cache[6] = ($event) => stepPreview(1)),
|
|
2812
|
+
onDownload: _cache[7] || (_cache[7] = ($event) => previewNode.value && download(previewNode.value))
|
|
2654
2813
|
}, null, 8, [
|
|
2655
2814
|
"open",
|
|
2656
2815
|
"file",
|