vitepress-plugin-filelist 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- function compareNodes(a, b, key, ascending = true) {
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.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
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
  /**
@@ -899,6 +916,22 @@ const fileListLocales = {
899
916
  forgetPassword: "忘记",
900
917
  unlockFolder: "输入 {name} 的密码",
901
918
  networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
919
+ deepHint: "只筛选了已加载的内容,",
920
+ deepSearch: "尝试深度搜索",
921
+ deepRunning: "正在深度搜索 · 已查找 {count} 个文件夹",
922
+ deepStop: "停止",
923
+ deepDone: "已搜索全部 {count} 个文件夹",
924
+ deepCapped: "已查找 {count} 个文件夹,还有未搜索的子目录",
925
+ deepContinue: "继续",
926
+ deepSkipped: "跳过 {count} 个加密文件夹",
927
+ deepFailed: "{count} 个文件夹读取失败",
928
+ deepLocked: "未搜索(需要密码)",
929
+ clearFilter: "清除筛选",
930
+ viewMode: "显示方式",
931
+ viewSource: "源码",
932
+ viewRendered: "渲染",
933
+ htmlSandboxNote: "已禁用脚本;网页引用的相对路径资源不会加载",
934
+ imageOmitted: "[图片:{alt}]",
902
935
  batchFailedTitle: "打包失败",
903
936
  batchFailedHint: "可以减少勾选的文件后重试。",
904
937
  downloadStarted: "已交给浏览器下载:{name}",
@@ -979,6 +1012,22 @@ const fileListLocales = {
979
1012
  forgetPassword: "Forget",
980
1013
  unlockFolder: "Enter the password for {name}",
981
1014
  networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
1015
+ deepHint: "Only loaded folders are filtered. ",
1016
+ deepSearch: "Try deep search",
1017
+ deepRunning: "Searching · {count} folders checked",
1018
+ deepStop: "Stop",
1019
+ deepDone: "Searched all {count} folders",
1020
+ deepCapped: "{count} folders checked; some sub-folders remain",
1021
+ deepContinue: "Continue",
1022
+ deepSkipped: "{count} locked folders skipped",
1023
+ deepFailed: "{count} folders could not be read",
1024
+ deepLocked: "Not searched (password needed)",
1025
+ clearFilter: "Clear filter",
1026
+ viewMode: "View",
1027
+ viewSource: "Source",
1028
+ viewRendered: "Rendered",
1029
+ htmlSandboxNote: "Scripts are disabled; resources at relative paths do not load",
1030
+ imageOmitted: "[Image: {alt}]",
982
1031
  batchFailedTitle: "Could not create the ZIP",
983
1032
  batchFailedHint: "Try again with fewer files selected.",
984
1033
  downloadStarted: "Downloading {name} in your browser",
@@ -1007,6 +1056,45 @@ function resolveLabels(locale = "zh-CN", overrides) {
1007
1056
  ...overrides
1008
1057
  };
1009
1058
  }
1059
+ // src/client/markdown.ts
1060
+ /**
1061
+ * Markdown → safe HTML for the preview dialog. Loaded on demand, so pages that
1062
+ * never render Markdown do not download markdown-it or DOMPurify.
1063
+ *
1064
+ * - Raw HTML in the file is allowed, then sanitised (no scripts, handlers, iframes...)
1065
+ * - Only absolute http(s) / mailto links stay clickable, opening in a new tab;
1066
+ * relative links (to other files in the share) become plain text
1067
+ * - Images with a relative source are replaced by their alt text, because the
1068
+ * share's relative paths do not resolve on the documentation site
1069
+ */
1070
+ async function renderMarkdown(source, imageLabel) {
1071
+ const [{ default: MarkdownIt }, { default: DOMPurify }] = await Promise.all([import("markdown-it"), import("dompurify")]);
1072
+ const probe = DOMPurify.isSupported ? DOMPurify.sanitize("<img src=\"x\" onerror=\"1\"><script>1<\/script>") : "<script";
1073
+ if (/<script|onerror/i.test(probe)) throw new Error("HTML sanitiser unavailable");
1074
+ const html = new MarkdownIt({
1075
+ html: true,
1076
+ linkify: true
1077
+ }).render(source);
1078
+ const template = document.createElement("template");
1079
+ template.innerHTML = DOMPurify.sanitize(html);
1080
+ const fragment = template.content;
1081
+ for (const link of fragment.querySelectorAll("a")) {
1082
+ const href = link.getAttribute("href") ?? "";
1083
+ if (/^(https?:|mailto:)/i.test(href)) {
1084
+ link.setAttribute("target", "_blank");
1085
+ link.setAttribute("rel", "noopener noreferrer");
1086
+ } else link.replaceWith(...link.childNodes);
1087
+ }
1088
+ for (const image of fragment.querySelectorAll("img")) {
1089
+ const src = image.getAttribute("src") ?? "";
1090
+ if (/^(https?:|data:image\/)/i.test(src)) continue;
1091
+ const placeholder = document.createElement("span");
1092
+ placeholder.className = "fl-md-image";
1093
+ placeholder.textContent = imageLabel(image.getAttribute("alt") || src.split("/").pop() || "");
1094
+ image.replaceWith(placeholder);
1095
+ }
1096
+ return template.innerHTML;
1097
+ }
1010
1098
  // src/client/PreviewDialog.vue
1011
1099
  const _hoisted_1$1 = ["aria-label"];
1012
1100
  const _hoisted_2$1 = {
@@ -1031,33 +1119,40 @@ const _hoisted_13$1 = [
1031
1119
  "aria-label",
1032
1120
  "title"
1033
1121
  ];
1034
- const _hoisted_14$1 = { class: "fl-preview-tools fl-preview-actions" };
1035
- const _hoisted_15$1 = ["aria-label", "title"];
1036
- const _hoisted_16$1 = ["aria-label", "title"];
1037
- const _hoisted_17$1 = ["data-mode"];
1038
- const _hoisted_18$1 = {
1122
+ const _hoisted_14$1 = ["aria-label"];
1123
+ const _hoisted_15$1 = ["aria-pressed", "onClick"];
1124
+ const _hoisted_16$1 = { class: "fl-preview-tools fl-preview-actions" };
1125
+ const _hoisted_17$1 = ["aria-label", "title"];
1126
+ const _hoisted_18$1 = ["aria-label", "title"];
1127
+ const _hoisted_19$1 = ["data-mode"];
1128
+ const _hoisted_20$1 = {
1039
1129
  key: 0,
1040
1130
  class: "fl-preview-status",
1041
1131
  role: "status"
1042
1132
  };
1043
- const _hoisted_19$1 = ["src", "alt"];
1044
- const _hoisted_20$1 = ["src"];
1045
- const _hoisted_21$1 = ["src"];
1046
- const _hoisted_22$1 = {
1047
- key: 4,
1048
- class: "fl-preview-text"
1049
- };
1050
- const _hoisted_23$1 = {
1133
+ const _hoisted_21$1 = ["src", "alt"];
1134
+ const _hoisted_22$1 = ["src"];
1135
+ const _hoisted_23$1 = ["src"];
1136
+ const _hoisted_24$1 = {
1051
1137
  key: 0,
1052
1138
  class: "fl-preview-note"
1053
1139
  };
1054
- const _hoisted_24$1 = ["src", "title"];
1055
- const _hoisted_25$1 = {
1140
+ const _hoisted_25$1 = ["innerHTML"];
1141
+ const _hoisted_26$1 = {
1142
+ key: 1,
1143
+ class: "fl-preview-status",
1144
+ role: "status"
1145
+ };
1146
+ const _hoisted_27$1 = { class: "fl-preview-note" };
1147
+ const _hoisted_28$1 = ["srcdoc", "title"];
1148
+ const _hoisted_29$1 = { key: 3 };
1149
+ const _hoisted_30$1 = ["src", "title"];
1150
+ const _hoisted_31$1 = {
1056
1151
  key: 6,
1057
1152
  class: "fl-preview-info"
1058
1153
  };
1059
- const _hoisted_26$1 = ["data-kind"];
1060
- const _hoisted_27$1 = { class: "fl-preview-message" };
1154
+ const _hoisted_32$1 = ["data-kind"];
1155
+ const _hoisted_33$1 = { class: "fl-preview-message" };
1061
1156
  const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1062
1157
  __name: "PreviewDialog",
1063
1158
  props: {
@@ -1090,7 +1185,9 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1090
1185
  /**
1091
1186
  * File preview in a native <dialog>, without leaving the page.
1092
1187
  *
1093
- * image / video / audio: native elements; text: the first `maxTextBytes`
1188
+ * image / video / audio: native elements; text: the first `maxTextBytes`,
1189
+ * with Markdown and HTML switchable between source (default) and rendered
1190
+ * (Markdown sanitised in VitePress' doc style, HTML in a script-less sandbox);
1094
1191
  * through a Range request; pdf: the first matching OpenList `iframe_previews`
1095
1192
  * viewer, else the browser's reader through a Blob. Anything else (Office
1096
1193
  * included), failures, oversized PDFs and demo mode: file info + download.
@@ -1109,8 +1206,18 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1109
1206
  const blobUrl = ref("");
1110
1207
  const rotation = ref(0);
1111
1208
  const actualSize = ref(false);
1209
+ /** Markdown / HTML: show the source or render it. Kept while the page is open. */
1210
+ const view = ref("source");
1211
+ const renderedHtml = ref("");
1112
1212
  let controller;
1113
1213
  let loadSeq = 0;
1214
+ /** Text files that can also be rendered. */
1215
+ const renderAs = computed(() => {
1216
+ const name = props.file?.name ?? "";
1217
+ if (/\.(md|markdown)$/i.test(name)) return "markdown";
1218
+ if (/\.html?$/i.test(name)) return "html";
1219
+ return null;
1220
+ });
1114
1221
  const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
1115
1222
  const mode = computed(() => {
1116
1223
  if (props.demo) return "demo";
@@ -1120,6 +1227,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1120
1227
  return kind.value;
1121
1228
  });
1122
1229
  const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
1230
+ const canRender = computed(() => !!renderAs.value && mode.value === "text" && !busy.value);
1123
1231
  const infoMessage = computed(() => {
1124
1232
  const file = props.file;
1125
1233
  if (!file) return "";
@@ -1140,6 +1248,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1140
1248
  loadingBody.value = false;
1141
1249
  bodyFailed.value = false;
1142
1250
  textContent.value = "";
1251
+ renderedHtml.value = "";
1143
1252
  truncated.value = false;
1144
1253
  rotation.value = 0;
1145
1254
  actualSize.value = false;
@@ -1221,6 +1330,20 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1221
1330
  props.error,
1222
1331
  props.demo
1223
1332
  ], () => void loadBody(), { immediate: true });
1333
+ watch(() => [
1334
+ view.value,
1335
+ renderAs.value,
1336
+ textContent.value
1337
+ ], async ([current, as, text]) => {
1338
+ if (current !== "rendered" || as !== "markdown" || !text) return;
1339
+ const seq = loadSeq;
1340
+ try {
1341
+ const html = await renderMarkdown(text, (alt) => t("imageOmitted", { alt }));
1342
+ if (seq === loadSeq) renderedHtml.value = html;
1343
+ } catch {
1344
+ if (seq === loadSeq) view.value = "source";
1345
+ }
1346
+ });
1224
1347
  watch(() => props.open, (open) => {
1225
1348
  const el = dialog.value;
1226
1349
  if (!el) return;
@@ -1270,49 +1393,66 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1270
1393
  "aria-hidden": "true"
1271
1394
  }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_4$1),
1272
1395
  createElementVNode("div", _hoisted_5$1, [createElementVNode("span", _hoisted_6$1, toDisplayString(__props.file.name), 1), createElementVNode("span", _hoisted_7$1, toDisplayString(unref(formatSize)(__props.file.size)) + " · " + toDisplayString(unref(formatDate)(__props.file.modified)), 1)]),
1273
- __props.total > 1 || mode.value === "image" && !busy.value ? (openBlock(), createElementBlock("div", _hoisted_8$1, [__props.total > 1 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
1274
- createElementVNode("button", {
1275
- class: "fl-preview-tool",
1396
+ __props.total > 1 || mode.value === "image" && !busy.value || canRender.value ? (openBlock(), createElementBlock("div", _hoisted_8$1, [
1397
+ __props.total > 1 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
1398
+ createElementVNode("button", {
1399
+ class: "fl-preview-tool",
1400
+ type: "button",
1401
+ "aria-label": t("previous"),
1402
+ title: t("previous"),
1403
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("prev"))
1404
+ }, [createVNode(_sfc_main$2, {
1405
+ name: "previous",
1406
+ size: 18
1407
+ })], 8, _hoisted_9$1),
1408
+ createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
1409
+ createElementVNode("button", {
1410
+ class: "fl-preview-tool",
1411
+ type: "button",
1412
+ "aria-label": t("next"),
1413
+ title: t("next"),
1414
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("next"))
1415
+ }, [createVNode(_sfc_main$2, {
1416
+ name: "next",
1417
+ size: 18
1418
+ })], 8, _hoisted_11$1)
1419
+ ], 64)) : createCommentVNode("v-if", true),
1420
+ mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
1421
+ class: "fl-preview-tool fl-preview-rotate",
1276
1422
  type: "button",
1277
- "aria-label": t("previous"),
1278
- title: t("previous"),
1279
- onClick: _cache[0] || (_cache[0] = ($event) => emit("prev"))
1423
+ "aria-label": t("rotate"),
1424
+ title: t("rotate"),
1425
+ onClick: _cache[2] || (_cache[2] = ($event) => rotation.value = (rotation.value + 90) % 360)
1280
1426
  }, [createVNode(_sfc_main$2, {
1281
- name: "previous",
1282
- size: 18
1283
- })], 8, _hoisted_9$1),
1284
- createElementVNode("span", _hoisted_10$1, toDisplayString(__props.position) + " / " + toDisplayString(__props.total), 1),
1285
- createElementVNode("button", {
1286
- class: "fl-preview-tool",
1427
+ name: "refresh",
1428
+ size: 17
1429
+ })], 8, _hoisted_12$1), createElementVNode("button", {
1430
+ class: "fl-preview-tool fl-preview-zoom",
1287
1431
  type: "button",
1288
- "aria-label": t("next"),
1289
- title: t("next"),
1290
- onClick: _cache[1] || (_cache[1] = ($event) => emit("next"))
1432
+ "aria-pressed": actualSize.value,
1433
+ "aria-label": t("actualSize"),
1434
+ title: t("actualSize"),
1435
+ onClick: _cache[3] || (_cache[3] = ($event) => actualSize.value = !actualSize.value)
1291
1436
  }, [createVNode(_sfc_main$2, {
1292
- name: "next",
1293
- size: 18
1294
- })], 8, _hoisted_11$1)
1295
- ], 64)) : createCommentVNode("v-if", true), mode.value === "image" && !busy.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
1296
- class: "fl-preview-tool fl-preview-rotate",
1297
- type: "button",
1298
- "aria-label": t("rotate"),
1299
- title: t("rotate"),
1300
- onClick: _cache[2] || (_cache[2] = ($event) => rotation.value = (rotation.value + 90) % 360)
1301
- }, [createVNode(_sfc_main$2, {
1302
- name: "refresh",
1303
- size: 17
1304
- })], 8, _hoisted_12$1), createElementVNode("button", {
1305
- class: "fl-preview-tool fl-preview-zoom",
1306
- type: "button",
1307
- "aria-pressed": actualSize.value,
1308
- "aria-label": t("actualSize"),
1309
- title: t("actualSize"),
1310
- onClick: _cache[3] || (_cache[3] = ($event) => actualSize.value = !actualSize.value)
1311
- }, [createVNode(_sfc_main$2, {
1312
- name: actualSize.value ? "zoomOut" : "zoomIn",
1313
- size: 17
1314
- }, null, 8, ["name"])], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
1315
- createElementVNode("div", _hoisted_14$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
1437
+ name: actualSize.value ? "zoomOut" : "zoomIn",
1438
+ size: 17
1439
+ }, null, 8, ["name"])], 8, _hoisted_13$1)], 64)) : createCommentVNode("v-if", true),
1440
+ canRender.value ? (openBlock(), createElementBlock("span", {
1441
+ key: 2,
1442
+ class: "fl-preview-view",
1443
+ role: "group",
1444
+ "aria-label": t("viewMode")
1445
+ }, [(openBlock(), createElementBlock(Fragment, null, renderList(["source", "rendered"], (option) => {
1446
+ return createElementVNode("button", {
1447
+ key: option,
1448
+ class: "fl-preview-view-btn",
1449
+ type: "button",
1450
+ "aria-pressed": view.value === option,
1451
+ onClick: ($event) => view.value = option
1452
+ }, toDisplayString(t(option === "source" ? "viewSource" : "viewRendered")), 9, _hoisted_15$1);
1453
+ }), 64))], 8, _hoisted_14$1)) : createCommentVNode("v-if", true)
1454
+ ])) : createCommentVNode("v-if", true),
1455
+ createElementVNode("div", _hoisted_16$1, [!__props.demo ? (openBlock(), createElementBlock("button", {
1316
1456
  key: 0,
1317
1457
  class: "fl-preview-tool fl-preview-download",
1318
1458
  type: "button",
@@ -1322,7 +1462,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1322
1462
  }, [createVNode(_sfc_main$2, {
1323
1463
  name: "download",
1324
1464
  size: 17
1325
- })], 8, _hoisted_15$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
1465
+ })], 8, _hoisted_17$1)) : createCommentVNode("v-if", true), createElementVNode("button", {
1326
1466
  class: "fl-preview-tool fl-preview-close",
1327
1467
  type: "button",
1328
1468
  autofocus: "",
@@ -1332,18 +1472,18 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1332
1472
  }, [createVNode(_sfc_main$2, {
1333
1473
  name: "close",
1334
1474
  size: 18
1335
- })], 8, _hoisted_16$1)])
1475
+ })], 8, _hoisted_18$1)])
1336
1476
  ]), createElementVNode("div", {
1337
1477
  class: "fl-preview-body",
1338
1478
  "data-mode": busy.value ? "loading" : mode.value
1339
- }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_18$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
1479
+ }, [busy.value ? (openBlock(), createElementBlock("div", _hoisted_20$1, toDisplayString(t("loading")), 1)) : mode.value === "image" ? (openBlock(), createElementBlock("img", {
1340
1480
  key: 1,
1341
1481
  class: normalizeClass(["fl-preview-image", { "fl-preview-actual": actualSize.value }]),
1342
1482
  src: __props.src,
1343
1483
  alt: __props.file.name,
1344
1484
  style: normalizeStyle({ transform: rotation.value ? `rotate(${rotation.value}deg)` : void 0 }),
1345
1485
  onError: markFailed
1346
- }, null, 46, _hoisted_19$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
1486
+ }, null, 46, _hoisted_21$1)) : mode.value === "video" ? (openBlock(), createElementBlock("video", {
1347
1487
  key: 2,
1348
1488
  class: "fl-preview-video",
1349
1489
  src: __props.src,
@@ -1351,25 +1491,42 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1351
1491
  playsinline: "",
1352
1492
  preload: "metadata",
1353
1493
  onError: markFailed
1354
- }, null, 40, _hoisted_20$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
1494
+ }, null, 40, _hoisted_22$1)) : mode.value === "audio" ? (openBlock(), createElementBlock("audio", {
1355
1495
  key: 3,
1356
1496
  class: "fl-preview-audio",
1357
1497
  src: __props.src,
1358
1498
  controls: "",
1359
1499
  preload: "metadata",
1360
1500
  onError: markFailed
1361
- }, 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", {
1501
+ }, null, 40, _hoisted_23$1)) : mode.value === "text" ? (openBlock(), createElementBlock("div", {
1502
+ key: 4,
1503
+ class: normalizeClass(["fl-preview-text", { "fl-preview-rendered": renderAs.value && view.value === "rendered" }])
1504
+ }, [truncated.value ? (openBlock(), createElementBlock("p", _hoisted_24$1, toDisplayString(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) })), 1)) : createCommentVNode("v-if", true), renderAs.value === "markdown" && view.value === "rendered" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" sanitised by renderMarkdown (DOMPurify) "), renderedHtml.value ? (openBlock(), createElementBlock("div", {
1505
+ key: 0,
1506
+ class: "vp-doc fl-md",
1507
+ innerHTML: renderedHtml.value
1508
+ }, null, 8, _hoisted_25$1)) : (openBlock(), createElementBlock("div", _hoisted_26$1, toDisplayString(t("loading")), 1))], 64)) : renderAs.value === "html" && view.value === "rendered" ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
1509
+ createElementVNode("p", _hoisted_27$1, toDisplayString(t("htmlSandboxNote")), 1),
1510
+ createCommentVNode(" empty sandbox: no scripts, no forms, no popups, an opaque origin "),
1511
+ createElementVNode("iframe", {
1512
+ class: "fl-preview-html",
1513
+ sandbox: "",
1514
+ referrerpolicy: "no-referrer",
1515
+ srcdoc: textContent.value,
1516
+ title: __props.file.name
1517
+ }, null, 8, _hoisted_28$1)
1518
+ ], 64)) : (openBlock(), createElementBlock("pre", _hoisted_29$1, toDisplayString(textContent.value), 1))], 2)) : mode.value === "pdf" ? (openBlock(), createElementBlock("iframe", {
1362
1519
  key: 5,
1363
1520
  class: "fl-preview-frame",
1364
1521
  src: __props.viewer || blobUrl.value,
1365
1522
  title: __props.file.name
1366
- }, null, 8, _hoisted_24$1)) : (openBlock(), createElementBlock("div", _hoisted_25$1, [
1523
+ }, null, 8, _hoisted_30$1)) : (openBlock(), createElementBlock("div", _hoisted_31$1, [
1367
1524
  createElementVNode("span", {
1368
1525
  class: "fl-type fl-preview-badge",
1369
1526
  "data-kind": unref(fileKind)(__props.file.name),
1370
1527
  "aria-hidden": "true"
1371
- }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_26$1),
1372
- createElementVNode("p", _hoisted_27$1, toDisplayString(infoMessage.value), 1),
1528
+ }, toDisplayString(unref(extOf)(__props.file.name) || "FILE"), 9, _hoisted_32$1),
1529
+ createElementVNode("p", _hoisted_33$1, toDisplayString(infoMessage.value), 1),
1373
1530
  !__props.demo ? (openBlock(), createElementBlock("button", {
1374
1531
  key: 0,
1375
1532
  class: "fl-btn fl-btn-primary",
@@ -1379,7 +1536,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
1379
1536
  name: "download",
1380
1537
  size: 15
1381
1538
  }), createTextVNode(" " + toDisplayString(t("download")), 1)])) : createCommentVNode("v-if", true)
1382
- ]))], 8, _hoisted_17$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
1539
+ ]))], 8, _hoisted_19$1)])) : createCommentVNode("v-if", true)], 40, _hoisted_1$1);
1383
1540
  };
1384
1541
  }
1385
1542
  });
@@ -1406,14 +1563,22 @@ function toNodes(items, parent, password) {
1406
1563
  zipPath: [...parent.zipPath, item.name]
1407
1564
  }));
1408
1565
  }
1409
- function sortTree(nodes, key, ascending) {
1410
- nodes.sort((a, b) => compareNodes(a, b, key, ascending));
1411
- for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
1566
+ function sortTree(nodes, key, ascending, locale) {
1567
+ nodes.sort((a, b) => compareNodes(a, b, key, ascending, locale));
1568
+ for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending, locale);
1412
1569
  }
1413
1570
  /** Loaded files at or under a node. */
1414
1571
  function descendantFiles(node) {
1415
1572
  return node.is_dir ? node.children.flatMap(descendantFiles) : [node];
1416
1573
  }
1574
+ /** Every loaded folder (expanded or not), e.g. what a deep search has read. */
1575
+ function loadedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1576
+ for (const node of nodes) if (node.is_dir && node.loaded) {
1577
+ out.add(node.path);
1578
+ loadedPathsOf(node.children, out);
1579
+ }
1580
+ return out;
1581
+ }
1417
1582
  function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1418
1583
  for (const node of nodes) if (node.is_dir && node.expanded) {
1419
1584
  out.add(node.path);
@@ -1616,104 +1781,105 @@ const _hoisted_13 = { class: "fl-clear-text" };
1616
1781
  const _hoisted_14 = { class: "fl-tools" };
1617
1782
  const _hoisted_15 = { class: "fl-search" };
1618
1783
  const _hoisted_16 = ["placeholder", "aria-label"];
1619
- const _hoisted_17 = [
1784
+ const _hoisted_17 = ["aria-label", "title"];
1785
+ const _hoisted_18 = [
1620
1786
  "disabled",
1621
1787
  "aria-label",
1622
1788
  "title"
1623
1789
  ];
1624
- const _hoisted_18 = { class: "fl-row fl-head" };
1625
- const _hoisted_19 = {
1790
+ const _hoisted_19 = { class: "fl-row fl-head" };
1791
+ const _hoisted_20 = {
1626
1792
  key: 0,
1627
1793
  class: "fl-progress",
1628
1794
  "aria-hidden": "true"
1629
1795
  };
1630
- const _hoisted_20 = { class: "fl-cell fl-cell-check" };
1631
- const _hoisted_21 = [
1796
+ const _hoisted_21 = { class: "fl-cell fl-cell-check" };
1797
+ const _hoisted_22 = [
1632
1798
  "aria-label",
1633
1799
  "disabled",
1634
1800
  "checked",
1635
1801
  ".indeterminate"
1636
1802
  ];
1637
- const _hoisted_22 = ["aria-sort", "onClick"];
1638
- const _hoisted_23 = {
1803
+ const _hoisted_23 = ["aria-sort", "onClick"];
1804
+ const _hoisted_24 = {
1639
1805
  class: "fl-sort",
1640
1806
  "aria-hidden": "true"
1641
1807
  };
1642
- const _hoisted_24 = { class: "fl-cell fl-cell-op" };
1643
- const _hoisted_25 = { class: "fl-sort-select" };
1644
- const _hoisted_26 = {
1808
+ const _hoisted_25 = { class: "fl-cell fl-cell-op" };
1809
+ const _hoisted_26 = { class: "fl-sort-select" };
1810
+ const _hoisted_27 = {
1645
1811
  class: "fl-sort-current",
1646
1812
  "aria-hidden": "true"
1647
1813
  };
1648
- const _hoisted_27 = ["aria-label"];
1649
- const _hoisted_28 = ["value"];
1650
- const _hoisted_29 = {
1814
+ const _hoisted_28 = ["aria-label"];
1815
+ const _hoisted_29 = ["value"];
1816
+ const _hoisted_30 = {
1651
1817
  key: 0,
1652
1818
  class: "fl-status"
1653
1819
  };
1654
- const _hoisted_30 = ["for"];
1655
- const _hoisted_31 = ["id"];
1656
- const _hoisted_32 = { class: "fl-unlock" };
1657
- const _hoisted_33 = [
1820
+ const _hoisted_31 = ["for"];
1821
+ const _hoisted_32 = ["id"];
1822
+ const _hoisted_33 = { class: "fl-unlock" };
1823
+ const _hoisted_34 = [
1658
1824
  "id",
1659
1825
  "data-unlock",
1660
1826
  "placeholder",
1661
1827
  "aria-invalid",
1662
1828
  "aria-describedby"
1663
1829
  ];
1664
- const _hoisted_34 = [
1830
+ const _hoisted_35 = [
1665
1831
  "aria-pressed",
1666
1832
  "aria-label",
1667
1833
  "title"
1668
1834
  ];
1669
- const _hoisted_35 = ["disabled"];
1670
- const _hoisted_36 = {
1835
+ const _hoisted_36 = ["disabled"];
1836
+ const _hoisted_37 = {
1671
1837
  key: 2,
1672
1838
  class: "fl-status fl-error"
1673
1839
  };
1674
- const _hoisted_37 = {
1840
+ const _hoisted_38 = {
1675
1841
  key: 0,
1676
1842
  class: "fl-hint"
1677
1843
  };
1678
- const _hoisted_38 = {
1844
+ const _hoisted_39 = {
1679
1845
  key: 1,
1680
1846
  class: "fl-hint"
1681
1847
  };
1682
- const _hoisted_39 = {
1848
+ const _hoisted_40 = {
1683
1849
  key: 3,
1684
1850
  class: "fl-status"
1685
1851
  };
1686
- const _hoisted_40 = { class: "fl-cell fl-more-cell" };
1687
- const _hoisted_41 = { class: "fl-more-count" };
1688
- const _hoisted_42 = ["onClick"];
1689
- const _hoisted_43 = { class: "fl-more-page" };
1690
- const _hoisted_44 = ["onClick"];
1852
+ const _hoisted_41 = { class: "fl-cell fl-more-cell" };
1853
+ const _hoisted_42 = { class: "fl-more-count" };
1854
+ const _hoisted_43 = ["onClick"];
1855
+ const _hoisted_44 = { class: "fl-more-page" };
1691
1856
  const _hoisted_45 = ["onClick"];
1692
- const _hoisted_46 = { class: "fl-cell fl-cell-check" };
1693
- const _hoisted_47 = [
1857
+ const _hoisted_46 = ["onClick"];
1858
+ const _hoisted_47 = { class: "fl-cell fl-cell-check" };
1859
+ const _hoisted_48 = [
1694
1860
  "disabled",
1695
1861
  "aria-label",
1696
1862
  "checked",
1697
1863
  ".indeterminate",
1698
1864
  "onChange"
1699
1865
  ];
1700
- const _hoisted_48 = ["title"];
1701
- const _hoisted_49 = [
1866
+ const _hoisted_49 = ["title"];
1867
+ const _hoisted_50 = [
1702
1868
  "aria-label",
1703
1869
  "title",
1704
1870
  "onClick"
1705
1871
  ];
1706
- const _hoisted_50 = [
1872
+ const _hoisted_51 = [
1707
1873
  "aria-label",
1708
1874
  "aria-expanded",
1709
1875
  "onClick"
1710
1876
  ];
1711
- const _hoisted_51 = {
1877
+ const _hoisted_52 = {
1712
1878
  key: 2,
1713
1879
  class: "fl-arrow-placeholder",
1714
1880
  "aria-hidden": "true"
1715
1881
  };
1716
- const _hoisted_52 = {
1882
+ const _hoisted_53 = {
1717
1883
  key: 3,
1718
1884
  class: "fl-folder-icon",
1719
1885
  viewBox: "0 0 24 24",
@@ -1721,27 +1887,39 @@ const _hoisted_52 = {
1721
1887
  height: "18",
1722
1888
  "aria-hidden": "true"
1723
1889
  };
1724
- const _hoisted_53 = ["data-kind"];
1725
- const _hoisted_54 = { class: "fl-label" };
1726
- const _hoisted_55 = ["onClick"];
1727
- const _hoisted_56 = { class: "fl-meta" };
1890
+ const _hoisted_54 = ["data-kind"];
1891
+ const _hoisted_55 = { class: "fl-label" };
1892
+ const _hoisted_56 = ["onClick"];
1728
1893
  const _hoisted_57 = {
1894
+ key: 0,
1895
+ class: "fl-hit"
1896
+ };
1897
+ const _hoisted_58 = { class: "fl-meta" };
1898
+ const _hoisted_59 = {
1899
+ key: 2,
1900
+ class: "fl-locked-note"
1901
+ };
1902
+ const _hoisted_60 = {
1729
1903
  key: 5,
1904
+ class: "fl-locked-note fl-locked-hint"
1905
+ };
1906
+ const _hoisted_61 = {
1907
+ key: 6,
1730
1908
  class: "fl-count"
1731
1909
  };
1732
- const _hoisted_58 = ["id"];
1733
- const _hoisted_59 = [
1910
+ const _hoisted_62 = ["id"];
1911
+ const _hoisted_63 = [
1734
1912
  "aria-label",
1735
1913
  "title",
1736
1914
  "onClick"
1737
1915
  ];
1738
- const _hoisted_60 = {
1739
- key: 8,
1916
+ const _hoisted_64 = {
1917
+ key: 9,
1740
1918
  class: "fl-loading",
1741
1919
  "aria-hidden": "true"
1742
1920
  };
1743
- const _hoisted_61 = ["onSubmit", "onKeydown"];
1744
- const _hoisted_62 = [
1921
+ const _hoisted_65 = ["onSubmit", "onKeydown"];
1922
+ const _hoisted_66 = [
1745
1923
  "onUpdate:modelValue",
1746
1924
  "data-unlock",
1747
1925
  "placeholder",
@@ -1749,40 +1927,51 @@ const _hoisted_62 = [
1749
1927
  "aria-invalid",
1750
1928
  "aria-describedby"
1751
1929
  ];
1752
- const _hoisted_63 = [
1930
+ const _hoisted_67 = [
1753
1931
  "aria-pressed",
1754
1932
  "aria-label",
1755
1933
  "title",
1756
1934
  "onClick"
1757
1935
  ];
1758
- const _hoisted_64 = ["disabled"];
1759
- const _hoisted_65 = [
1936
+ const _hoisted_68 = ["disabled"];
1937
+ const _hoisted_69 = [
1760
1938
  "aria-label",
1761
1939
  "title",
1762
1940
  "onClick"
1763
1941
  ];
1764
- const _hoisted_66 = { class: "fl-cell fl-cell-size" };
1765
- const _hoisted_67 = { class: "fl-cell fl-cell-date" };
1766
- const _hoisted_68 = { class: "fl-cell fl-cell-op" };
1767
- const _hoisted_69 = ["aria-label", "onClick"];
1768
- const _hoisted_70 = [
1942
+ const _hoisted_70 = { class: "fl-cell fl-cell-size" };
1943
+ const _hoisted_71 = { class: "fl-cell fl-cell-date" };
1944
+ const _hoisted_72 = { class: "fl-cell fl-cell-op" };
1945
+ const _hoisted_73 = ["aria-label", "onClick"];
1946
+ const _hoisted_74 = [
1769
1947
  "aria-label",
1770
1948
  "aria-expanded",
1771
1949
  "onClick"
1772
1950
  ];
1773
- const _hoisted_71 = [
1951
+ const _hoisted_75 = [
1774
1952
  "aria-label",
1775
1953
  "title",
1776
1954
  "onClick"
1777
1955
  ];
1778
- const _hoisted_72 = [
1956
+ const _hoisted_76 = [
1779
1957
  "aria-label",
1780
1958
  "title",
1781
1959
  "onClick"
1782
1960
  ];
1783
- const _hoisted_73 = { class: "fl-foot" };
1784
- const _hoisted_74 = {
1961
+ const _hoisted_77 = {
1785
1962
  key: 4,
1963
+ class: "fl-deep",
1964
+ role: "status"
1965
+ };
1966
+ const _hoisted_78 = { class: "fl-deep-text" };
1967
+ const _hoisted_79 = {
1968
+ key: 0,
1969
+ class: "fl-deep-bar",
1970
+ "aria-hidden": "true"
1971
+ };
1972
+ const _hoisted_80 = { class: "fl-foot" };
1973
+ const _hoisted_81 = {
1974
+ key: 5,
1786
1975
  class: "fl-toast",
1787
1976
  role: "status"
1788
1977
  };
@@ -1806,6 +1995,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1806
1995
  default: true
1807
1996
  },
1808
1997
  pageSize: { default: 200 },
1998
+ deepSearchLimit: { default: 500 },
1999
+ deepSearch: { default: "manual" },
1809
2000
  zipWarnSize: {},
1810
2001
  zipMaxSize: {},
1811
2002
  locale: { default: "zh-CN" },
@@ -1870,7 +2061,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1870
2061
  return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
1871
2062
  }
1872
2063
  function sortNodes(nodes) {
1873
- sortTree(nodes, sortKey.value, sortAsc.value);
2064
+ sortTree(nodes, sortKey.value, sortAsc.value, props.locale);
1874
2065
  }
1875
2066
  function focusUnlock(path) {
1876
2067
  nextTick(() => {
@@ -1975,7 +2166,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1975
2166
  * Load a folder's children. A password error locks the folder instead of
1976
2167
  * toasting; `prompt` also shows its same-row input (and focuses it).
1977
2168
  */
1978
- async function loadChildren(node, prompt = false) {
2169
+ async function loadChildren(node, prompt = false, quiet = false) {
1979
2170
  if (node.loaded) return true;
1980
2171
  if (node.lock) {
1981
2172
  if (prompt) startPrompt(node, prompt === "focus");
@@ -1991,7 +2182,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1991
2182
  node.expanded = false;
1992
2183
  node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
1993
2184
  if (prompt) startPrompt(node, prompt === "focus");
1994
- } else showToast(failureMessage("folderFailed", error));
2185
+ } else if (!quiet) showToast(failureMessage("folderFailed", error));
1995
2186
  return false;
1996
2187
  } finally {
1997
2188
  node.loading = false;
@@ -2038,6 +2229,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2038
2229
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
2039
2230
  };
2040
2231
  }
2232
+ /** Run at most `size` tasks at once. */
2233
+ function concurrency(size) {
2234
+ let active = 0;
2235
+ const waiting = [];
2236
+ return async (task) => {
2237
+ if (active >= size) await new Promise((resolve) => waiting.push(resolve));
2238
+ active += 1;
2239
+ try {
2240
+ return await task();
2241
+ } finally {
2242
+ active -= 1;
2243
+ waiting.shift()?.();
2244
+ }
2245
+ };
2246
+ }
2041
2247
  class RootLockedError extends Error {
2042
2248
  rememberedAt;
2043
2249
  constructor(rememberedAt) {
@@ -2049,7 +2255,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2049
2255
  * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
2050
2256
  * what was open); without it the configured `expanded` defaults apply.
2051
2257
  */
2052
- async function buildTree(expandedPaths) {
2258
+ async function buildTree(expandedPaths, loadedPaths) {
2053
2259
  let nodes;
2054
2260
  if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
2055
2261
  else {
@@ -2060,13 +2266,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2060
2266
  throw isPasswordError(error) ? new RootLockedError(rememberedAt) : error;
2061
2267
  }
2062
2268
  }
2269
+ const limit = concurrency(4);
2063
2270
  const open = async (list) => {
2064
- for (const node of list) {
2065
- const wanted = node.expanded || expandedPaths?.has(node.path);
2066
- if (!node.is_dir || node.lock || !wanted) continue;
2067
- node.expanded = await loadChildren(node, "show");
2068
- if (node.expanded && expandedPaths) await open(node.children);
2069
- }
2271
+ await Promise.all(list.map(async (node) => {
2272
+ const wanted = !!(node.expanded || expandedPaths?.has(node.path));
2273
+ const keep = !!loadedPaths?.has(node.path);
2274
+ if (!node.is_dir || node.lock || !(wanted || keep)) return;
2275
+ const ok = await limit(() => loadChildren(node, wanted ? "show" : false, !wanted));
2276
+ node.expanded = wanted && ok;
2277
+ if (ok && (expandedPaths || loadedPaths)) await open(node.children);
2278
+ }));
2070
2279
  };
2071
2280
  await open(nodes);
2072
2281
  return nodes;
@@ -2078,6 +2287,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2078
2287
  }
2079
2288
  /** First load, or a retry after an error: shows the loading state. */
2080
2289
  async function loadRoot() {
2290
+ stopDeepSearch();
2081
2291
  refreshSeq += 1;
2082
2292
  refreshing.value = false;
2083
2293
  loadingRoot.value = true;
@@ -2101,6 +2311,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2101
2311
  */
2102
2312
  async function refresh() {
2103
2313
  if (refreshing.value || loadingRoot.value) return;
2314
+ stopDeepSearch();
2104
2315
  if (!roots.value.length || errorMsg.value || rootLock.value) {
2105
2316
  await loadRoot();
2106
2317
  return;
@@ -2108,7 +2319,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2108
2319
  const seq = ++refreshSeq;
2109
2320
  refreshing.value = true;
2110
2321
  try {
2111
- const fresh = await buildTree(expandedPathsOf(roots.value));
2322
+ const fresh = await buildTree(expandedPathsOf(roots.value), deepMode.value ? loadedPathsOf(roots.value) : void 0);
2112
2323
  if (seq !== refreshSeq) return;
2113
2324
  roots.value = fresh;
2114
2325
  const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
@@ -2181,7 +2392,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2181
2392
  const rows = computed(() => {
2182
2393
  const out = [];
2183
2394
  const query = keyword.value.trim().toLowerCase();
2184
- const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && node.children.some(matches);
2395
+ const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && (node.children.some(matches) || deepMode.value && !!node.lock);
2185
2396
  const walk = (nodes, key, depth) => {
2186
2397
  const list = nodes.filter(matches);
2187
2398
  const limit = query ? list.length : Math.min(list.length, limitOf(key));
@@ -2200,6 +2411,172 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2200
2411
  return out;
2201
2412
  });
2202
2413
  const visible = computed(() => rows.value.filter((row) => !isMore(row)));
2414
+ /** A name split around the filter keyword, for highlighting. */
2415
+ function nameParts(name) {
2416
+ const query = keyword.value.trim().toLowerCase();
2417
+ if (!query) return [{
2418
+ text: name,
2419
+ hit: false
2420
+ }];
2421
+ const parts = [];
2422
+ const lower = name.toLowerCase();
2423
+ let from = 0;
2424
+ for (let at = lower.indexOf(query); at !== -1; at = lower.indexOf(query, from)) {
2425
+ if (at > from) parts.push({
2426
+ text: name.slice(from, at),
2427
+ hit: false
2428
+ });
2429
+ parts.push({
2430
+ text: name.slice(at, at + query.length),
2431
+ hit: true
2432
+ });
2433
+ from = at + query.length;
2434
+ }
2435
+ if (from < name.length) parts.push({
2436
+ text: name.slice(from),
2437
+ hit: false
2438
+ });
2439
+ return parts;
2440
+ }
2441
+ /**
2442
+ * The filter only sees loaded folders. Deep search loads the rest under the
2443
+ * mounted paths (four at a time, `deepSearchLimit` per run) so matches appear
2444
+ * in place, in their folders, like any other row. Locked folders are skipped.
2445
+ */
2446
+ const deep = reactive({
2447
+ running: false,
2448
+ searched: 0,
2449
+ failed: 0,
2450
+ capped: false,
2451
+ done: false
2452
+ });
2453
+ /**
2454
+ * Deep search mode: entered by “Try deep search” (or at once with
2455
+ * `deep-search="auto"`), left by clearing the filter. While on, new keywords,
2456
+ * refreshes and newly unlocked folders are searched without asking again.
2457
+ */
2458
+ const deepMode = ref(false);
2459
+ let deepSeq = 0;
2460
+ /** Folders that are known but not loaded yet (locked ones excluded). */
2461
+ function unloadedFolders() {
2462
+ const out = [];
2463
+ const walk = (nodes) => {
2464
+ for (const node of nodes) {
2465
+ if (!node.is_dir || node.lock) continue;
2466
+ if (!node.loaded) out.push(node);
2467
+ else walk(node.children);
2468
+ }
2469
+ };
2470
+ walk(roots.value);
2471
+ return out;
2472
+ }
2473
+ const hasUnloaded = computed(() => unloadedFolders().length > 0);
2474
+ const deepRow = computed(() => {
2475
+ if (!keyword.value.trim()) return null;
2476
+ if (deep.running) return "running";
2477
+ if (deep.capped) return "capped";
2478
+ if (!deepMode.value) return hasUnloaded.value ? "idle" : null;
2479
+ return deep.done ? "done" : null;
2480
+ });
2481
+ /** “Try deep search”, “Continue”, and the automatic runs of deep search mode. */
2482
+ async function deepSearch() {
2483
+ deepMode.value = true;
2484
+ const seq = ++deepSeq;
2485
+ const limit = Math.max(1, Math.floor(props.deepSearchLimit ?? 500));
2486
+ const runStart = deep.searched;
2487
+ Object.assign(deep, {
2488
+ running: true,
2489
+ capped: false,
2490
+ done: false
2491
+ });
2492
+ let wave = unloadedFolders();
2493
+ while (wave.length && seq === deepSeq) {
2494
+ const next = [];
2495
+ const queue = [...wave];
2496
+ const worker = async () => {
2497
+ for (let node = queue.shift(); node && seq === deepSeq; node = queue.shift()) {
2498
+ if (deep.searched - runStart >= limit) return;
2499
+ deep.searched += 1;
2500
+ if (!await loadChildren(node, false, true)) {
2501
+ if (!node.lock) deep.failed += 1;
2502
+ continue;
2503
+ }
2504
+ for (const child of node.children) if (child.is_dir && !child.loaded && !child.lock) next.push(child);
2505
+ }
2506
+ };
2507
+ await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
2508
+ if (deep.searched - runStart >= limit) break;
2509
+ wave = next;
2510
+ }
2511
+ if (seq !== deepSeq) return;
2512
+ deep.running = false;
2513
+ deep.capped = hasUnloaded.value && deep.searched - runStart >= limit;
2514
+ deep.done = !deep.capped;
2515
+ }
2516
+ /** Pause a run (internal: refresh, reload). */
2517
+ function stopDeepSearch() {
2518
+ deepSeq += 1;
2519
+ deep.running = false;
2520
+ }
2521
+ /** The user's Stop: keep the mode, offer “Continue”, do not restart by itself. */
2522
+ function pauseDeepSearch() {
2523
+ const wasRunning = deep.running;
2524
+ stopDeepSearch();
2525
+ if (wasRunning && hasUnloaded.value) deep.capped = true;
2526
+ }
2527
+ function clearFilter() {
2528
+ keyword.value = "";
2529
+ }
2530
+ watch(keyword, (value) => {
2531
+ if (value.trim()) {
2532
+ if (props.deepSearch === "auto") deepMode.value = true;
2533
+ return;
2534
+ }
2535
+ stopDeepSearch();
2536
+ deepMode.value = false;
2537
+ Object.assign(deep, {
2538
+ searched: 0,
2539
+ failed: 0,
2540
+ capped: false,
2541
+ done: false
2542
+ });
2543
+ });
2544
+ watch(() => deepMode.value && hasUnloaded.value && !deep.running && !deep.capped && !refreshing.value, (ready) => {
2545
+ if (ready) deepSearch();
2546
+ });
2547
+ /**
2548
+ * Counted from the tree, so a refresh that re-reads the same folders shows the
2549
+ * same numbers: folders read (or found locked), and the locked ones.
2550
+ */
2551
+ const folderCounts = computed(() => {
2552
+ const counts = {
2553
+ checked: 0,
2554
+ locked: 0
2555
+ };
2556
+ const walk = (nodes) => {
2557
+ for (const node of nodes) {
2558
+ if (!node.is_dir) continue;
2559
+ if (node.lock) {
2560
+ counts.checked += 1;
2561
+ counts.locked += 1;
2562
+ } else if (node.loaded) {
2563
+ counts.checked += 1;
2564
+ walk(node.children);
2565
+ }
2566
+ }
2567
+ };
2568
+ walk(roots.value);
2569
+ return counts;
2570
+ });
2571
+ const lockedFolders = computed(() => folderCounts.value.locked);
2572
+ function lockedInSearch(node) {
2573
+ return deepMode.value && !!node.lock && !!keyword.value.trim();
2574
+ }
2575
+ const deepSummary = computed(() => [
2576
+ t(deepRow.value === "capped" ? "deepCapped" : "deepDone", { count: folderCounts.value.checked }),
2577
+ lockedFolders.value ? t("deepSkipped", { count: lockedFolders.value }) : "",
2578
+ deep.failed ? t("deepFailed", { count: deep.failed }) : ""
2579
+ ].filter(Boolean).join(" · "));
2203
2580
  const stats = computed(() => folderStats(roots.value, selected.value));
2204
2581
  const noStats = {
2205
2582
  files: 0,
@@ -2525,15 +2902,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2525
2902
  name: "close",
2526
2903
  size: 18
2527
2904
  }), createElementVNode("span", _hoisted_13, toDisplayString(t("clear")), 1)], 8, _hoisted_12)) : createCommentVNode("v-if", true),
2528
- createElementVNode("div", _hoisted_14, [createElementVNode("label", _hoisted_15, [createVNode(_sfc_main$2, {
2529
- name: "search",
2530
- size: 15
2531
- }), withDirectives(createElementVNode("input", {
2532
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
2533
- type: "text",
2534
- placeholder: t("filter"),
2535
- "aria-label": t("filter")
2536
- }, null, 8, _hoisted_16), [[vModelText, keyword.value]])]), createElementVNode("button", {
2905
+ createElementVNode("div", _hoisted_14, [createElementVNode("label", _hoisted_15, [
2906
+ createVNode(_sfc_main$2, {
2907
+ name: "search",
2908
+ size: 15
2909
+ }),
2910
+ withDirectives(createElementVNode("input", {
2911
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
2912
+ type: "text",
2913
+ placeholder: t("filter"),
2914
+ "aria-label": t("filter"),
2915
+ onKeydown: _cache[1] || (_cache[1] = withKeys(($event) => keyword.value && ($event.preventDefault(), clearFilter()), ["esc"]))
2916
+ }, null, 40, _hoisted_16), [[vModelText, keyword.value]]),
2917
+ keyword.value ? (openBlock(), createElementBlock("button", {
2918
+ key: 0,
2919
+ class: "fl-search-clear",
2920
+ type: "button",
2921
+ "aria-label": t("clearFilter"),
2922
+ title: t("clearFilter"),
2923
+ onClick: clearFilter
2924
+ }, [createVNode(_sfc_main$2, {
2925
+ name: "close",
2926
+ size: 12
2927
+ })], 8, _hoisted_17)) : createCommentVNode("v-if", true)
2928
+ ]), createElementVNode("button", {
2537
2929
  class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
2538
2930
  type: "button",
2539
2931
  disabled: refreshing.value || loadingRoot.value,
@@ -2543,7 +2935,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2543
2935
  }, [createVNode(_sfc_main$2, {
2544
2936
  name: "refresh",
2545
2937
  size: 15
2546
- })], 10, _hoisted_17)]),
2938
+ })], 10, _hoisted_18)]),
2547
2939
  batchBusy.value ? (openBlock(), createElementBlock("button", {
2548
2940
  key: 4,
2549
2941
  class: "fl-btn fl-zip-cancel",
@@ -2559,16 +2951,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2559
2951
  size: 15
2560
2952
  }), createTextVNode(" " + toDisplayString(t("batch")), 1)])) : createCommentVNode("v-if", true)
2561
2953
  ]),
2562
- createElementVNode("div", _hoisted_18, [
2563
- refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_19)) : createCommentVNode("v-if", true),
2564
- createElementVNode("label", _hoisted_20, [createElementVNode("input", {
2954
+ createElementVNode("div", _hoisted_19, [
2955
+ refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_20)) : createCommentVNode("v-if", true),
2956
+ createElementVNode("label", _hoisted_21, [createElementVNode("input", {
2565
2957
  type: "checkbox",
2566
2958
  "aria-label": t("selectAll"),
2567
2959
  disabled: batchBusy.value,
2568
2960
  checked: allChecked.value,
2569
2961
  ".indeterminate": someChecked.value,
2570
2962
  onChange: toggleCheckAll
2571
- }, null, 40, _hoisted_21)]),
2963
+ }, null, 40, _hoisted_22)]),
2572
2964
  (openBlock(), createElementBlock(Fragment, null, renderList([
2573
2965
  "name",
2574
2966
  "size",
@@ -2582,23 +2974,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2582
2974
  type: "button",
2583
2975
  "aria-sort": sortKey.value === column ? sortAsc.value ? "ascending" : "descending" : void 0,
2584
2976
  onClick: ($event) => setSort(column)
2585
- }, [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);
2977
+ }, [createTextVNode(toDisplayString(t(column === "name" ? "columnName" : column === "size" ? "columnSize" : "columnDate")), 1), createElementVNode("span", _hoisted_24, toDisplayString(sortKey.value === column ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_23)], 2);
2586
2978
  }), 64)),
2587
- createElementVNode("span", _hoisted_24, [createElementVNode("label", _hoisted_25, [
2979
+ createElementVNode("span", _hoisted_25, [createElementVNode("label", _hoisted_26, [
2588
2980
  createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
2589
- createElementVNode("span", _hoisted_26, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
2981
+ createElementVNode("span", _hoisted_27, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
2590
2982
  withDirectives(createElementVNode("select", {
2591
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => sortValue.value = $event),
2983
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => sortValue.value = $event),
2592
2984
  "aria-label": t("sortBy")
2593
2985
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
2594
2986
  return openBlock(), createElementBlock("option", {
2595
2987
  key: option.value,
2596
2988
  value: option.value
2597
- }, toDisplayString(option.label), 9, _hoisted_28);
2598
- }), 128))], 8, _hoisted_27), [[vModelSelect, sortValue.value]])
2989
+ }, toDisplayString(option.label), 9, _hoisted_29);
2990
+ }), 128))], 8, _hoisted_28), [[vModelSelect, sortValue.value]])
2599
2991
  ])])
2600
2992
  ]),
2601
- loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_29, toDisplayString(t("loading")), 1)) : rootLock.value ? (openBlock(), createElementBlock("form", {
2993
+ loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_30, toDisplayString(t("loading")), 1)) : rootLock.value ? (openBlock(), createElementBlock("form", {
2602
2994
  key: 1,
2603
2995
  class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.value.error }]),
2604
2996
  onSubmit: withModifiers(submitRootUnlock, ["prevent"])
@@ -2610,24 +3002,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2610
3002
  createElementVNode("label", {
2611
3003
  class: "fl-unlock-label",
2612
3004
  for: `${unref(uid)}-root`
2613
- }, toDisplayString(t("passwordPrompt")), 9, _hoisted_30),
3005
+ }, toDisplayString(t("passwordPrompt")), 9, _hoisted_31),
2614
3006
  rootLock.value.error ? (openBlock(), createElementBlock("span", {
2615
3007
  key: 0,
2616
3008
  id: `${unref(uid)}-root-error`,
2617
3009
  class: "fl-unlock-message",
2618
3010
  role: "alert"
2619
- }, toDisplayString(unlockErrorText(rootLock.value.error)), 9, _hoisted_31)) : createCommentVNode("v-if", true),
2620
- createElementVNode("span", _hoisted_32, [
3011
+ }, toDisplayString(unlockErrorText(rootLock.value.error)), 9, _hoisted_32)) : createCommentVNode("v-if", true),
3012
+ createElementVNode("span", _hoisted_33, [
2621
3013
  withDirectives(createElementVNode("input", {
2622
3014
  id: `${unref(uid)}-root`,
2623
- "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => rootLock.value.input = $event),
3015
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => rootLock.value.input = $event),
2624
3016
  type: "password",
2625
3017
  autocomplete: "off",
2626
3018
  "data-unlock": __props.path,
2627
3019
  placeholder: t("passwordPlaceholder"),
2628
3020
  "aria-invalid": !!rootLock.value.error,
2629
3021
  "aria-describedby": rootLock.value.error ? `${unref(uid)}-root-error` : void 0
2630
- }, null, 8, _hoisted_33), [[vModelText, rootLock.value.input]]),
3022
+ }, null, 8, _hoisted_34), [[vModelText, rootLock.value.input]]),
2631
3023
  canRemember.value ? (openBlock(), createElementBlock("button", {
2632
3024
  key: 0,
2633
3025
  class: "fl-unlock-remember",
@@ -2635,49 +3027,49 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2635
3027
  "aria-pressed": rootLock.value.remember,
2636
3028
  "aria-label": t("rememberPassword"),
2637
3029
  title: t("rememberPassword"),
2638
- onClick: _cache[3] || (_cache[3] = ($event) => rootLock.value.remember = !rootLock.value.remember)
2639
- }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_34)) : createCommentVNode("v-if", true),
3030
+ onClick: _cache[4] || (_cache[4] = ($event) => rootLock.value.remember = !rootLock.value.remember)
3031
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_35)) : createCommentVNode("v-if", true),
2640
3032
  createElementVNode("button", {
2641
3033
  class: "fl-unlock-submit",
2642
3034
  type: "submit",
2643
3035
  disabled: rootLock.value.busy || !rootLock.value.input
2644
- }, toDisplayString(t("open")), 9, _hoisted_35)
3036
+ }, toDisplayString(t("open")), 9, _hoisted_36)
2645
3037
  ])
2646
- ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_36, [
3038
+ ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_37, [
2647
3039
  createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
2648
- /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_37, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
2649
- unref(isNetworkError)(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_38, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
3040
+ /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_38, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
3041
+ unref(isNetworkError)(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_39, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
2650
3042
  createElementVNode("button", {
2651
3043
  class: "fl-btn",
2652
3044
  type: "button",
2653
3045
  onClick: loadRoot
2654
3046
  }, toDisplayString(t("retry")), 1)
2655
- ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_39, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
3047
+ ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_40, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2656
3048
  (openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (node, index) => {
2657
3049
  return openBlock(), createElementBlock(Fragment, { key: isMore(node) ? `more:${node.key}` : node.path }, [isMore(node) ? (openBlock(), createElementBlock("div", {
2658
3050
  key: 0,
2659
3051
  class: "fl-row fl-more",
2660
3052
  style: normalizeStyle({ "--fl-depth": node.depth })
2661
- }, [_cache[9] || (_cache[9] = createElementVNode("span", { class: "fl-cell fl-cell-check" }, null, -1)), createElementVNode("span", _hoisted_40, [
2662
- _cache[8] || (_cache[8] = createElementVNode("span", {
3053
+ }, [_cache[10] || (_cache[10] = createElementVNode("span", { class: "fl-cell fl-cell-check" }, null, -1)), createElementVNode("span", _hoisted_41, [
3054
+ _cache[9] || (_cache[9] = createElementVNode("span", {
2663
3055
  class: "fl-indent",
2664
3056
  "aria-hidden": "true"
2665
3057
  }, null, -1)),
2666
- createElementVNode("span", _hoisted_41, toDisplayString(t("moreRemaining", { count: node.remaining })), 1),
3058
+ createElementVNode("span", _hoisted_42, toDisplayString(t("moreRemaining", { count: node.remaining })), 1),
2667
3059
  node.remaining > (__props.pageSize || 0) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("button", {
2668
3060
  class: "fl-more-btn",
2669
3061
  type: "button",
2670
3062
  onClick: ($event) => showMore(node, false)
2671
- }, [createTextVNode(toDisplayString(t("showMore")), 1), createElementVNode("span", _hoisted_43, toDisplayString(` ${t("folderItems", { count: __props.pageSize })}`), 1)], 8, _hoisted_42), createElementVNode("button", {
3063
+ }, [createTextVNode(toDisplayString(t("showMore")), 1), createElementVNode("span", _hoisted_44, toDisplayString(` ${t("folderItems", { count: __props.pageSize })}`), 1)], 8, _hoisted_43), createElementVNode("button", {
2672
3064
  class: "fl-more-btn fl-more-all",
2673
3065
  type: "button",
2674
3066
  onClick: ($event) => showMore(node, true)
2675
- }, toDisplayString(t("showAll")), 9, _hoisted_44)], 64)) : (openBlock(), createElementBlock("button", {
3067
+ }, toDisplayString(t("showAll")), 9, _hoisted_45)], 64)) : (openBlock(), createElementBlock("button", {
2676
3068
  key: 1,
2677
3069
  class: "fl-more-btn",
2678
3070
  type: "button",
2679
3071
  onClick: ($event) => showMore(node, true)
2680
- }, toDisplayString(t("showRest", { count: node.remaining })), 9, _hoisted_45))
3072
+ }, toDisplayString(t("showRest", { count: node.remaining })), 9, _hoisted_46))
2681
3073
  ])], 4)) : (openBlock(), createElementBlock("div", {
2682
3074
  key: 1,
2683
3075
  class: normalizeClass(["fl-row", {
@@ -2690,19 +3082,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2690
3082
  }]),
2691
3083
  style: normalizeStyle({ "--fl-depth": node.depth })
2692
3084
  }, [
2693
- createElementVNode("label", _hoisted_46, [createElementVNode("input", {
3085
+ createElementVNode("label", _hoisted_47, [createElementVNode("input", {
2694
3086
  type: "checkbox",
2695
3087
  disabled: !!node.lock || batchBusy.value,
2696
3088
  "aria-label": t("selectItem", { name: node.name }),
2697
3089
  checked: isChecked(node),
2698
3090
  ".indeterminate": isIndeterminate(node),
2699
3091
  onChange: ($event) => toggleCheck(node, $event)
2700
- }, null, 40, _hoisted_47)]),
3092
+ }, null, 40, _hoisted_48)]),
2701
3093
  createElementVNode("span", {
2702
3094
  class: "fl-cell fl-cell-name",
2703
3095
  title: node.isRoot ? node.path : void 0
2704
3096
  }, [
2705
- _cache[11] || (_cache[11] = createElementVNode("span", {
3097
+ _cache[12] || (_cache[12] = createElementVNode("span", {
2706
3098
  class: "fl-indent",
2707
3099
  "aria-hidden": "true"
2708
3100
  }, null, -1)),
@@ -2716,7 +3108,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2716
3108
  }, [createVNode(_sfc_main$2, {
2717
3109
  name: "lock",
2718
3110
  size: 14
2719
- })], 8, _hoisted_49)) : node.is_dir ? (openBlock(), createElementBlock("button", {
3111
+ })], 8, _hoisted_50)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2720
3112
  key: 1,
2721
3113
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
2722
3114
  type: "button",
@@ -2726,26 +3118,29 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2726
3118
  }, [createVNode(_sfc_main$2, {
2727
3119
  name: "next",
2728
3120
  size: 14
2729
- })], 10, _hoisted_50)) : (openBlock(), createElementBlock("span", _hoisted_51)),
2730
- 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", {
3121
+ })], 10, _hoisted_51)) : (openBlock(), createElementBlock("span", _hoisted_52)),
3122
+ node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_53, [..._cache[11] || (_cache[11] = [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", {
2731
3123
  key: 4,
2732
3124
  class: "fl-type",
2733
3125
  "data-kind": unref(fileKind)(node.name),
2734
3126
  "aria-hidden": "true"
2735
- }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_53)),
2736
- createElementVNode("span", _hoisted_54, [createElementVNode("span", {
3127
+ }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_54)),
3128
+ createElementVNode("span", _hoisted_55, [createElementVNode("span", {
2737
3129
  class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
2738
3130
  onClick: ($event) => node.lock || node.is_dir ? toggleExpand(node) : download(node)
2739
- }, 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)])]),
2740
- node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_57, toDisplayString(t("folderItems", { count: statsOf(node).files })), 1)) : createCommentVNode("v-if", true),
3131
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(nameParts(node.name), (part, at) => {
3132
+ return openBlock(), createElementBlock(Fragment, { key: at }, [part.hit ? (openBlock(), createElementBlock("mark", _hoisted_57, toDisplayString(part.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(part.text), 1)], 64))], 64);
3133
+ }), 128))], 10, _hoisted_56), createElementVNode("span", _hoisted_58, [!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)) : lockedInSearch(node) ? (openBlock(), createElementBlock("span", _hoisted_59, toDisplayString(t("deepLocked")), 1)) : createCommentVNode("v-if", true)])]),
3134
+ lockedInSearch(node) && !node.lock?.prompting ? (openBlock(), createElementBlock("span", _hoisted_60, toDisplayString(t("deepLocked")), 1)) : createCommentVNode("v-if", true),
3135
+ node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_61, toDisplayString(t("folderItems", { count: statsOf(node).files })), 1)) : createCommentVNode("v-if", true),
2741
3136
  node.lock?.prompting && node.lock.error ? (openBlock(), createElementBlock("span", {
2742
- key: 6,
3137
+ key: 7,
2743
3138
  id: `${unref(uid)}-error-${index}`,
2744
3139
  class: "fl-unlock-message",
2745
3140
  role: "alert"
2746
- }, toDisplayString(unlockErrorText(node.lock.error)), 9, _hoisted_58)) : createCommentVNode("v-if", true),
3141
+ }, toDisplayString(unlockErrorText(node.lock.error)), 9, _hoisted_62)) : createCommentVNode("v-if", true),
2747
3142
  !node.lock && unref(remembered).has(node.path) ? (openBlock(), createElementBlock("button", {
2748
- key: 7,
3143
+ key: 8,
2749
3144
  class: "fl-remembered",
2750
3145
  type: "button",
2751
3146
  "aria-label": `${t("forgetPassword")} ${node.name}`,
@@ -2754,9 +3149,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2754
3149
  }, [createVNode(_sfc_main$2, {
2755
3150
  name: "key",
2756
3151
  size: 11
2757
- }), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_59)) : createCommentVNode("v-if", true),
2758
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_60, "…")) : createCommentVNode("v-if", true)
2759
- ], 8, _hoisted_48),
3152
+ }), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_63)) : createCommentVNode("v-if", true),
3153
+ node.loading ? (openBlock(), createElementBlock("span", _hoisted_64, "…")) : createCommentVNode("v-if", true)
3154
+ ], 8, _hoisted_49),
2760
3155
  node.lock?.prompting ? (openBlock(), createElementBlock("form", {
2761
3156
  key: 0,
2762
3157
  class: "fl-unlock fl-unlock-row",
@@ -2777,7 +3172,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2777
3172
  "aria-label": t("passwordFor", { name: node.name }),
2778
3173
  "aria-invalid": !!node.lock.error,
2779
3174
  "aria-describedby": node.lock.error ? `${unref(uid)}-error-${index}` : void 0
2780
- }, null, 8, _hoisted_62), [[vModelText, node.lock.input]]),
3175
+ }, null, 8, _hoisted_66), [[vModelText, node.lock.input]]),
2781
3176
  canRemember.value ? (openBlock(), createElementBlock("button", {
2782
3177
  key: 0,
2783
3178
  class: "fl-unlock-remember",
@@ -2786,12 +3181,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2786
3181
  "aria-label": t("rememberPassword"),
2787
3182
  title: t("rememberPassword"),
2788
3183
  onClick: ($event) => node.lock.remember = !node.lock.remember
2789
- }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_63)) : createCommentVNode("v-if", true),
3184
+ }, [createVNode(_sfc_main$2, { name: "key" })], 8, _hoisted_67)) : createCommentVNode("v-if", true),
2790
3185
  createElementVNode("button", {
2791
3186
  class: "fl-unlock-submit",
2792
3187
  type: "submit",
2793
3188
  disabled: node.lock.busy || !node.lock.input
2794
- }, toDisplayString(t("open")), 9, _hoisted_64),
3189
+ }, toDisplayString(t("open")), 9, _hoisted_68),
2795
3190
  createElementVNode("button", {
2796
3191
  class: "fl-unlock-cancel",
2797
3192
  type: "button",
@@ -2801,11 +3196,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2801
3196
  }, [createVNode(_sfc_main$2, {
2802
3197
  name: "close",
2803
3198
  size: 14
2804
- })], 8, _hoisted_65)
2805
- ], 40, _hoisted_61)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2806
- createElementVNode("span", _hoisted_66, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
2807
- createElementVNode("span", _hoisted_67, toDisplayString(unref(formatDate)(node.modified)), 1),
2808
- createElementVNode("span", _hoisted_68, [node.lock ? (openBlock(), createElementBlock("button", {
3199
+ })], 8, _hoisted_69)
3200
+ ], 40, _hoisted_65)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
3201
+ createElementVNode("span", _hoisted_70, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
3202
+ createElementVNode("span", _hoisted_71, toDisplayString(unref(formatDate)(node.modified)), 1),
3203
+ createElementVNode("span", _hoisted_72, [node.lock ? (openBlock(), createElementBlock("button", {
2809
3204
  key: 0,
2810
3205
  class: "fl-action fl-arrow-end fl-lock",
2811
3206
  type: "button",
@@ -2814,7 +3209,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2814
3209
  }, [createVNode(_sfc_main$2, {
2815
3210
  name: "lock",
2816
3211
  size: 18
2817
- })], 8, _hoisted_69)) : node.is_dir ? (openBlock(), createElementBlock("button", {
3212
+ })], 8, _hoisted_73)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2818
3213
  key: 1,
2819
3214
  class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
2820
3215
  type: "button",
@@ -2824,25 +3219,38 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2824
3219
  }, [createVNode(_sfc_main$2, {
2825
3220
  name: "next",
2826
3221
  size: 18
2827
- })], 10, _hoisted_70)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
3222
+ })], 10, _hoisted_74)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2828
3223
  key: 0,
2829
3224
  class: "fl-action fl-preview-btn",
2830
3225
  type: "button",
2831
3226
  "aria-label": `${t("preview")} ${node.name}`,
2832
3227
  title: t("preview"),
2833
3228
  onClick: ($event) => preview(node)
2834
- }, [createVNode(_sfc_main$2, { name: "eye" })], 8, _hoisted_71)) : createCommentVNode("v-if", true), createElementVNode("button", {
3229
+ }, [createVNode(_sfc_main$2, { name: "eye" })], 8, _hoisted_75)) : createCommentVNode("v-if", true), createElementVNode("button", {
2835
3230
  class: "fl-action fl-download",
2836
3231
  type: "button",
2837
3232
  "aria-label": `${t("download")} ${node.name}`,
2838
3233
  title: t("download"),
2839
3234
  onClick: ($event) => download(node)
2840
- }, [createVNode(_sfc_main$2, { name: "download" })], 8, _hoisted_72)], 64))])
3235
+ }, [createVNode(_sfc_main$2, { name: "download" })], 8, _hoisted_76)], 64))])
2841
3236
  ], 64))
2842
3237
  ], 6))], 64);
2843
3238
  }), 128)),
2844
- 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)]),
2845
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_74, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
3239
+ deepRow.value ? (openBlock(), createElementBlock("div", _hoisted_77, [createElementVNode("p", _hoisted_78, [deepRow.value === "running" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("deepRunning", { count: folderCounts.value.checked })) + " · ", 1), createElementVNode("button", {
3240
+ class: "fl-deep-link",
3241
+ type: "button",
3242
+ onClick: pauseDeepSearch
3243
+ }, toDisplayString(t("deepStop")), 1)], 64)) : deepRow.value === "idle" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(t("deepHint")), 1), createElementVNode("button", {
3244
+ class: "fl-deep-link",
3245
+ type: "button",
3246
+ onClick: deepSearch
3247
+ }, toDisplayString(t("deepSearch")), 1)], 64)) : deepRow.value === "capped" ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createTextVNode(toDisplayString(deepSummary.value) + " · ", 1), createElementVNode("button", {
3248
+ class: "fl-deep-link",
3249
+ type: "button",
3250
+ onClick: deepSearch
3251
+ }, toDisplayString(t("deepContinue")), 1)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [createTextVNode(toDisplayString(deepSummary.value), 1)], 64))]), deepRow.value === "running" ? (openBlock(), createElementBlock("span", _hoisted_79)) : createCommentVNode("v-if", true)])) : createCommentVNode("v-if", true),
3252
+ createElementVNode("div", _hoisted_80, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) })), 1)]),
3253
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_81, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
2846
3254
  createVNode(_sfc_main$3, {
2847
3255
  open: !!zipDialog.value,
2848
3256
  title: zipDialog.value?.title ?? "",
@@ -2851,7 +3259,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2851
3259
  "confirm-label": zipDialog.value?.confirm,
2852
3260
  "cancel-label": zipDialog.value?.cancel ?? "",
2853
3261
  onConfirm: confirmZip,
2854
- onClose: _cache[4] || (_cache[4] = ($event) => zipNotice.value = null)
3262
+ onClose: _cache[5] || (_cache[5] = ($event) => zipNotice.value = null)
2855
3263
  }, null, 8, [
2856
3264
  "open",
2857
3265
  "title",
@@ -2872,9 +3280,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2872
3280
  position: previewPosition.value,
2873
3281
  total: previewPosition.value ? previewList.value.length : 0,
2874
3282
  onClose: closePreview,
2875
- onPrev: _cache[5] || (_cache[5] = ($event) => stepPreview(-1)),
2876
- onNext: _cache[6] || (_cache[6] = ($event) => stepPreview(1)),
2877
- onDownload: _cache[7] || (_cache[7] = ($event) => previewNode.value && download(previewNode.value))
3283
+ onPrev: _cache[6] || (_cache[6] = ($event) => stepPreview(-1)),
3284
+ onNext: _cache[7] || (_cache[7] = ($event) => stepPreview(1)),
3285
+ onDownload: _cache[8] || (_cache[8] = ($event) => previewNode.value && download(previewNode.value))
2878
3286
  }, null, 8, [
2879
3287
  "open",
2880
3288
  "file",