vitepress-plugin-filelist 0.6.1 → 0.7.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 +26 -3
- package/dist/client/browser/index.d.ts +28 -1
- package/dist/client/browser/index.js +459 -119
- package/dist/client/ssr/index.d.ts +28 -1
- package/dist/client/ssr/index.js +278 -21
- package/dist/client/style.css +101 -5
- package/package.json +8 -2
|
@@ -92,6 +92,27 @@ interface FileListLabels {
|
|
|
92
92
|
unlockFolder: string;
|
|
93
93
|
/** @deprecated Network failures now append `corsHint`. */
|
|
94
94
|
networkHint: string;
|
|
95
|
+
/** Deep search: the row at the end of a filtered list. `{count}` where noted */
|
|
96
|
+
deepHint: string;
|
|
97
|
+
deepSearch: string;
|
|
98
|
+
deepRunning: string;
|
|
99
|
+
deepStop: string;
|
|
100
|
+
deepDone: string;
|
|
101
|
+
deepCapped: string;
|
|
102
|
+
deepContinue: string;
|
|
103
|
+
deepSkipped: string;
|
|
104
|
+
deepFailed: string;
|
|
105
|
+
/** Markdown / HTML preview: the source / rendered switch. */
|
|
106
|
+
viewMode: string;
|
|
107
|
+
viewSource: string;
|
|
108
|
+
viewRendered: string;
|
|
109
|
+
/** Above a rendered HTML file. */
|
|
110
|
+
htmlSandboxNote: string;
|
|
111
|
+
/** Stands in for a Markdown image with a relative path. `{alt}` */
|
|
112
|
+
imageOmitted: string;
|
|
113
|
+
/** Title of the dialog when a batch ZIP fails (the reason is the text). */
|
|
114
|
+
batchFailedTitle: string;
|
|
115
|
+
batchFailedHint: string;
|
|
95
116
|
/** Single file handed to the browser's download manager. `{name}` */
|
|
96
117
|
downloadStarted: string;
|
|
97
118
|
/** Batch progress detail. `{done}` `{total}` sizes, `{files}` `{count}` file counts */
|
|
@@ -153,6 +174,11 @@ interface FileListProps {
|
|
|
153
174
|
* everything. Data is still loaded in full; only rendering is limited.
|
|
154
175
|
*/
|
|
155
176
|
pageSize?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Deep search loads at most this many folders per click (“Continue” goes on).
|
|
179
|
+
* Defaults to `500`.
|
|
180
|
+
*/
|
|
181
|
+
deepSearchLimit?: number;
|
|
156
182
|
/**
|
|
157
183
|
* Browsers that must build the ZIP in memory (Firefox, Safari) ask first above
|
|
158
184
|
* this many MB. Defaults to 500 (200 on touch devices); `0` never asks.
|
|
@@ -189,6 +215,7 @@ declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {},
|
|
|
189
215
|
title: string;
|
|
190
216
|
rememberPassword: boolean;
|
|
191
217
|
pageSize: number;
|
|
218
|
+
deepSearchLimit: number;
|
|
192
219
|
locale: FileListLocale;
|
|
193
220
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
194
221
|
declare global {
|
|
@@ -307,7 +334,7 @@ export declare function previewKind(name: string): PreviewKind;
|
|
|
307
334
|
export declare function totalSize(items: Array<{
|
|
308
335
|
size?: number;
|
|
309
336
|
}>): number;
|
|
310
|
-
export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean): number;
|
|
337
|
+
export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean, locale?: string): number;
|
|
311
338
|
interface IframePreview {
|
|
312
339
|
label: string;
|
|
313
340
|
scheme: string;
|
package/dist/client/ssr/index.js
CHANGED
|
@@ -179,12 +179,29 @@ function previewKind(name) {
|
|
|
179
179
|
function totalSize(items) {
|
|
180
180
|
return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
const collators = /* @__PURE__ */ new Map();
|
|
183
|
+
/**
|
|
184
|
+
* Name order for a UI locale: pinyin for Chinese, alphabetical for English,
|
|
185
|
+
* with numbers in natural order (“2” before “10”) either way.
|
|
186
|
+
*/
|
|
187
|
+
function collatorFor(locale) {
|
|
188
|
+
const tag = locale.toLowerCase().startsWith("zh") ? "zh-Hans-CN" : "en";
|
|
189
|
+
let collator = collators.get(tag);
|
|
190
|
+
if (!collator) {
|
|
191
|
+
collator = new Intl.Collator(tag, {
|
|
192
|
+
numeric: true,
|
|
193
|
+
sensitivity: "base"
|
|
194
|
+
});
|
|
195
|
+
collators.set(tag, collator);
|
|
196
|
+
}
|
|
197
|
+
return collator;
|
|
198
|
+
}
|
|
199
|
+
function compareNodes(a, b, key, ascending = true, locale = "zh-CN") {
|
|
183
200
|
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
184
201
|
let result = 0;
|
|
185
202
|
if (key === "size") result = a.size - b.size;
|
|
186
203
|
else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
|
|
187
|
-
else result = a.name.
|
|
204
|
+
else result = collatorFor(locale).compare(a.name, b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
188
205
|
return ascending ? result : -result;
|
|
189
206
|
}
|
|
190
207
|
/**
|
|
@@ -852,6 +869,22 @@ const fileListLocales = {
|
|
|
852
869
|
forgetPassword: "忘记",
|
|
853
870
|
unlockFolder: "输入 {name} 的密码",
|
|
854
871
|
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
872
|
+
deepHint: "只筛选了已加载的内容",
|
|
873
|
+
deepSearch: "深度搜索",
|
|
874
|
+
deepRunning: "正在深度搜索 · 已查找 {count} 个文件夹",
|
|
875
|
+
deepStop: "停止",
|
|
876
|
+
deepDone: "已搜索全部 {count} 个文件夹",
|
|
877
|
+
deepCapped: "已查找 {count} 个文件夹,还有未搜索的子目录",
|
|
878
|
+
deepContinue: "继续",
|
|
879
|
+
deepSkipped: "跳过 {count} 个加密文件夹",
|
|
880
|
+
deepFailed: "{count} 个文件夹读取失败",
|
|
881
|
+
viewMode: "显示方式",
|
|
882
|
+
viewSource: "源码",
|
|
883
|
+
viewRendered: "渲染",
|
|
884
|
+
htmlSandboxNote: "已禁用脚本;网页引用的相对路径资源不会加载",
|
|
885
|
+
imageOmitted: "[图片:{alt}]",
|
|
886
|
+
batchFailedTitle: "打包失败",
|
|
887
|
+
batchFailedHint: "可以减少勾选的文件后重试。",
|
|
855
888
|
downloadStarted: "已交给浏览器下载:{name}",
|
|
856
889
|
batchProgress: "{done} / {total} · {files} / {count} 个文件",
|
|
857
890
|
batchDone: "已打包 {name}({count} 个文件,{size})",
|
|
@@ -930,6 +963,22 @@ const fileListLocales = {
|
|
|
930
963
|
forgetPassword: "Forget",
|
|
931
964
|
unlockFolder: "Enter the password for {name}",
|
|
932
965
|
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
966
|
+
deepHint: "Only loaded folders are filtered",
|
|
967
|
+
deepSearch: "Deep search",
|
|
968
|
+
deepRunning: "Searching · {count} folders checked",
|
|
969
|
+
deepStop: "Stop",
|
|
970
|
+
deepDone: "Searched all {count} folders",
|
|
971
|
+
deepCapped: "{count} folders checked; some sub-folders remain",
|
|
972
|
+
deepContinue: "Continue",
|
|
973
|
+
deepSkipped: "{count} locked folders skipped",
|
|
974
|
+
deepFailed: "{count} folders could not be read",
|
|
975
|
+
viewMode: "View",
|
|
976
|
+
viewSource: "Source",
|
|
977
|
+
viewRendered: "Rendered",
|
|
978
|
+
htmlSandboxNote: "Scripts are disabled; resources at relative paths do not load",
|
|
979
|
+
imageOmitted: "[Image: {alt}]",
|
|
980
|
+
batchFailedTitle: "Could not create the ZIP",
|
|
981
|
+
batchFailedHint: "Try again with fewer files selected.",
|
|
933
982
|
downloadStarted: "Downloading {name} in your browser",
|
|
934
983
|
batchProgress: "{done} / {total} · {files} / {count} files",
|
|
935
984
|
batchDone: "Saved {name} ({count} files, {size})",
|
|
@@ -956,6 +1005,45 @@ function resolveLabels(locale = "zh-CN", overrides) {
|
|
|
956
1005
|
...overrides
|
|
957
1006
|
};
|
|
958
1007
|
}
|
|
1008
|
+
// src/client/markdown.ts
|
|
1009
|
+
/**
|
|
1010
|
+
* Markdown → safe HTML for the preview dialog. Loaded on demand, so pages that
|
|
1011
|
+
* never render Markdown do not download markdown-it or DOMPurify.
|
|
1012
|
+
*
|
|
1013
|
+
* - Raw HTML in the file is allowed, then sanitised (no scripts, handlers, iframes...)
|
|
1014
|
+
* - Only absolute http(s) / mailto links stay clickable, opening in a new tab;
|
|
1015
|
+
* relative links (to other files in the share) become plain text
|
|
1016
|
+
* - Images with a relative source are replaced by their alt text, because the
|
|
1017
|
+
* share's relative paths do not resolve on the documentation site
|
|
1018
|
+
*/
|
|
1019
|
+
async function renderMarkdown(source, imageLabel) {
|
|
1020
|
+
const [{ default: MarkdownIt }, { default: DOMPurify }] = await Promise.all([import("markdown-it"), import("dompurify")]);
|
|
1021
|
+
const probe = DOMPurify.isSupported ? DOMPurify.sanitize("<img src=\"x\" onerror=\"1\"><script>1<\/script>") : "<script";
|
|
1022
|
+
if (/<script|onerror/i.test(probe)) throw new Error("HTML sanitiser unavailable");
|
|
1023
|
+
const html = new MarkdownIt({
|
|
1024
|
+
html: true,
|
|
1025
|
+
linkify: true
|
|
1026
|
+
}).render(source);
|
|
1027
|
+
const template = document.createElement("template");
|
|
1028
|
+
template.innerHTML = DOMPurify.sanitize(html);
|
|
1029
|
+
const fragment = template.content;
|
|
1030
|
+
for (const link of fragment.querySelectorAll("a")) {
|
|
1031
|
+
const href = link.getAttribute("href") ?? "";
|
|
1032
|
+
if (/^(https?:|mailto:)/i.test(href)) {
|
|
1033
|
+
link.setAttribute("target", "_blank");
|
|
1034
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
1035
|
+
} else link.replaceWith(...link.childNodes);
|
|
1036
|
+
}
|
|
1037
|
+
for (const image of fragment.querySelectorAll("img")) {
|
|
1038
|
+
const src = image.getAttribute("src") ?? "";
|
|
1039
|
+
if (/^(https?:|data:image\/)/i.test(src)) continue;
|
|
1040
|
+
const placeholder = document.createElement("span");
|
|
1041
|
+
placeholder.className = "fl-md-image";
|
|
1042
|
+
placeholder.textContent = imageLabel(image.getAttribute("alt") || src.split("/").pop() || "");
|
|
1043
|
+
image.replaceWith(placeholder);
|
|
1044
|
+
}
|
|
1045
|
+
return template.innerHTML;
|
|
1046
|
+
}
|
|
959
1047
|
// src/client/PreviewDialog.vue
|
|
960
1048
|
const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
961
1049
|
__name: "PreviewDialog",
|
|
@@ -990,7 +1078,9 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
990
1078
|
/**
|
|
991
1079
|
* File preview in a native <dialog>, without leaving the page.
|
|
992
1080
|
*
|
|
993
|
-
* image / video / audio: native elements; text: the first `maxTextBytes
|
|
1081
|
+
* image / video / audio: native elements; text: the first `maxTextBytes`,
|
|
1082
|
+
* with Markdown and HTML switchable between source (default) and rendered
|
|
1083
|
+
* (Markdown sanitised in VitePress' doc style, HTML in a script-less sandbox);
|
|
994
1084
|
* through a Range request; pdf: the first matching OpenList `iframe_previews`
|
|
995
1085
|
* viewer, else the browser's reader through a Blob. Anything else (Office
|
|
996
1086
|
* included), failures, oversized PDFs and demo mode: file info + download.
|
|
@@ -1008,8 +1098,18 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1008
1098
|
const blobUrl = ref("");
|
|
1009
1099
|
const rotation = ref(0);
|
|
1010
1100
|
const actualSize = ref(false);
|
|
1101
|
+
/** Markdown / HTML: show the source or render it. Kept while the page is open. */
|
|
1102
|
+
const view = ref("source");
|
|
1103
|
+
const renderedHtml = ref("");
|
|
1011
1104
|
let controller;
|
|
1012
1105
|
let loadSeq = 0;
|
|
1106
|
+
/** Text files that can also be rendered. */
|
|
1107
|
+
const renderAs = computed(() => {
|
|
1108
|
+
const name = props.file?.name ?? "";
|
|
1109
|
+
if (/\.(md|markdown)$/i.test(name)) return "markdown";
|
|
1110
|
+
if (/\.html?$/i.test(name)) return "html";
|
|
1111
|
+
return null;
|
|
1112
|
+
});
|
|
1013
1113
|
const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
|
|
1014
1114
|
const mode = computed(() => {
|
|
1015
1115
|
if (props.demo) return "demo";
|
|
@@ -1019,6 +1119,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1019
1119
|
return kind.value;
|
|
1020
1120
|
});
|
|
1021
1121
|
const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
|
|
1122
|
+
const canRender = computed(() => !!renderAs.value && mode.value === "text" && !busy.value);
|
|
1022
1123
|
const infoMessage = computed(() => {
|
|
1023
1124
|
const file = props.file;
|
|
1024
1125
|
if (!file) return "";
|
|
@@ -1039,6 +1140,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1039
1140
|
loadingBody.value = false;
|
|
1040
1141
|
bodyFailed.value = false;
|
|
1041
1142
|
textContent.value = "";
|
|
1143
|
+
renderedHtml.value = "";
|
|
1042
1144
|
truncated.value = false;
|
|
1043
1145
|
rotation.value = 0;
|
|
1044
1146
|
actualSize.value = false;
|
|
@@ -1120,6 +1222,20 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1120
1222
|
props.error,
|
|
1121
1223
|
props.demo
|
|
1122
1224
|
], () => void loadBody(), { immediate: true });
|
|
1225
|
+
watch(() => [
|
|
1226
|
+
view.value,
|
|
1227
|
+
renderAs.value,
|
|
1228
|
+
textContent.value
|
|
1229
|
+
], async ([current, as, text]) => {
|
|
1230
|
+
if (current !== "rendered" || as !== "markdown" || !text) return;
|
|
1231
|
+
const seq = loadSeq;
|
|
1232
|
+
try {
|
|
1233
|
+
const html = await renderMarkdown(text, (alt) => t("imageOmitted", { alt }));
|
|
1234
|
+
if (seq === loadSeq) renderedHtml.value = html;
|
|
1235
|
+
} catch {
|
|
1236
|
+
if (seq === loadSeq) view.value = "source";
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1123
1239
|
watch(() => props.open, (open) => {
|
|
1124
1240
|
const el = dialog.value;
|
|
1125
1241
|
if (!el) return;
|
|
@@ -1141,7 +1257,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1141
1257
|
}, _attrs))}>`);
|
|
1142
1258
|
if (__props.open && __props.file) {
|
|
1143
1259
|
_push(`<div class="fl-preview-panel"><header class="fl-preview-head"><span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><div class="fl-preview-title"><span class="fl-preview-name">${ssrInterpolate(__props.file.name)}</span><span class="fl-preview-meta">${ssrInterpolate(unref(formatSize)(__props.file.size))} · ${ssrInterpolate(unref(formatDate)(__props.file.modified))}</span></div>`);
|
|
1144
|
-
if (__props.total > 1 || mode.value === "image" && !busy.value) {
|
|
1260
|
+
if (__props.total > 1 || mode.value === "image" && !busy.value || canRender.value) {
|
|
1145
1261
|
_push(`<div class="fl-preview-tools fl-preview-nav">`);
|
|
1146
1262
|
if (__props.total > 1) {
|
|
1147
1263
|
_push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}>`);
|
|
@@ -1169,6 +1285,13 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1169
1285
|
}, null, _parent));
|
|
1170
1286
|
_push(`</button><!--]-->`);
|
|
1171
1287
|
} else _push(`<!---->`);
|
|
1288
|
+
if (canRender.value) {
|
|
1289
|
+
_push(`<span class="fl-preview-view" role="group"${ssrRenderAttr("aria-label", t("viewMode"))}><!--[-->`);
|
|
1290
|
+
ssrRenderList(["source", "rendered"], (option) => {
|
|
1291
|
+
_push(`<button class="fl-preview-view-btn" type="button"${ssrRenderAttr("aria-pressed", view.value === option)}>${ssrInterpolate(t(option === "source" ? "viewSource" : "viewRendered"))}</button>`);
|
|
1292
|
+
});
|
|
1293
|
+
_push(`<!--]--></span>`);
|
|
1294
|
+
} else _push(`<!---->`);
|
|
1172
1295
|
_push(`</div>`);
|
|
1173
1296
|
} else _push(`<!---->`);
|
|
1174
1297
|
_push(`<div class="fl-preview-tools fl-preview-actions">`);
|
|
@@ -1191,10 +1314,17 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1191
1314
|
else if (mode.value === "video") _push(`<video class="fl-preview-video"${ssrRenderAttr("src", __props.src)} controls playsinline preload="metadata"></video>`);
|
|
1192
1315
|
else if (mode.value === "audio") _push(`<audio class="fl-preview-audio"${ssrRenderAttr("src", __props.src)} controls preload="metadata"></audio>`);
|
|
1193
1316
|
else if (mode.value === "text") {
|
|
1194
|
-
_push(`<div class="fl-preview-text">`);
|
|
1317
|
+
_push(`<div class="${ssrRenderClass([{ "fl-preview-rendered": renderAs.value && view.value === "rendered" }, "fl-preview-text"])}">`);
|
|
1195
1318
|
if (truncated.value) _push(`<p class="fl-preview-note">${ssrInterpolate(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) }))}</p>`);
|
|
1196
1319
|
else _push(`<!---->`);
|
|
1197
|
-
|
|
1320
|
+
if (renderAs.value === "markdown" && view.value === "rendered") {
|
|
1321
|
+
_push(`<!--[--><!-- sanitised by renderMarkdown (DOMPurify) -->`);
|
|
1322
|
+
if (renderedHtml.value) _push(`<div class="vp-doc fl-md">${renderedHtml.value ?? ""}</div>`);
|
|
1323
|
+
else _push(`<div class="fl-preview-status" role="status">${ssrInterpolate(t("loading"))}</div>`);
|
|
1324
|
+
_push(`<!--]-->`);
|
|
1325
|
+
} else if (renderAs.value === "html" && view.value === "rendered") _push(`<!--[--><p class="fl-preview-note">${ssrInterpolate(t("htmlSandboxNote"))}</p><!-- empty sandbox: no scripts, no forms, no popups, an opaque origin --><iframe class="fl-preview-html" sandbox="" referrerpolicy="no-referrer"${ssrRenderAttr("srcdoc", textContent.value)}${ssrRenderAttr("title", __props.file.name)}></iframe><!--]-->`);
|
|
1326
|
+
else _push(`<pre>${ssrInterpolate(textContent.value)}</pre>`);
|
|
1327
|
+
_push(`</div>`);
|
|
1198
1328
|
} else if (mode.value === "pdf") _push(`<iframe class="fl-preview-frame"${ssrRenderAttr("src", __props.viewer || blobUrl.value)}${ssrRenderAttr("title", __props.file.name)}></iframe>`);
|
|
1199
1329
|
else {
|
|
1200
1330
|
_push(`<div class="fl-preview-info"><span class="fl-type fl-preview-badge"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><p class="fl-preview-message">${ssrInterpolate(infoMessage.value)}</p>`);
|
|
@@ -1243,9 +1373,9 @@ function toNodes(items, parent, password) {
|
|
|
1243
1373
|
zipPath: [...parent.zipPath, item.name]
|
|
1244
1374
|
}));
|
|
1245
1375
|
}
|
|
1246
|
-
function sortTree(nodes, key, ascending) {
|
|
1247
|
-
nodes.sort((a, b) => compareNodes(a, b, key, ascending));
|
|
1248
|
-
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
|
|
1376
|
+
function sortTree(nodes, key, ascending, locale) {
|
|
1377
|
+
nodes.sort((a, b) => compareNodes(a, b, key, ascending, locale));
|
|
1378
|
+
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending, locale);
|
|
1249
1379
|
}
|
|
1250
1380
|
/** Loaded files at or under a node. */
|
|
1251
1381
|
function descendantFiles(node) {
|
|
@@ -1432,6 +1562,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1432
1562
|
default: true
|
|
1433
1563
|
},
|
|
1434
1564
|
pageSize: { default: 200 },
|
|
1565
|
+
deepSearchLimit: { default: 500 },
|
|
1435
1566
|
zipWarnSize: {},
|
|
1436
1567
|
zipMaxSize: {},
|
|
1437
1568
|
locale: { default: "zh-CN" },
|
|
@@ -1496,7 +1627,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1496
1627
|
return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
|
|
1497
1628
|
}
|
|
1498
1629
|
function sortNodes(nodes) {
|
|
1499
|
-
sortTree(nodes, sortKey.value, sortAsc.value);
|
|
1630
|
+
sortTree(nodes, sortKey.value, sortAsc.value, props.locale);
|
|
1500
1631
|
}
|
|
1501
1632
|
function focusUnlock(path) {
|
|
1502
1633
|
nextTick(() => {
|
|
@@ -1528,7 +1659,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1528
1659
|
* Load a folder's children. A password error locks the folder instead of
|
|
1529
1660
|
* toasting; `prompt` also shows its same-row input (and focuses it).
|
|
1530
1661
|
*/
|
|
1531
|
-
async function loadChildren(node, prompt = false) {
|
|
1662
|
+
async function loadChildren(node, prompt = false, quiet = false) {
|
|
1532
1663
|
if (node.loaded) return true;
|
|
1533
1664
|
if (node.lock) {
|
|
1534
1665
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
@@ -1544,7 +1675,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1544
1675
|
node.expanded = false;
|
|
1545
1676
|
node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1546
1677
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1547
|
-
} else showToast(failureMessage("folderFailed", error));
|
|
1678
|
+
} else if (!quiet) showToast(failureMessage("folderFailed", error));
|
|
1548
1679
|
return false;
|
|
1549
1680
|
} finally {
|
|
1550
1681
|
node.loading = false;
|
|
@@ -1631,6 +1762,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1631
1762
|
}
|
|
1632
1763
|
/** First load, or a retry after an error: shows the loading state. */
|
|
1633
1764
|
async function loadRoot() {
|
|
1765
|
+
stopDeepSearch();
|
|
1634
1766
|
refreshSeq += 1;
|
|
1635
1767
|
refreshing.value = false;
|
|
1636
1768
|
loadingRoot.value = true;
|
|
@@ -1707,6 +1839,88 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1707
1839
|
return out;
|
|
1708
1840
|
});
|
|
1709
1841
|
const visible = computed(() => rows.value.filter((row) => !isMore(row)));
|
|
1842
|
+
/** A name split around the filter keyword, for highlighting. */
|
|
1843
|
+
function nameParts(name) {
|
|
1844
|
+
const query = keyword.value.trim().toLowerCase();
|
|
1845
|
+
if (!query) return [{
|
|
1846
|
+
text: name,
|
|
1847
|
+
hit: false
|
|
1848
|
+
}];
|
|
1849
|
+
const parts = [];
|
|
1850
|
+
const lower = name.toLowerCase();
|
|
1851
|
+
let from = 0;
|
|
1852
|
+
for (let at = lower.indexOf(query); at !== -1; at = lower.indexOf(query, from)) {
|
|
1853
|
+
if (at > from) parts.push({
|
|
1854
|
+
text: name.slice(from, at),
|
|
1855
|
+
hit: false
|
|
1856
|
+
});
|
|
1857
|
+
parts.push({
|
|
1858
|
+
text: name.slice(at, at + query.length),
|
|
1859
|
+
hit: true
|
|
1860
|
+
});
|
|
1861
|
+
from = at + query.length;
|
|
1862
|
+
}
|
|
1863
|
+
if (from < name.length) parts.push({
|
|
1864
|
+
text: name.slice(from),
|
|
1865
|
+
hit: false
|
|
1866
|
+
});
|
|
1867
|
+
return parts;
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* The filter only sees loaded folders. Deep search loads the rest under the
|
|
1871
|
+
* mounted paths (four at a time, `deepSearchLimit` per run) so matches appear
|
|
1872
|
+
* in place, in their folders, like any other row. Locked folders are skipped.
|
|
1873
|
+
*/
|
|
1874
|
+
const deep = reactive({
|
|
1875
|
+
running: false,
|
|
1876
|
+
searched: 0,
|
|
1877
|
+
skipped: 0,
|
|
1878
|
+
failed: 0,
|
|
1879
|
+
capped: false,
|
|
1880
|
+
done: false
|
|
1881
|
+
});
|
|
1882
|
+
let deepSeq = 0;
|
|
1883
|
+
/** Folders that are known but not loaded yet (locked ones excluded). */
|
|
1884
|
+
function unloadedFolders() {
|
|
1885
|
+
const out = [];
|
|
1886
|
+
const walk = (nodes) => {
|
|
1887
|
+
for (const node of nodes) {
|
|
1888
|
+
if (!node.is_dir || node.lock) continue;
|
|
1889
|
+
if (!node.loaded) out.push(node);
|
|
1890
|
+
else walk(node.children);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
walk(roots.value);
|
|
1894
|
+
return out;
|
|
1895
|
+
}
|
|
1896
|
+
const hasUnloaded = computed(() => unloadedFolders().length > 0);
|
|
1897
|
+
const deepRow = computed(() => {
|
|
1898
|
+
if (!keyword.value.trim()) return null;
|
|
1899
|
+
if (deep.running) return "running";
|
|
1900
|
+
if (deep.capped) return "capped";
|
|
1901
|
+
if (hasUnloaded.value) return "idle";
|
|
1902
|
+
return deep.done ? "done" : null;
|
|
1903
|
+
});
|
|
1904
|
+
function stopDeepSearch() {
|
|
1905
|
+
deepSeq += 1;
|
|
1906
|
+
deep.running = false;
|
|
1907
|
+
}
|
|
1908
|
+
watch(keyword, (value) => {
|
|
1909
|
+
if (value.trim()) return;
|
|
1910
|
+
stopDeepSearch();
|
|
1911
|
+
Object.assign(deep, {
|
|
1912
|
+
searched: 0,
|
|
1913
|
+
skipped: 0,
|
|
1914
|
+
failed: 0,
|
|
1915
|
+
capped: false,
|
|
1916
|
+
done: false
|
|
1917
|
+
});
|
|
1918
|
+
});
|
|
1919
|
+
const deepSummary = computed(() => [
|
|
1920
|
+
deepRow.value === "capped" ? t("deepCapped", { count: deep.searched }) : t("deepDone", { count: deep.searched }),
|
|
1921
|
+
deep.skipped ? t("deepSkipped", { count: deep.skipped }) : "",
|
|
1922
|
+
deep.failed ? t("deepFailed", { count: deep.failed }) : ""
|
|
1923
|
+
].filter(Boolean).join(" · "));
|
|
1710
1924
|
const stats = computed(() => folderStats(roots.value, selected.value));
|
|
1711
1925
|
const noStats = {
|
|
1712
1926
|
files: 0,
|
|
@@ -1750,9 +1964,28 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1750
1964
|
showToast(t("downloadFailed", { message: errorText(error) }));
|
|
1751
1965
|
}
|
|
1752
1966
|
}
|
|
1967
|
+
/** A ZIP question or notice: over the warning size, over the limit, or failed. */
|
|
1753
1968
|
const zipNotice = ref(null);
|
|
1754
1969
|
/** Only desktop browsers get “use Chrome or Edge”; on iPhone every browser is Safari. */
|
|
1755
1970
|
const zipTip = computed(() => typeof window !== "undefined" && !window.matchMedia?.("(pointer: coarse)").matches ? t("zipStreamTip") : "");
|
|
1971
|
+
const zipDialog = computed(() => {
|
|
1972
|
+
const notice = zipNotice.value;
|
|
1973
|
+
if (!notice) return null;
|
|
1974
|
+
if (notice.kind === "failed") return {
|
|
1975
|
+
title: t("batchFailedTitle"),
|
|
1976
|
+
text: notice.message,
|
|
1977
|
+
tip: t("batchFailedHint"),
|
|
1978
|
+
cancel: t("gotIt")
|
|
1979
|
+
};
|
|
1980
|
+
const warn = notice.kind === "warn";
|
|
1981
|
+
return {
|
|
1982
|
+
title: t(warn ? "zipWarnTitle" : "zipBlockTitle", { size: formatSize(notice.size) }),
|
|
1983
|
+
text: t(warn ? "zipWarnText" : "zipBlockText", { limit: formatSize(notice.limit) }),
|
|
1984
|
+
tip: zipTip.value,
|
|
1985
|
+
confirm: warn ? t("zipContinue") : void 0,
|
|
1986
|
+
cancel: warn ? t("cancel") : t("gotIt")
|
|
1987
|
+
};
|
|
1988
|
+
});
|
|
1756
1989
|
function zipName() {
|
|
1757
1990
|
return zipFileName(props.title || t("defaultZip"));
|
|
1758
1991
|
}
|
|
@@ -1783,7 +2016,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1783
2016
|
}));
|
|
1784
2017
|
selected.value = /* @__PURE__ */ new Set();
|
|
1785
2018
|
} catch (error) {
|
|
1786
|
-
|
|
2019
|
+
if (controller.signal.aborted) showToast(t("batchCanceled"));
|
|
2020
|
+
else zipNotice.value = {
|
|
2021
|
+
kind: "failed",
|
|
2022
|
+
message: errorText(error)
|
|
2023
|
+
};
|
|
1787
2024
|
} finally {
|
|
1788
2025
|
zipAbort = null;
|
|
1789
2026
|
batchBusy.value = false;
|
|
@@ -2004,7 +2241,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2004
2241
|
} else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
|
|
2005
2242
|
if (node.is_dir) _push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>`);
|
|
2006
2243
|
else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
|
|
2007
|
-
_push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}"
|
|
2244
|
+
_push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}"><!--[-->`);
|
|
2245
|
+
ssrRenderList(nameParts(node.name), (part, at) => {
|
|
2246
|
+
_push(`<!--[-->`);
|
|
2247
|
+
if (part.hit) _push(`<mark class="fl-hit">${ssrInterpolate(part.text)}</mark>`);
|
|
2248
|
+
else _push(`<!--[-->${ssrInterpolate(part.text)}<!--]-->`);
|
|
2249
|
+
_push(`<!--]-->`);
|
|
2250
|
+
});
|
|
2251
|
+
_push(`<!--]--></span><span class="fl-meta">`);
|
|
2008
2252
|
if (!node.is_dir) _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
|
|
2009
2253
|
else if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))} · ${ssrInterpolate(unref(formatSize)(statsOf(node).bytes))}<!--]-->`);
|
|
2010
2254
|
else _push(`<!---->`);
|
|
@@ -2076,16 +2320,29 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2076
2320
|
}
|
|
2077
2321
|
_push(`<!--]-->`);
|
|
2078
2322
|
});
|
|
2079
|
-
_push(`<!--]
|
|
2323
|
+
_push(`<!--]-->`);
|
|
2324
|
+
if (deepRow.value) {
|
|
2325
|
+
_push(`<div class="fl-row fl-deep" role="status"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><span class="fl-indent" aria-hidden="true"></span>`);
|
|
2326
|
+
if (deepRow.value === "running") _push(`<!--[--><span class="fl-more-count">${ssrInterpolate(t("deepRunning", { count: deep.searched }))}</span><button class="fl-more-btn fl-more-all" type="button">${ssrInterpolate(t("deepStop"))}</button><span class="fl-deep-bar" aria-hidden="true"></span><!--]-->`);
|
|
2327
|
+
else if (deepRow.value === "idle") _push(`<!--[--><span class="fl-more-count">${ssrInterpolate(deep.searched ? deepSummary.value : t("deepHint"))}</span><button class="fl-more-btn" type="button">${ssrInterpolate(t("deepSearch"))}</button><!--]-->`);
|
|
2328
|
+
else {
|
|
2329
|
+
_push(`<!--[--><span class="fl-more-count">${ssrInterpolate(deepSummary.value)}</span>`);
|
|
2330
|
+
if (deepRow.value === "capped") _push(`<button class="fl-more-btn" type="button">${ssrInterpolate(t("deepContinue"))}</button>`);
|
|
2331
|
+
else _push(`<!---->`);
|
|
2332
|
+
_push(`<!--]-->`);
|
|
2333
|
+
}
|
|
2334
|
+
_push(`</span></div>`);
|
|
2335
|
+
} else _push(`<!---->`);
|
|
2336
|
+
_push(`<div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) }))}</span></div>`);
|
|
2080
2337
|
if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
|
|
2081
2338
|
else _push(`<!---->`);
|
|
2082
2339
|
_push(ssrRenderComponent(_sfc_main$3, {
|
|
2083
|
-
open: !!
|
|
2084
|
-
title:
|
|
2085
|
-
text:
|
|
2086
|
-
tip:
|
|
2087
|
-
"confirm-label":
|
|
2088
|
-
"cancel-label":
|
|
2340
|
+
open: !!zipDialog.value,
|
|
2341
|
+
title: zipDialog.value?.title ?? "",
|
|
2342
|
+
text: zipDialog.value?.text ?? "",
|
|
2343
|
+
tip: zipDialog.value?.tip,
|
|
2344
|
+
"confirm-label": zipDialog.value?.confirm,
|
|
2345
|
+
"cancel-label": zipDialog.value?.cancel ?? "",
|
|
2089
2346
|
onConfirm: confirmZip,
|
|
2090
2347
|
onClose: ($event) => zipNotice.value = null
|
|
2091
2348
|
}, null, _parent));
|
package/dist/client/style.css
CHANGED
|
@@ -787,11 +787,13 @@
|
|
|
787
787
|
}
|
|
788
788
|
|
|
789
789
|
.fl-toast {
|
|
790
|
-
|
|
791
|
-
|
|
790
|
+
/* the viewport, not the card: a long list would push the card's bottom off screen */
|
|
791
|
+
position: fixed;
|
|
792
|
+
bottom: calc(24px + env(safe-area-inset-bottom));
|
|
792
793
|
left: 50%;
|
|
793
|
-
z-index:
|
|
794
|
-
|
|
794
|
+
z-index: 100;
|
|
795
|
+
box-sizing: border-box;
|
|
796
|
+
max-width: min(560px, calc(100vw - 32px));
|
|
795
797
|
padding: 7px 14px;
|
|
796
798
|
overflow: hidden;
|
|
797
799
|
font-size: 13px;
|
|
@@ -822,7 +824,8 @@
|
|
|
822
824
|
|
|
823
825
|
/* ---------------- long folders ---------------- */
|
|
824
826
|
|
|
825
|
-
.fl-more
|
|
827
|
+
.fl-more,
|
|
828
|
+
.fl-deep {
|
|
826
829
|
background: var(--vp-c-bg-soft);
|
|
827
830
|
}
|
|
828
831
|
|
|
@@ -866,6 +869,37 @@
|
|
|
866
869
|
border-color: transparent;
|
|
867
870
|
}
|
|
868
871
|
|
|
872
|
+
/* ---------------- filter and deep search ---------------- */
|
|
873
|
+
|
|
874
|
+
.fl-hit {
|
|
875
|
+
padding: 0 1px;
|
|
876
|
+
color: inherit;
|
|
877
|
+
background: var(--fl-hit-bg, rgb(250 204 21 / 32%));
|
|
878
|
+
border-radius: 3px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.fl-deep {
|
|
882
|
+
position: relative;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.fl-deep-bar {
|
|
886
|
+
position: absolute;
|
|
887
|
+
bottom: -1px;
|
|
888
|
+
left: 0;
|
|
889
|
+
width: 30%;
|
|
890
|
+
height: 2px;
|
|
891
|
+
background: var(--vp-c-brand-1);
|
|
892
|
+
animation: fl-progress 1.2s ease-in-out infinite;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@media (prefers-reduced-motion: reduce) {
|
|
896
|
+
.fl-deep-bar {
|
|
897
|
+
width: 100%;
|
|
898
|
+
animation: none;
|
|
899
|
+
opacity: 0.6;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
869
903
|
/* ---------------- confirm dialog ---------------- */
|
|
870
904
|
|
|
871
905
|
.fl-confirm {
|
|
@@ -1314,6 +1348,62 @@
|
|
|
1314
1348
|
background: var(--vp-c-default-soft);
|
|
1315
1349
|
}
|
|
1316
1350
|
|
|
1351
|
+
/* Markdown / HTML: source or rendered */
|
|
1352
|
+
.fl-preview-view {
|
|
1353
|
+
display: inline-flex;
|
|
1354
|
+
padding: 2px;
|
|
1355
|
+
margin-left: 6px;
|
|
1356
|
+
background: var(--vp-c-default-soft);
|
|
1357
|
+
border-radius: 8px;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.fl-preview-view-btn {
|
|
1361
|
+
height: 28px;
|
|
1362
|
+
padding: 0 10px;
|
|
1363
|
+
font-size: 12px;
|
|
1364
|
+
color: var(--vp-c-text-2);
|
|
1365
|
+
cursor: pointer;
|
|
1366
|
+
background: transparent;
|
|
1367
|
+
border: 0;
|
|
1368
|
+
border-radius: 6px;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.fl-preview-view-btn[aria-pressed='true'] {
|
|
1372
|
+
font-weight: 600;
|
|
1373
|
+
color: var(--vp-c-text-1);
|
|
1374
|
+
background: var(--vp-c-bg);
|
|
1375
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 12%);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.fl-preview-rendered {
|
|
1379
|
+
display: flex;
|
|
1380
|
+
flex-direction: column;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.fl-preview .fl-md {
|
|
1384
|
+
box-sizing: border-box;
|
|
1385
|
+
max-width: 860px;
|
|
1386
|
+
padding: 16px 24px 32px;
|
|
1387
|
+
margin: 0 auto;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.fl-md-image {
|
|
1391
|
+
display: inline-block;
|
|
1392
|
+
padding: 2px 8px;
|
|
1393
|
+
font-size: 12px;
|
|
1394
|
+
color: var(--vp-c-text-2);
|
|
1395
|
+
border: 1px dashed var(--vp-c-divider);
|
|
1396
|
+
border-radius: 6px;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.fl-preview-html {
|
|
1400
|
+
flex: 1 1 auto;
|
|
1401
|
+
width: 100%;
|
|
1402
|
+
min-height: 0;
|
|
1403
|
+
background: #fff;
|
|
1404
|
+
border: 0;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1317
1407
|
.fl-preview-count {
|
|
1318
1408
|
min-width: 44px;
|
|
1319
1409
|
font-size: 12px;
|
|
@@ -1485,6 +1575,12 @@
|
|
|
1485
1575
|
min-width: 36px;
|
|
1486
1576
|
}
|
|
1487
1577
|
|
|
1578
|
+
.fl-preview-view-btn {
|
|
1579
|
+
height: 36px;
|
|
1580
|
+
padding: 0 14px;
|
|
1581
|
+
font-size: 14px;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1488
1584
|
.fl-preview .fl-preview-head .fl-type {
|
|
1489
1585
|
display: none;
|
|
1490
1586
|
}
|