vitepress-plugin-filelist 0.7.2 → 0.9.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 +28 -301
- package/dist/client/browser/index.d.ts +15 -83
- package/dist/client/browser/index.js +413 -139
- package/dist/client/ssr/index.d.ts +15 -83
- package/dist/client/ssr/index.js +242 -30
- package/dist/client/style.css +153 -29
- package/package.json +16 -6
|
@@ -6,8 +6,6 @@ interface FileListLabels {
|
|
|
6
6
|
filter: string;
|
|
7
7
|
refresh: string;
|
|
8
8
|
batch: string;
|
|
9
|
-
/** @deprecated No longer shown; the selection count lives in the toolbar. */
|
|
10
|
-
batchCount: string;
|
|
11
9
|
batching: string;
|
|
12
10
|
passwordPrompt: string;
|
|
13
11
|
passwordPlaceholder: string;
|
|
@@ -27,15 +25,11 @@ interface FileListLabels {
|
|
|
27
25
|
download: string;
|
|
28
26
|
loaded: string;
|
|
29
27
|
selected: string;
|
|
30
|
-
/** @deprecated No longer shown; the footer only shows loaded files and size. */
|
|
31
|
-
unloaded: string;
|
|
32
28
|
folderFailed: string;
|
|
33
29
|
demoDownload: string;
|
|
34
30
|
demoPreview: string;
|
|
35
31
|
demoBatch: string;
|
|
36
32
|
downloadFailed: string;
|
|
37
|
-
/** @deprecated Preview errors are shown in the dialog (`previewUnavailable`). */
|
|
38
|
-
previewFailed: string;
|
|
39
33
|
batchFailed: string;
|
|
40
34
|
defaultZip: string;
|
|
41
35
|
/** Text button that clears the selection (wide layout). */
|
|
@@ -90,8 +84,6 @@ interface FileListLabels {
|
|
|
90
84
|
forgetPassword: string;
|
|
91
85
|
/** Lock button on a folder that needs a password. `{name}` */
|
|
92
86
|
unlockFolder: string;
|
|
93
|
-
/** @deprecated Network failures now append `corsHint`. */
|
|
94
|
-
networkHint: string;
|
|
95
87
|
/** Deep search: the row at the end of a filtered list. `{count}` where noted */
|
|
96
88
|
deepHint: string;
|
|
97
89
|
deepSearch: string;
|
|
@@ -136,12 +128,23 @@ interface FileListLabels {
|
|
|
136
128
|
/** `{limit}` */
|
|
137
129
|
zipBlockText: string;
|
|
138
130
|
gotIt: string;
|
|
139
|
-
/** Last row of a long folder. `{count}` */
|
|
131
|
+
/** Last row of a long folder: how many are not shown yet. `{count}` */
|
|
140
132
|
moreRemaining: string;
|
|
141
|
-
|
|
133
|
+
/** Pinned folder row: `{shown}` of `{total}`. */
|
|
134
|
+
moreShown: string;
|
|
135
|
+
/** “Show all”, short form for narrow lists. */
|
|
136
|
+
showAllShort: string;
|
|
137
|
+
/** Show the next page. `{count}` */
|
|
138
|
+
showNext: string;
|
|
139
|
+
/** Show the next page, short form for narrow lists. `{count}` */
|
|
140
|
+
showNextShort: string;
|
|
142
141
|
showAll: string;
|
|
143
142
|
/** Fewer than a page left. `{count}` */
|
|
144
143
|
showRest: string;
|
|
144
|
+
/** Pinned folder row: back to the first page. */
|
|
145
|
+
showLess: string;
|
|
146
|
+
/** Pinned folder row once everything is shown. `{count}` */
|
|
147
|
+
allShown: string;
|
|
145
148
|
}
|
|
146
149
|
interface FileListSource {
|
|
147
150
|
/** OpenList path, directory or single file. */
|
|
@@ -174,7 +177,7 @@ interface FileListProps {
|
|
|
174
177
|
*/
|
|
175
178
|
rememberPassword?: boolean;
|
|
176
179
|
/**
|
|
177
|
-
* Rows shown per folder before “Show more”. Defaults to `
|
|
180
|
+
* Rows shown per folder before “Show more”. Defaults to `15`; `0` shows
|
|
178
181
|
* everything. Data is still loaded in full; only rendering is limited.
|
|
179
182
|
*/
|
|
180
183
|
pageSize?: number;
|
|
@@ -203,17 +206,6 @@ interface FileListProps {
|
|
|
203
206
|
/** Per-key label overrides on top of the locale preset. */
|
|
204
207
|
labels?: Partial<FileListLabels>;
|
|
205
208
|
}
|
|
206
|
-
/** Shape of one entry returned by `POST /api/fs/list`. */
|
|
207
|
-
interface OpenListEntry {
|
|
208
|
-
name: string;
|
|
209
|
-
size: number;
|
|
210
|
-
is_dir: boolean;
|
|
211
|
-
modified: string;
|
|
212
|
-
created?: string;
|
|
213
|
-
sign?: string;
|
|
214
|
-
thumb?: string;
|
|
215
|
-
type?: number;
|
|
216
|
-
}
|
|
217
209
|
declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
|
|
218
210
|
base: string;
|
|
219
211
|
path: string;
|
|
@@ -305,66 +297,6 @@ declare global {
|
|
|
305
297
|
export declare const fileListLocales: Record<FileListLocale, FileListLabels>;
|
|
306
298
|
export declare function formatLabel(template: string, values?: Record<string, string | number>): string;
|
|
307
299
|
export declare function resolveLabels(locale?: FileListLocale, overrides?: Partial<FileListLabels>): FileListLabels;
|
|
308
|
-
interface SortableNode {
|
|
309
|
-
name: string;
|
|
310
|
-
size: number;
|
|
311
|
-
is_dir: boolean;
|
|
312
|
-
modified: string;
|
|
313
|
-
}
|
|
314
|
-
type SortKey = 'name' | 'size' | 'modified';
|
|
315
|
-
export declare function joinPath(parent: string, name: string): string;
|
|
316
|
-
export declare function encodePath(path: string): string;
|
|
317
|
-
/**
|
|
318
|
-
* Make one path segment safe as a ZIP entry segment: no separators,
|
|
319
|
-
* no `.` / `..` traversal, no empty names.
|
|
320
|
-
*/
|
|
321
|
-
export declare function zipSegment(name: string): string;
|
|
322
|
-
/** Join path segments into a ZIP entry name, sanitising each segment. */
|
|
323
|
-
export declare function zipEntryName(segments: string[]): string;
|
|
324
|
-
/**
|
|
325
|
-
* Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
|
|
326
|
-
* The suffix goes before the extension of the last segment. Adds the result to `used`.
|
|
327
|
-
*/
|
|
328
|
-
export declare function uniqueEntryName(name: string, used: Set<string>): string;
|
|
329
|
-
/** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
|
|
330
|
-
export declare function formatSize(size: number): string;
|
|
331
|
-
export declare function formatDate(iso: string): string;
|
|
332
|
-
/** Upper-case extension for the type badge, e.g. `PDF`. */
|
|
333
|
-
export declare function extOf(name: string): string;
|
|
334
|
-
type FileKind = 'doc' | 'sheet' | 'slide' | 'pdf' | 'image' | 'media' | 'archive' | 'code' | 'other';
|
|
335
|
-
type PreviewKind = 'image' | 'video' | 'audio' | 'text' | 'pdf' | 'none';
|
|
336
|
-
/** Rough file family, used to colour the type badge. */
|
|
337
|
-
export declare function fileKind(name: string): FileKind;
|
|
338
|
-
/**
|
|
339
|
-
* How a file is shown in the preview dialog. Office documents and anything
|
|
340
|
-
* else the browser cannot render are `none` (info + download).
|
|
341
|
-
*/
|
|
342
|
-
export declare function previewKind(name: string): PreviewKind;
|
|
343
|
-
/** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
|
|
344
|
-
export declare function totalSize(items: Array<{
|
|
345
|
-
size?: number;
|
|
346
|
-
}>): number;
|
|
347
|
-
export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean, locale?: string): number;
|
|
348
|
-
interface IframePreview {
|
|
349
|
-
label: string;
|
|
350
|
-
scheme: string;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Viewers from OpenList's public `iframe_previews` setting that match a file,
|
|
354
|
-
* in the order OpenList lists them. Keys are comma-separated extensions or a
|
|
355
|
-
* `/regex/flags` tested against the extension, as in OpenList's frontend.
|
|
356
|
-
*/
|
|
357
|
-
export declare function iframePreviewsFor(name: string, setting: string | undefined): IframePreview[];
|
|
358
|
-
/**
|
|
359
|
-
* Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
|
|
360
|
-
* (signed download URL), each optionally prefixed with `e` (URI-encode) or
|
|
361
|
-
* `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
|
|
362
|
-
*/
|
|
363
|
-
export declare function fillPreviewUrl(scheme: string, args: {
|
|
364
|
-
name: string;
|
|
365
|
-
rawUrl: string;
|
|
366
|
-
downloadUrl: string;
|
|
367
|
-
}): string;
|
|
368
300
|
/**
|
|
369
301
|
* Register `<FileList>` and `<VPFileList>` globally.
|
|
370
302
|
*
|
|
@@ -382,4 +314,4 @@ export declare function fillPreviewUrl(scheme: string, args: {
|
|
|
382
314
|
* ```
|
|
383
315
|
*/
|
|
384
316
|
export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
|
|
385
|
-
export {
|
|
317
|
+
export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource };
|
package/dist/client/ssr/index.js
CHANGED
|
@@ -353,6 +353,56 @@ function createOpenListApi(base, token) {
|
|
|
353
353
|
}
|
|
354
354
|
};
|
|
355
355
|
}
|
|
356
|
+
// src/client/demoSamples.ts
|
|
357
|
+
const SAMPLE_FOLDER = "/示例文件";
|
|
358
|
+
const SAMPLES = {
|
|
359
|
+
"使用说明.md": {
|
|
360
|
+
type: "text/markdown",
|
|
361
|
+
body: "# 使用说明\n\n这是 **vitepress-plugin-filelist** 演示数据自带的 Markdown 文件。\n\n## 试一试\n\n- 点右上角“渲染”,按 VitePress 正文样式显示\n- 用 ← / → 切换到同一目录里的其它文件\n- 勾选“示例文件”文件夹,点“下载 ZIP”打包\n\n| 文件 | 类型 |\n| --- | --- |\n| 讲义.pdf | PDF |\n| 课程海报.svg | 图片 |\n| 网页示例.html | 网页 |\n\n> 接入 OpenList 后,这里显示的就是网盘里的真实文件。\n\n更多说明见 [文档站](https://nrjycyd.github.io/vitepress-plugin-filelist/)。\n"
|
|
362
|
+
},
|
|
363
|
+
"网页示例.html": {
|
|
364
|
+
type: "text/html",
|
|
365
|
+
body: "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<title>网页示例</title>\n<style>\n body { font-family: system-ui, sans-serif; margin: 24px; line-height: 1.6; color: #1f2937; }\n h1 { color: #3451b2; }\n .card { padding: 12px 16px; border: 1px solid #d1d5db; border-radius: 8px; }\n</style>\n</head>\n<body>\n<h1>网页示例</h1>\n<p>切换到“渲染”后,这个网页在沙盒中显示:带样式,但脚本不会运行。</p>\n<div class=\"card\">下面的按钮由脚本控制,在沙盒里点了没有反应。</div>\n<p><button onclick=\"alert('脚本运行了')\">点我</button></p>\n<script>document.body.dataset.ran = 'yes'<\/script>\n</body>\n</html>\n"
|
|
366
|
+
},
|
|
367
|
+
"课程海报.svg": {
|
|
368
|
+
type: "image/svg+xml",
|
|
369
|
+
body: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"450\" viewBox=\"0 0 800 450\">\n<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3451b2\"/><stop offset=\"1\" stop-color=\"#5672cd\"/></linearGradient></defs>\n<rect width=\"800\" height=\"450\" fill=\"url(#g)\"/>\n<circle cx=\"660\" cy=\"90\" r=\"120\" fill=\"#ffffff\" opacity=\"0.08\"/>\n<circle cx=\"120\" cy=\"420\" r=\"160\" fill=\"#ffffff\" opacity=\"0.06\"/>\n<text x=\"64\" y=\"190\" font-family=\"system-ui, sans-serif\" font-size=\"56\" font-weight=\"700\" fill=\"#ffffff\">课程海报</text>\n<text x=\"64\" y=\"250\" font-family=\"system-ui, sans-serif\" font-size=\"26\" fill=\"#dfe5ff\">线性代数 · 2026 秋季学期</text>\n<text x=\"64\" y=\"380\" font-family=\"system-ui, sans-serif\" font-size=\"18\" fill=\"#dfe5ff\">vitepress-plugin-filelist 演示图片</text>\n</svg>\n"
|
|
370
|
+
},
|
|
371
|
+
"讲义.pdf": {
|
|
372
|
+
type: "application/pdf",
|
|
373
|
+
body: "%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n5 0 obj\n<< /Length 257 >>\nstream\nBT\n/F1 20 Tf\n72 760 Td\n(FileList sample PDF) Tj\n/F1 12 Tf\n0 -22 Td\n() Tj\n0 -22 Td\n(This small PDF ships with the demo data of) Tj\n0 -22 Td\n(vitepress-plugin-filelist, so preview, download) Tj\n0 -22 Td\n(and ZIP can be tried without an OpenList server.) Tj\nET\nendstream\nendobj\nxref\n0 6\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000241 00000 n \n0000000311 00000 n \ntrailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n619\n%%EOF\n"
|
|
374
|
+
},
|
|
375
|
+
"课程表.txt": {
|
|
376
|
+
type: "text/plain",
|
|
377
|
+
body: "课程表(演示文本)\n\n周一 08:00 线性代数 教 301\n周三 10:00 线性代数习题课 教 205\n周五 14:00 答疑 图书馆 2 楼\n\n纯文本文件只读取前 1 MB,按原样显示。\n"
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const SAMPLE_NAMES = Object.keys(SAMPLES);
|
|
381
|
+
/** Size in bytes of a sample, as UTF-8. */
|
|
382
|
+
function sampleSize(name) {
|
|
383
|
+
return new TextEncoder().encode(SAMPLES[name].body).byteLength;
|
|
384
|
+
}
|
|
385
|
+
function nameOf(path) {
|
|
386
|
+
const name = path.slice(6);
|
|
387
|
+
return path.startsWith(`/示例文件/`) && name in SAMPLES ? name : void 0;
|
|
388
|
+
}
|
|
389
|
+
/** Whether a demo path is one of the sample files (which have a real URL). */
|
|
390
|
+
function isSample(path) {
|
|
391
|
+
return nameOf(path) !== void 0;
|
|
392
|
+
}
|
|
393
|
+
const urls = /* @__PURE__ */ new Map();
|
|
394
|
+
/** A blob: URL for a sample file, `''` for any other path or outside a browser. */
|
|
395
|
+
function sampleUrl(path) {
|
|
396
|
+
const name = nameOf(path);
|
|
397
|
+
if (!name || typeof URL.createObjectURL !== "function") return "";
|
|
398
|
+
let url = urls.get(name);
|
|
399
|
+
if (!url) {
|
|
400
|
+
const { type, body } = SAMPLES[name];
|
|
401
|
+
url = URL.createObjectURL(new Blob([body], { type: `${type};charset=utf-8` }));
|
|
402
|
+
urls.set(name, url);
|
|
403
|
+
}
|
|
404
|
+
return url;
|
|
405
|
+
}
|
|
356
406
|
// src/client/demo.ts
|
|
357
407
|
/** Built-in sample data, used when no `base` is given. */
|
|
358
408
|
const TREE = {
|
|
@@ -392,8 +442,32 @@ const TREE = {
|
|
|
392
442
|
size: 0,
|
|
393
443
|
is_dir: true,
|
|
394
444
|
modified: "2026-10-20T18:00:00+08:00"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "示例文件",
|
|
448
|
+
size: 0,
|
|
449
|
+
is_dir: true,
|
|
450
|
+
modified: "2026-11-11T09:00:00+08:00"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "照片(260 张)",
|
|
454
|
+
size: 0,
|
|
455
|
+
is_dir: true,
|
|
456
|
+
modified: "2026-10-01T12:00:00+08:00"
|
|
395
457
|
}
|
|
396
458
|
],
|
|
459
|
+
[SAMPLE_FOLDER]: SAMPLE_NAMES.map((name) => ({
|
|
460
|
+
name,
|
|
461
|
+
size: sampleSize(name),
|
|
462
|
+
is_dir: false,
|
|
463
|
+
modified: "2026-11-11T09:00:00+08:00"
|
|
464
|
+
})),
|
|
465
|
+
"/照片(260 张)": Array.from({ length: 260 }, (_, index) => ({
|
|
466
|
+
name: `IMG_${String(index + 1).padStart(4, "0")}.jpg`,
|
|
467
|
+
size: 2e6 + index * 7919 % 15e5,
|
|
468
|
+
is_dir: false,
|
|
469
|
+
modified: "2026-10-01T12:00:00+08:00"
|
|
470
|
+
})),
|
|
397
471
|
"/assignments": [{
|
|
398
472
|
name: "线代作业参考答案",
|
|
399
473
|
size: 0,
|
|
@@ -514,7 +588,7 @@ function guard(path, password) {
|
|
|
514
588
|
function parentOf(path) {
|
|
515
589
|
return path.slice(0, path.lastIndexOf("/")) || "/";
|
|
516
590
|
}
|
|
517
|
-
/** Demo OpenList: same API, no network; files have
|
|
591
|
+
/** Demo OpenList: same API, no network; only the sample files have a URL. */
|
|
518
592
|
function createDemoApi(delay = 220) {
|
|
519
593
|
return {
|
|
520
594
|
async list(path, password) {
|
|
@@ -533,12 +607,46 @@ function createDemoApi(delay = 220) {
|
|
|
533
607
|
title: "",
|
|
534
608
|
iframePreviews: ""
|
|
535
609
|
}),
|
|
536
|
-
fileUrl: async () =>
|
|
610
|
+
fileUrl: async (file) => sampleUrl(file.path),
|
|
537
611
|
directUrl: () => "",
|
|
538
|
-
isAttachmentUrl: () =>
|
|
612
|
+
isAttachmentUrl: (url) => url.startsWith("blob:"),
|
|
539
613
|
clearCache: () => {}
|
|
540
614
|
};
|
|
541
615
|
}
|
|
616
|
+
/** Whether a demo file can really be previewed, downloaded and zipped. */
|
|
617
|
+
const isDemoFile = isSample;
|
|
618
|
+
// src/client/devWarnings.ts
|
|
619
|
+
const SECURITY_DOC = "https://nrjycyd.github.io/vitepress-plugin-filelist/guide/security";
|
|
620
|
+
/** Props a page wrote that end up, readable by anyone, in the built site. */
|
|
621
|
+
function publicSecrets(props) {
|
|
622
|
+
const found = [];
|
|
623
|
+
if (props.token) found.push("token");
|
|
624
|
+
if (props.password || props.sources?.some((source) => source.password)) found.push("password");
|
|
625
|
+
return found;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Development builds only. The site's Vite replaces `import.meta.env` in
|
|
629
|
+
* dependencies too (`DEV` is true under `vitepress dev`, false in a build);
|
|
630
|
+
* without a bundler there is no `env` and this is not a development build.
|
|
631
|
+
*/
|
|
632
|
+
function isDevBuild() {
|
|
633
|
+
return import.meta.env?.DEV === true;
|
|
634
|
+
}
|
|
635
|
+
let warned = false;
|
|
636
|
+
/**
|
|
637
|
+
* While the site runs in development, say once per page load that `token` and
|
|
638
|
+
* `password` written in Markdown are public. Production builds stay silent.
|
|
639
|
+
* Returns whether it warned.
|
|
640
|
+
*/
|
|
641
|
+
function warnPublicSecrets(props, locale, dev = isDevBuild()) {
|
|
642
|
+
if (warned || !dev) return false;
|
|
643
|
+
const found = publicSecrets(props);
|
|
644
|
+
if (!found.length) return false;
|
|
645
|
+
warned = true;
|
|
646
|
+
const names = found.join(" / ");
|
|
647
|
+
console.warn(locale === "en-US" ? `[vitepress-plugin-filelist] This page sets ${names} on <FileList>. It is copied as is into the built site, where every visitor can read it. Prefer a guest account limited to a base path. ${SECURITY_DOC} (shown in development only)` : `[vitepress-plugin-filelist] 本页的 <FileList> 写了 ${names}:它会原样进入构建后的网页,所有访客都能看到。建议改用限定基础路径的 guest 账户。${SECURITY_DOC}(仅开发时提示)`);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
542
650
|
// src/client/ConfirmDialog.vue
|
|
543
651
|
const _sfc_main$3 = /*@__PURE__*/ defineComponent({
|
|
544
652
|
__name: "ConfirmDialog",
|
|
@@ -754,6 +862,14 @@ const ICONS = {
|
|
|
754
862
|
stroke: 2.2,
|
|
755
863
|
paths: ["m15 6-6 6 6 6"]
|
|
756
864
|
},
|
|
865
|
+
down: {
|
|
866
|
+
stroke: 2.4,
|
|
867
|
+
paths: ["m6 9 6 6 6-6"]
|
|
868
|
+
},
|
|
869
|
+
up: {
|
|
870
|
+
stroke: 2.4,
|
|
871
|
+
paths: ["m18 15-6-6-6 6"]
|
|
872
|
+
},
|
|
757
873
|
zoomIn: {
|
|
758
874
|
stroke: 2,
|
|
759
875
|
paths: [
|
|
@@ -813,7 +929,6 @@ const fileListLocales = {
|
|
|
813
929
|
filter: "筛选文件名",
|
|
814
930
|
refresh: "刷新",
|
|
815
931
|
batch: "下载 ZIP",
|
|
816
|
-
batchCount: "批量下载({count})",
|
|
817
932
|
batching: "打包中 {percent}%",
|
|
818
933
|
passwordPrompt: "该目录受密码保护:",
|
|
819
934
|
passwordPlaceholder: "输入密码",
|
|
@@ -831,13 +946,11 @@ const fileListLocales = {
|
|
|
831
946
|
download: "下载",
|
|
832
947
|
loaded: "{count} 个已加载文件",
|
|
833
948
|
selected: "已选 {count} 项",
|
|
834
|
-
unloaded: "{count} 个目录未展开,未计入全选",
|
|
835
949
|
folderFailed: "打开文件夹失败:{message}",
|
|
836
950
|
demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
|
|
837
951
|
demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
|
|
838
952
|
demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
|
|
839
953
|
downloadFailed: "下载失败:{message}",
|
|
840
|
-
previewFailed: "预览失败:{message}",
|
|
841
954
|
batchFailed: "打包失败:{message}",
|
|
842
955
|
defaultZip: "资源",
|
|
843
956
|
clear: "清除",
|
|
@@ -868,7 +981,6 @@ const fileListLocales = {
|
|
|
868
981
|
remembered: "已记住",
|
|
869
982
|
forgetPassword: "忘记",
|
|
870
983
|
unlockFolder: "输入 {name} 的密码",
|
|
871
|
-
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
872
984
|
deepHint: "只筛选了已加载的内容,",
|
|
873
985
|
deepSearch: "尝试深度搜索",
|
|
874
986
|
deepRunning: "正在深度搜索 · 已查找 {count} 个文件夹",
|
|
@@ -899,9 +1011,14 @@ const fileListLocales = {
|
|
|
899
1011
|
zipBlockText: "当前浏览器一次最多打包 {limit},请分批勾选下载。",
|
|
900
1012
|
gotIt: "知道了",
|
|
901
1013
|
moreRemaining: "还有 {count} 项",
|
|
902
|
-
showMore: "显示更多",
|
|
903
1014
|
showAll: "全部显示",
|
|
904
|
-
showRest: "显示剩余 {count} 项"
|
|
1015
|
+
showRest: "显示剩余 {count} 项",
|
|
1016
|
+
showLess: "收起",
|
|
1017
|
+
allShown: "已显示全部 {count} 项",
|
|
1018
|
+
moreShown: "已显示 {shown} / {total} 项",
|
|
1019
|
+
showAllShort: "全部",
|
|
1020
|
+
showNext: "再显示 {count} 项",
|
|
1021
|
+
showNextShort: "+{count}"
|
|
905
1022
|
},
|
|
906
1023
|
"en-US": {
|
|
907
1024
|
allFiles: "All files",
|
|
@@ -909,7 +1026,6 @@ const fileListLocales = {
|
|
|
909
1026
|
filter: "Filter files",
|
|
910
1027
|
refresh: "Refresh",
|
|
911
1028
|
batch: "Download ZIP",
|
|
912
|
-
batchCount: "Download ZIP ({count})",
|
|
913
1029
|
batching: "Zipping {percent}%",
|
|
914
1030
|
passwordPrompt: "This folder is password protected:",
|
|
915
1031
|
passwordPlaceholder: "Folder password",
|
|
@@ -927,13 +1043,11 @@ const fileListLocales = {
|
|
|
927
1043
|
download: "Download",
|
|
928
1044
|
loaded: "{count} loaded file(s)",
|
|
929
1045
|
selected: "{count} selected",
|
|
930
|
-
unloaded: "{count} folder(s) not expanded, excluded from select-all",
|
|
931
1046
|
folderFailed: "Failed to open folder: {message}",
|
|
932
1047
|
demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
|
|
933
1048
|
demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
|
|
934
1049
|
demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
|
|
935
1050
|
downloadFailed: "Download failed: {message}",
|
|
936
|
-
previewFailed: "Preview failed: {message}",
|
|
937
1051
|
batchFailed: "Failed to create ZIP: {message}",
|
|
938
1052
|
defaultZip: "resources",
|
|
939
1053
|
clear: "Clear",
|
|
@@ -964,7 +1078,6 @@ const fileListLocales = {
|
|
|
964
1078
|
remembered: "Remembered",
|
|
965
1079
|
forgetPassword: "Forget",
|
|
966
1080
|
unlockFolder: "Enter the password for {name}",
|
|
967
|
-
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
968
1081
|
deepHint: "Only loaded folders are filtered. ",
|
|
969
1082
|
deepSearch: "Try deep search",
|
|
970
1083
|
deepRunning: "Searching · {count} folders checked",
|
|
@@ -995,9 +1108,14 @@ const fileListLocales = {
|
|
|
995
1108
|
zipBlockText: "This browser can zip at most {limit} at once. Select fewer files.",
|
|
996
1109
|
gotIt: "OK",
|
|
997
1110
|
moreRemaining: "{count} more",
|
|
998
|
-
showMore: "Show more",
|
|
999
1111
|
showAll: "Show all",
|
|
1000
|
-
showRest: "Show the remaining {count}"
|
|
1112
|
+
showRest: "Show the remaining {count}",
|
|
1113
|
+
showLess: "Show less",
|
|
1114
|
+
allShown: "All {count} shown",
|
|
1115
|
+
moreShown: "Showing {shown} of {total}",
|
|
1116
|
+
showAllShort: "All",
|
|
1117
|
+
showNext: "Show {count} more",
|
|
1118
|
+
showNextShort: "+{count}"
|
|
1001
1119
|
}
|
|
1002
1120
|
};
|
|
1003
1121
|
function formatLabel(template, values = {}) {
|
|
@@ -1565,7 +1683,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1565
1683
|
type: Boolean,
|
|
1566
1684
|
default: true
|
|
1567
1685
|
},
|
|
1568
|
-
pageSize: { default:
|
|
1686
|
+
pageSize: { default: 15 },
|
|
1569
1687
|
deepSearchLimit: { default: 500 },
|
|
1570
1688
|
deepSearch: { default: "manual" },
|
|
1571
1689
|
zipWarnSize: {},
|
|
@@ -1831,18 +1949,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1831
1949
|
applySort(key, direction !== "desc");
|
|
1832
1950
|
}
|
|
1833
1951
|
});
|
|
1834
|
-
/**
|
|
1952
|
+
/** The row closing a long folder while its children are cut at `pageSize`. */
|
|
1835
1953
|
const shown = reactive(/* @__PURE__ */ new Map());
|
|
1954
|
+
function pageSizeOf() {
|
|
1955
|
+
return Math.max(0, Math.floor(props.pageSize ?? 15));
|
|
1956
|
+
}
|
|
1836
1957
|
function limitOf(key) {
|
|
1837
|
-
const size =
|
|
1958
|
+
const size = pageSizeOf();
|
|
1838
1959
|
return size ? shown.get(key) ?? size : Number.POSITIVE_INFINITY;
|
|
1839
1960
|
}
|
|
1840
1961
|
function isMore(row) {
|
|
1841
1962
|
return "more" in row;
|
|
1842
1963
|
}
|
|
1843
|
-
const
|
|
1964
|
+
const layout = computed(() => {
|
|
1844
1965
|
const out = [];
|
|
1966
|
+
const long = [];
|
|
1845
1967
|
const query = keyword.value.trim().toLowerCase();
|
|
1968
|
+
const size = pageSizeOf();
|
|
1846
1969
|
const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && (node.children.some(matches) || deepMode.value && !!node.lock);
|
|
1847
1970
|
const walk = (nodes, key, depth) => {
|
|
1848
1971
|
const list = nodes.filter(matches);
|
|
@@ -1857,10 +1980,70 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1857
1980
|
depth,
|
|
1858
1981
|
remaining: list.length - limit
|
|
1859
1982
|
});
|
|
1983
|
+
let end = out.length - 1;
|
|
1984
|
+
while (end >= 0 && isMore(out[end])) end -= 1;
|
|
1985
|
+
const last = out[end];
|
|
1986
|
+
if (!query && size > 0 && limit > size && last && !isMore(last)) long.push({
|
|
1987
|
+
key,
|
|
1988
|
+
depth: depth - 1,
|
|
1989
|
+
shown: limit,
|
|
1990
|
+
total: list.length,
|
|
1991
|
+
last: last.path
|
|
1992
|
+
});
|
|
1860
1993
|
};
|
|
1861
1994
|
walk(roots.value, props.sources.length ? "" : props.path, 0);
|
|
1862
|
-
return
|
|
1995
|
+
return {
|
|
1996
|
+
rows: out,
|
|
1997
|
+
long
|
|
1998
|
+
};
|
|
1863
1999
|
});
|
|
2000
|
+
const rows = computed(() => layout.value.rows);
|
|
2001
|
+
const pinned = ref(null);
|
|
2002
|
+
/** Height of the sticky selection bar above the pinned row (narrow lists only). */
|
|
2003
|
+
const pinOffset = ref(0);
|
|
2004
|
+
function rowElement(path) {
|
|
2005
|
+
return [...rootEl.value?.querySelectorAll("[data-path]") ?? []].find((el) => el.dataset.path === path);
|
|
2006
|
+
}
|
|
2007
|
+
/** The pinned folder's name: its own, or for a single path's top level, the list's title. */
|
|
2008
|
+
const pinnedName = computed(() => {
|
|
2009
|
+
const folder = pinned.value;
|
|
2010
|
+
if (!folder) return "";
|
|
2011
|
+
if (folder.depth < 0) return props.title || folder.key.split("/").filter(Boolean).pop() || "/";
|
|
2012
|
+
const find = (nodes) => {
|
|
2013
|
+
for (const node of nodes) {
|
|
2014
|
+
if (node.path === folder.key) return node;
|
|
2015
|
+
const inner = isWithin(folder.key, node.path) ? find(node.children) : void 0;
|
|
2016
|
+
if (inner) return inner;
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
return find(roots.value)?.name ?? "";
|
|
2020
|
+
});
|
|
2021
|
+
/** Pin the innermost long folder whose own row has scrolled away while its files are still on screen. */
|
|
2022
|
+
function updatePin() {
|
|
2023
|
+
const root = rootEl.value;
|
|
2024
|
+
if (!root) return;
|
|
2025
|
+
const bar = root.querySelector(".fl-bar");
|
|
2026
|
+
pinOffset.value = bar && getComputedStyle(bar).position === "sticky" ? bar.offsetHeight : 0;
|
|
2027
|
+
const top = stickyTop.value + pinOffset.value;
|
|
2028
|
+
const rowHeight = root.querySelector(".fl-head")?.offsetHeight ?? 40;
|
|
2029
|
+
let found = null;
|
|
2030
|
+
for (const folder of layout.value.long) {
|
|
2031
|
+
const head = folder.depth < 0 ? root.querySelector(".fl-head") : rowElement(folder.key);
|
|
2032
|
+
const last = rowElement(folder.last);
|
|
2033
|
+
if (!head || !last) continue;
|
|
2034
|
+
if (head.getBoundingClientRect().bottom <= top && last.getBoundingClientRect().bottom > top + rowHeight && (!found || folder.depth > found.depth)) found = folder;
|
|
2035
|
+
}
|
|
2036
|
+
pinned.value = found;
|
|
2037
|
+
}
|
|
2038
|
+
let pinFrame = 0;
|
|
2039
|
+
function schedulePin() {
|
|
2040
|
+
if (pinFrame) return;
|
|
2041
|
+
pinFrame = requestAnimationFrame(() => {
|
|
2042
|
+
pinFrame = 0;
|
|
2043
|
+
updatePin();
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
watch(layout, () => void nextTick(schedulePin));
|
|
1864
2047
|
const visible = computed(() => rows.value.filter((row) => !isMore(row)));
|
|
1865
2048
|
/** A name split around the filter keyword, for highlighting. */
|
|
1866
2049
|
function nameParts(name) {
|
|
@@ -2047,7 +2230,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2047
2230
|
}
|
|
2048
2231
|
async function download(node) {
|
|
2049
2232
|
if (node.is_dir) return;
|
|
2050
|
-
if (demo.value) {
|
|
2233
|
+
if (demo.value && !isDemoFile(node.path)) {
|
|
2051
2234
|
showToast(t("demoDownload", { name: node.name }));
|
|
2052
2235
|
return;
|
|
2053
2236
|
}
|
|
@@ -2151,7 +2334,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2151
2334
|
previewViewer.value = "";
|
|
2152
2335
|
previewError.value = "";
|
|
2153
2336
|
previewResolving.value = false;
|
|
2154
|
-
if (demo.value || !canPreview(node)) return;
|
|
2337
|
+
if (demo.value && !isDemoFile(node.path) || !canPreview(node)) return;
|
|
2155
2338
|
previewResolving.value = true;
|
|
2156
2339
|
try {
|
|
2157
2340
|
const url = await fileUrlOf(node);
|
|
@@ -2195,15 +2378,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2195
2378
|
stickyTop.value = top;
|
|
2196
2379
|
}
|
|
2197
2380
|
onMounted(() => {
|
|
2381
|
+
warnPublicSecrets(props, props.locale);
|
|
2198
2382
|
passwords.sync();
|
|
2199
2383
|
loadRoot();
|
|
2200
2384
|
loadSettings();
|
|
2201
2385
|
measureStickyTop();
|
|
2202
2386
|
window.addEventListener("resize", measureStickyTop, { passive: true });
|
|
2387
|
+
window.addEventListener("scroll", schedulePin, { passive: true });
|
|
2388
|
+
window.addEventListener("resize", schedulePin, { passive: true });
|
|
2203
2389
|
});
|
|
2204
2390
|
onBeforeUnmount(() => {
|
|
2205
2391
|
zipAbort?.abort();
|
|
2206
2392
|
window.removeEventListener("resize", measureStickyTop);
|
|
2393
|
+
window.removeEventListener("scroll", schedulePin);
|
|
2394
|
+
window.removeEventListener("resize", schedulePin);
|
|
2395
|
+
cancelAnimationFrame(pinFrame);
|
|
2207
2396
|
clearTimeout(toastTimer);
|
|
2208
2397
|
});
|
|
2209
2398
|
return (_ctx, _push, _parent, _attrs) => {
|
|
@@ -2287,7 +2476,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2287
2476
|
ssrRenderList(sortOptions.value, (option) => {
|
|
2288
2477
|
_push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
|
|
2289
2478
|
});
|
|
2290
|
-
_push(`<!--]--></select></label></span></div>`);
|
|
2479
|
+
_push(`<!--]--></select></label></span></div><!-- While scrolling through a long folder, its row stays at the top with “show less”. --><div class="fl-pin-anchor" style="${ssrRenderStyle({ "--fl-pin-offset": `${pinOffset.value}px` })}">`);
|
|
2480
|
+
if (pinned.value) {
|
|
2481
|
+
_push(`<div class="fl-pin" style="${ssrRenderStyle({ "--fl-depth": Math.max(pinned.value.depth, 0) })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-cell-name">`);
|
|
2482
|
+
if (pinned.value.depth >= 0) _push(`<span class="fl-indent" aria-hidden="true"></span>`);
|
|
2483
|
+
else _push(`<!---->`);
|
|
2484
|
+
if (pinned.value.depth >= 0) {
|
|
2485
|
+
_push(`<span class="fl-pin-arrow" aria-hidden="true">`);
|
|
2486
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
2487
|
+
name: "next",
|
|
2488
|
+
size: 14
|
|
2489
|
+
}, null, _parent));
|
|
2490
|
+
_push(`</span>`);
|
|
2491
|
+
} else _push(`<!---->`);
|
|
2492
|
+
_push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg><span class="fl-pin-name">${ssrInterpolate(pinnedName.value)}</span><span class="fl-pin-count">${ssrInterpolate(pinned.value.shown < pinned.value.total ? t("moreShown", {
|
|
2493
|
+
shown: pinned.value.shown,
|
|
2494
|
+
total: pinned.value.total
|
|
2495
|
+
}) : t("allShown", { count: pinned.value.total }))}</span></span><button class="fl-pin-less" type="button">${ssrInterpolate(t("showLess"))}`);
|
|
2496
|
+
_push(ssrRenderComponent(_sfc_main$2, {
|
|
2497
|
+
name: "up",
|
|
2498
|
+
size: 12
|
|
2499
|
+
}, null, _parent));
|
|
2500
|
+
_push(`</button></div>`);
|
|
2501
|
+
} else _push(`<!---->`);
|
|
2502
|
+
_push(`</div>`);
|
|
2291
2503
|
if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
|
|
2292
2504
|
else if (rootLock.value) {
|
|
2293
2505
|
_push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.value.error }, "fl-unlock-root"])}">`);
|
|
@@ -2318,10 +2530,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2318
2530
|
ssrRenderList(rows.value, (node, index) => {
|
|
2319
2531
|
_push(`<!--[-->`);
|
|
2320
2532
|
if (isMore(node)) {
|
|
2321
|
-
_push(`<div class="fl-row fl-more" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"
|
|
2322
|
-
if (node.remaining > (__props.pageSize || 0)) _push(`<!--[--><button class="fl-more-
|
|
2323
|
-
else _push(`<button class="fl-more-
|
|
2324
|
-
_push(`</span></div>`);
|
|
2533
|
+
_push(`<div class="fl-row fl-more" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><!-- Reads as one more row of the folder: “⋯” in the type column, the count where names start. --><span class="fl-indent" aria-hidden="true"></span><span class="fl-arrow-placeholder" aria-hidden="true"></span><span class="fl-more-tag" aria-hidden="true">⋯</span><span class="fl-more-count">${ssrInterpolate(t("moreRemaining", { count: node.remaining }))}</span><span class="fl-more-actions">`);
|
|
2534
|
+
if (node.remaining > (__props.pageSize || 0)) _push(`<!--[--><button class="fl-more-link" type="button"${ssrRenderAttr("aria-label", t("showNext", { count: __props.pageSize }))}><span class="fl-more-long">${ssrInterpolate(t("showNext", { count: __props.pageSize }))}</span><span class="fl-more-short" aria-hidden="true">${ssrInterpolate(t("showNextShort", { count: __props.pageSize }))}</span></button><button class="fl-more-link fl-more-all" type="button"${ssrRenderAttr("aria-label", t("showAll"))}><span class="fl-more-long">${ssrInterpolate(t("showAll"))}</span><span class="fl-more-short" aria-hidden="true">${ssrInterpolate(t("showAllShort"))}</span></button><!--]-->`);
|
|
2535
|
+
else _push(`<button class="fl-more-link fl-more-all" type="button">${ssrInterpolate(t("showRest", { count: node.remaining }))}</button>`);
|
|
2536
|
+
_push(`</span></span></div>`);
|
|
2325
2537
|
} else {
|
|
2326
2538
|
_push(`<div class="${ssrRenderClass([{
|
|
2327
2539
|
"fl-row-dir": node.is_dir,
|
|
@@ -2330,7 +2542,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2330
2542
|
"fl-row-locked": !!node.lock,
|
|
2331
2543
|
"fl-row-prompting": node.lock?.prompting,
|
|
2332
2544
|
"fl-row-error": node.lock?.prompting && !!node.lock.error
|
|
2333
|
-
}, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock || batchBusy.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
|
|
2545
|
+
}, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"${ssrRenderAttr("data-path", node.path)}><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock || batchBusy.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
|
|
2334
2546
|
if (node.lock) {
|
|
2335
2547
|
_push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}>`);
|
|
2336
2548
|
_push(ssrRenderComponent(_sfc_main$2, {
|
|
@@ -2463,7 +2675,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2463
2675
|
viewer: previewViewer.value,
|
|
2464
2676
|
resolving: previewResolving.value,
|
|
2465
2677
|
error: previewError.value,
|
|
2466
|
-
demo: demo.value,
|
|
2678
|
+
demo: demo.value && !!previewNode.value && !unref(isDemoFile)(previewNode.value.path),
|
|
2467
2679
|
position: previewPosition.value,
|
|
2468
2680
|
total: previewPosition.value ? previewList.value.length : 0,
|
|
2469
2681
|
onClose: closePreview,
|
|
@@ -2502,4 +2714,4 @@ function enhanceAppWithFileList({ app }) {
|
|
|
2502
2714
|
if (!app._context.components.FileList) app.component("FileList", _sfc_main);
|
|
2503
2715
|
if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
|
|
2504
2716
|
}
|
|
2505
|
-
export { _sfc_main as FileList, _sfc_main as VPFileList,
|
|
2717
|
+
export { _sfc_main as FileList, _sfc_main as VPFileList, enhanceAppWithFileList, fileListLocales, formatLabel, resolveLabels };
|