vitepress-plugin-filelist 0.4.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { computed, defineComponent, mergeProps, onBeforeUnmount, onMounted, ref, unref, useSSRContext, watch } from "vue";
1
+ import { computed, defineComponent, mergeProps, nextTick, onBeforeUnmount, onMounted, reactive, ref, unref, useSSRContext, watch } from "vue";
2
2
  import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
3
3
  // src/client/locales.ts
4
4
  const fileListLocales = {
@@ -11,7 +11,7 @@ const fileListLocales = {
11
11
  batchCount: "批量下载({count})",
12
12
  batching: "打包中 {percent}%",
13
13
  passwordPrompt: "该目录受密码保护:",
14
- passwordPlaceholder: "输入目录密码",
14
+ passwordPlaceholder: "输入密码",
15
15
  open: "打开",
16
16
  columnName: "文件名",
17
17
  columnSize: "文件大小",
@@ -19,7 +19,7 @@ const fileListLocales = {
19
19
  loading: "加载中…",
20
20
  loadFailed: "加载失败:{message}",
21
21
  guestHint: "在 OpenList 后台启用 guest,或给组件传一个只读 token。",
22
- corsHint: "可能是跨域或混合内容被拦截,检查 OpenList 的 cors 配置与页面协议。",
22
+ corsHint: "可能是跨域或混合内容被拦截:检查 OpenList 的 cors.allow_origins(OpenList-Worker 为 ALLOW_URLS)与页面协议。",
23
23
  retry: "重试",
24
24
  empty: "没有文件",
25
25
  preview: "预览",
@@ -52,7 +52,18 @@ const fileListLocales = {
52
52
  previewUnsupported: "该类型暂不支持在线预览,可下载后查看",
53
53
  previewUnavailable: "该文件无法在页面内预览,可下载后查看",
54
54
  previewTooLarge: "文件较大({size}),请下载后查看",
55
- textTruncated: "仅显示前 {size},完整内容请下载"
55
+ textTruncated: "仅显示前 {size},完整内容请下载",
56
+ refreshing: "刷新中",
57
+ refreshFailed: "刷新失败:{message}",
58
+ cancel: "取消",
59
+ passwordFor: "{name} 的密码",
60
+ passwordWrong: "密码不正确或无权访问",
61
+ passwordStale: "已保存的密码已失效,请重新输入",
62
+ rememberPassword: "在此标签页中记住(关闭标签页后失效)",
63
+ remembered: "已记住",
64
+ forgetPassword: "忘记",
65
+ unlockFolder: "输入 {name} 的密码",
66
+ networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。"
56
67
  },
57
68
  "en-US": {
58
69
  allFiles: "All files",
@@ -71,7 +82,7 @@ const fileListLocales = {
71
82
  loading: "Loading…",
72
83
  loadFailed: "Failed to load: {message}",
73
84
  guestHint: "Enable the guest user in OpenList, or pass a read-only token.",
74
- corsHint: "Possibly blocked by CORS or mixed content. Check OpenList cors settings and the page protocol.",
85
+ corsHint: "Possibly blocked by CORS or mixed content. Check cors.allow_origins in OpenList (ALLOW_URLS in OpenList-Worker) and the page protocol.",
75
86
  retry: "Retry",
76
87
  empty: "No files",
77
88
  preview: "Preview",
@@ -104,7 +115,18 @@ const fileListLocales = {
104
115
  previewUnsupported: "This file type cannot be previewed here. Download it to view.",
105
116
  previewUnavailable: "This file cannot be previewed in the page. Download it to view.",
106
117
  previewTooLarge: "This file is large ({size}). Download it to view.",
107
- textTruncated: "Showing the first {size} only. Download for the full file."
118
+ textTruncated: "Showing the first {size} only. Download for the full file.",
119
+ refreshing: "Refreshing",
120
+ refreshFailed: "Refresh failed: {message}",
121
+ cancel: "Cancel",
122
+ passwordFor: "Password for {name}",
123
+ passwordWrong: "Wrong password or no permission",
124
+ passwordStale: "The saved password no longer works. Enter it again.",
125
+ rememberPassword: "Remember in this tab (cleared when the tab closes)",
126
+ remembered: "Remembered",
127
+ forgetPassword: "Forget",
128
+ unlockFolder: "Enter the password for {name}",
129
+ networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker."
108
130
  }
109
131
  };
110
132
  function formatLabel(template, values = {}) {
@@ -656,6 +678,70 @@ _sfc_main$1.setup = (props, ctx) => {
656
678
  (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/PreviewDialog.vue");
657
679
  return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
658
680
  };
681
+ // src/client/passwords.ts
682
+ /**
683
+ * Folder passwords remembered for the current browser tab.
684
+ *
685
+ * Stored in `sessionStorage`, so they live only in this tab and are gone when
686
+ * it closes. Keyed by OpenList base URL, then by folder path; a remembered
687
+ * folder also covers its sub-folders. Every access is guarded: private mode
688
+ * or blocked storage simply means nothing is remembered.
689
+ */
690
+ const PREFIX = "vitepress-plugin-filelist:passwords:";
691
+ function store() {
692
+ try {
693
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ function read(base) {
699
+ try {
700
+ const raw = store()?.getItem(PREFIX + base);
701
+ const parsed = raw ? JSON.parse(raw) : {};
702
+ return parsed && typeof parsed === "object" ? parsed : {};
703
+ } catch {
704
+ return {};
705
+ }
706
+ }
707
+ function write(base, map) {
708
+ try {
709
+ const target = store();
710
+ if (!target) return;
711
+ if (Object.keys(map).length) target.setItem(PREFIX + base, JSON.stringify(map));
712
+ else target.removeItem(PREFIX + base);
713
+ } catch {}
714
+ }
715
+ function normalise(path) {
716
+ return path.replace(/\/+$/, "") || "/";
717
+ }
718
+ /** Whether `path` is `folder` itself or inside it. */
719
+ function isWithin(path, folder) {
720
+ const p = normalise(path);
721
+ const f = normalise(folder);
722
+ return f === "/" || p === f || p.startsWith(`${f}/`);
723
+ }
724
+ /** The closest remembered folder at or above `path`, if any. */
725
+ function findRememberedPassword(base, path) {
726
+ let best = null;
727
+ for (const [folder, password] of Object.entries(read(base))) {
728
+ if (typeof password !== "string" || !isWithin(path, folder)) continue;
729
+ if (!best || folder.length > best.path.length) best = {
730
+ path: folder,
731
+ password
732
+ };
733
+ }
734
+ return best;
735
+ }
736
+ function forgetFolderPassword(base, path) {
737
+ const map = read(base);
738
+ delete map[normalise(path)];
739
+ write(base, map);
740
+ }
741
+ /** Folders with a remembered password, for showing the "remembered" tag. */
742
+ function rememberedFolders(base) {
743
+ return Object.keys(read(base));
744
+ }
659
745
  // src/client/FileList.vue
660
746
  const _sfc_main = /*@__PURE__*/ defineComponent({
661
747
  __name: "FileList",
@@ -671,6 +757,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
671
757
  token: { default: "" },
672
758
  password: { default: "" },
673
759
  title: { default: "" },
760
+ rememberPassword: {
761
+ type: Boolean,
762
+ default: true
763
+ },
674
764
  locale: { default: "zh-CN" },
675
765
  labels: {}
676
766
  },
@@ -689,12 +779,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
689
779
  }
690
780
  const demo = computed(() => !props.base);
691
781
  const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
692
- const activePassword = ref(props.password);
782
+ const rootEl = ref();
693
783
  const roots = ref([]);
694
784
  const loadingRoot = ref(false);
695
785
  const errorMsg = ref("");
696
- const needsPassword = ref(false);
697
- const passwordInput = ref("");
786
+ const refreshing = ref(false);
787
+ let refreshSeq = 0;
788
+ /** Single-path mode: the listed folder itself needs a password. */
789
+ const rootLock = reactive({
790
+ active: false,
791
+ input: "",
792
+ error: "",
793
+ remember: false,
794
+ busy: false
795
+ });
796
+ /** Password the visitor typed for the listed folder (single-path mode). */
797
+ const rootPassword = ref("");
698
798
  const keyword = ref("");
699
799
  const selected = ref(/* @__PURE__ */ new Set());
700
800
  const toast = ref("");
@@ -718,6 +818,85 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
718
818
  function isPasswordError(error) {
719
819
  return /password|密码/i.test(errorText(error));
720
820
  }
821
+ function isNetworkError(error) {
822
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
823
+ }
824
+ /** A failure toast; browser network errors are usually CORS, so say where to look. */
825
+ function failureMessage(key, error) {
826
+ const message = t(key, { message: errorText(error) });
827
+ return isNetworkError(error) ? `${message} ${t("networkHint")}` : message;
828
+ }
829
+ /** Total size for display: an empty total reads `0 B`, not `-`. */
830
+ function formatTotal(bytes) {
831
+ return bytes > 0 ? formatSize(bytes) : "0 B";
832
+ }
833
+ const canRemember = computed(() => props.rememberPassword !== false);
834
+ const passwordScope = computed(() => baseUrl.value || "demo");
835
+ /** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
836
+ const typedPasswords = /* @__PURE__ */ new Map();
837
+ function typedPasswordFor(path) {
838
+ let best = "";
839
+ let bestPath = "";
840
+ for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
841
+ best = password;
842
+ bestPath = folder;
843
+ }
844
+ return best;
845
+ }
846
+ /** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
847
+ const rememberedPaths = ref(/* @__PURE__ */ new Set());
848
+ function syncRemembered() {
849
+ rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
850
+ }
851
+ function storedPassword(path) {
852
+ return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
853
+ }
854
+ /**
855
+ * Password for an item: its own or inherited one first (site-configured or
856
+ * typed), then one remembered in this tab, then the component's `password`.
857
+ */
858
+ function passwordFor(node) {
859
+ if (node.password) return { password: node.password };
860
+ const typed = typedPasswordFor(node.path);
861
+ if (typed) return { password: typed };
862
+ const stored = storedPassword(node.path);
863
+ if (stored) return {
864
+ password: stored.password,
865
+ rememberedAt: stored.path
866
+ };
867
+ return { password: props.password };
868
+ }
869
+ function rootPasswordFor() {
870
+ if (rootPassword.value) return { password: rootPassword.value };
871
+ const stored = storedPassword(props.path);
872
+ if (stored) return {
873
+ password: stored.password,
874
+ rememberedAt: stored.path
875
+ };
876
+ return { password: props.password };
877
+ }
878
+ /** A remembered password stopped working: drop it so we do not keep retrying it. */
879
+ function dropStale(path) {
880
+ if (!path) return false;
881
+ forgetFolderPassword(passwordScope.value, path);
882
+ syncRemembered();
883
+ return true;
884
+ }
885
+ function focusUnlock(path) {
886
+ nextTick(() => {
887
+ (rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
888
+ });
889
+ }
890
+ /** Show the same-row password input; `focus` only for user actions, never on page load. */
891
+ function startPrompt(node, focus) {
892
+ node.prompting = true;
893
+ if (focus) focusUnlock(node.path);
894
+ }
895
+ function unlockErrorText(error) {
896
+ if (error === "wrong") return t("passwordWrong");
897
+ if (error === "stale") return t("passwordStale");
898
+ return "";
899
+ }
721
900
  const DEMO_TREE = {
722
901
  "/": [
723
902
  {
@@ -749,6 +928,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
749
928
  size: 0,
750
929
  is_dir: true,
751
930
  modified: "2026-09-01T08:00:00+08:00"
931
+ },
932
+ {
933
+ name: "加密示例(密码 1234)",
934
+ size: 0,
935
+ is_dir: true,
936
+ modified: "2026-10-20T18:00:00+08:00"
752
937
  }
753
938
  ],
754
939
  "/assignments": [{
@@ -839,6 +1024,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
839
1024
  is_dir: false,
840
1025
  modified: "2026-01-05T19:02:00+08:00"
841
1026
  }],
1027
+ "/加密示例(密码 1234)": [{
1028
+ name: "期末答案(教师版).pdf",
1029
+ size: 2621440,
1030
+ is_dir: false,
1031
+ modified: "2026-10-20T18:00:00+08:00"
1032
+ }, {
1033
+ name: "成绩登记表.xlsx",
1034
+ size: 48128,
1035
+ is_dir: false,
1036
+ modified: "2026-10-21T09:12:00+08:00"
1037
+ }],
842
1038
  "/slides": [{
843
1039
  name: "第一章 行列式.pptx",
844
1040
  size: 3145728,
@@ -851,6 +1047,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
851
1047
  modified: "2026-09-15T13:30:00+08:00"
852
1048
  }]
853
1049
  };
1050
+ /** Demo-only folder passwords, so the demo can show the password flow. */
1051
+ const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
854
1052
  class ApiError extends Error {
855
1053
  code;
856
1054
  constructor(message, code) {
@@ -858,16 +1056,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
858
1056
  this.code = code;
859
1057
  }
860
1058
  }
861
- function authHeaders() {
1059
+ /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1060
+ function authHeaders(json) {
862
1061
  return {
863
- "Content-Type": "application/json",
1062
+ ...json ? { "Content-Type": "application/json" } : {},
864
1063
  ...props.token ? { Authorization: props.token } : {}
865
1064
  };
866
1065
  }
867
1066
  async function request(apiPath, body) {
868
1067
  const response = await fetch(`${baseUrl.value}${apiPath}`, {
869
1068
  method: body === void 0 ? "GET" : "POST",
870
- headers: authHeaders(),
1069
+ headers: authHeaders(body !== void 0),
871
1070
  body: body === void 0 ? void 0 : JSON.stringify(body)
872
1071
  });
873
1072
  const payload = await response.json().catch(() => null);
@@ -875,9 +1074,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
875
1074
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
876
1075
  return payload.data;
877
1076
  }
878
- async function listDir(path, password = activePassword.value) {
1077
+ async function listDir(path, password) {
879
1078
  if (demo.value) {
880
1079
  await new Promise((resolve) => setTimeout(resolve, 220));
1080
+ const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
1081
+ if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
881
1082
  return DEMO_TREE[path] ?? [];
882
1083
  }
883
1084
  return (await request("/api/fs/list", {
@@ -907,7 +1108,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
907
1108
  try {
908
1109
  url = absoluteUrl((await request("/api/fs/get", {
909
1110
  path: node.path,
910
- password: node.password || activePassword.value
1111
+ password: passwordFor(node).password
911
1112
  }))?.raw_url ?? "");
912
1113
  } catch {
913
1114
  url = "";
@@ -957,84 +1158,154 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
957
1158
  const parts = path.split("/").filter(Boolean);
958
1159
  return parts[parts.length - 1] || path;
959
1160
  }
960
- async function loadChildren(node) {
1161
+ /**
1162
+ * Load a folder's children. A password error marks the folder locked instead of
1163
+ * toasting; `prompt` also shows its same-row input (and focuses it).
1164
+ */
1165
+ async function loadChildren(node, prompt = false) {
961
1166
  if (node.loaded) return true;
1167
+ if (node.locked) {
1168
+ if (prompt) startPrompt(node, prompt === "focus");
1169
+ return false;
1170
+ }
962
1171
  node.loading = true;
1172
+ const { password, rememberedAt } = passwordFor(node);
963
1173
  try {
964
- node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
1174
+ node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
965
1175
  sortTree(node.children);
966
1176
  node.loaded = true;
967
1177
  return true;
968
1178
  } catch (error) {
969
- if (isPasswordError(error)) needsPassword.value = true;
970
- showToast(t("folderFailed", { message: errorText(error) }));
1179
+ if (isPasswordError(error)) {
1180
+ node.locked = true;
1181
+ node.expanded = false;
1182
+ node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1183
+ if (prompt) startPrompt(node, prompt === "focus");
1184
+ } else showToast(failureMessage("folderFailed", error));
971
1185
  return false;
972
1186
  } finally {
973
1187
  node.loading = false;
974
1188
  }
975
1189
  }
976
- async function loadSourceRoot(source) {
1190
+ /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1191
+ function looksLikeFile(path) {
1192
+ const name = sourceName(path);
1193
+ return fileKind(name) !== "other" || previewKind(name) !== "none";
1194
+ }
1195
+ async function loadSourceRoot(source, expanded) {
977
1196
  const path = source.path;
978
- const password = source.password ?? "";
1197
+ const configured = source.password ?? "";
979
1198
  let item = {
980
1199
  name: sourceName(path),
981
1200
  size: 0,
982
- is_dir: true,
1201
+ is_dir: !looksLikeFile(path),
983
1202
  modified: ""
984
1203
  };
985
- if (demo.value) {
986
- if (/\/[^/]+\.[^/]+$/.test(path)) item = {
987
- ...item,
988
- is_dir: false,
989
- modified: "2026-09-01T08:00:00+08:00"
990
- };
991
- } else try {
992
- const data = await request("/api/fs/get", {
993
- path,
994
- password: password || activePassword.value
995
- });
996
- item = {
997
- ...item,
998
- ...data
999
- };
1000
- } catch {}
1204
+ let locked = false;
1205
+ let unlockError = "";
1206
+ if (!demo.value) {
1207
+ const { password, rememberedAt } = configured ? {
1208
+ password: configured,
1209
+ rememberedAt: void 0
1210
+ } : (() => {
1211
+ const stored = storedPassword(path);
1212
+ return stored ? {
1213
+ password: stored.password,
1214
+ rememberedAt: stored.path
1215
+ } : {
1216
+ password: props.password,
1217
+ rememberedAt: void 0
1218
+ };
1219
+ })();
1220
+ try {
1221
+ const data = await request("/api/fs/get", {
1222
+ path,
1223
+ password
1224
+ });
1225
+ item = {
1226
+ ...item,
1227
+ ...data
1228
+ };
1229
+ } catch (error) {
1230
+ if (isPasswordError(error)) {
1231
+ locked = true;
1232
+ unlockError = dropStale(rememberedAt) ? "stale" : "";
1233
+ }
1234
+ }
1235
+ }
1001
1236
  const name = source.title || item.name || sourceName(path);
1002
1237
  return {
1003
1238
  ...item,
1004
1239
  name,
1005
1240
  path,
1006
1241
  depth: 0,
1007
- expanded: source.expanded ?? props.expanded,
1242
+ expanded,
1008
1243
  loaded: false,
1009
1244
  loading: false,
1010
1245
  children: [],
1011
- password,
1246
+ password: configured,
1247
+ configuredPassword: configured,
1012
1248
  isRoot: true,
1249
+ locked,
1250
+ unresolved: locked,
1251
+ unlockError,
1252
+ prompting: locked && expanded,
1013
1253
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
1014
1254
  };
1015
1255
  }
1256
+ class RootLockedError extends Error {
1257
+ rememberedAt;
1258
+ constructor(rememberedAt) {
1259
+ super("root locked");
1260
+ this.rememberedAt = rememberedAt;
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
1265
+ * what was open); without it the configured `expanded` defaults apply.
1266
+ */
1267
+ async function buildTree(expandedPaths) {
1268
+ let nodes;
1269
+ if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1270
+ else {
1271
+ const { password, rememberedAt } = rootPasswordFor();
1272
+ let items;
1273
+ try {
1274
+ items = await listDir(props.path, password);
1275
+ } catch (error) {
1276
+ if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1277
+ throw error;
1278
+ }
1279
+ nodes = toNodes(items, props.path, 0, rootPassword.value);
1280
+ sortTree(nodes);
1281
+ }
1282
+ const open = async (list) => {
1283
+ for (const node of list) {
1284
+ if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
1285
+ node.expanded = await loadChildren(node, "show");
1286
+ if (node.expanded && expandedPaths) await open(node.children);
1287
+ }
1288
+ };
1289
+ await open(nodes);
1290
+ return nodes;
1291
+ }
1292
+ /** First load, or a retry after an error: shows the loading state. */
1016
1293
  async function loadRoot() {
1294
+ refreshSeq += 1;
1295
+ refreshing.value = false;
1017
1296
  loadingRoot.value = true;
1018
1297
  errorMsg.value = "";
1019
- needsPassword.value = false;
1020
1298
  try {
1021
- if (props.sources.length) {
1022
- const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
1023
- roots.value = nodes;
1024
- for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
1025
- selected.value = /* @__PURE__ */ new Set();
1026
- downloadCache.clear();
1027
- return;
1028
- }
1029
- const items = await listDir(props.path);
1030
- roots.value = toNodes(items, props.path, 0, activePassword.value);
1031
- sortTree(roots.value);
1299
+ roots.value = await buildTree();
1300
+ rootLock.active = false;
1032
1301
  selected.value = /* @__PURE__ */ new Set();
1033
1302
  downloadCache.clear();
1034
1303
  } catch (error) {
1035
- const message = errorText(error);
1036
- if (isPasswordError(error)) needsPassword.value = true;
1037
- else errorMsg.value = message;
1304
+ if (error instanceof RootLockedError) {
1305
+ roots.value = [];
1306
+ rootLock.active = true;
1307
+ rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
1308
+ } else errorMsg.value = errorText(error);
1038
1309
  } finally {
1039
1310
  loadingRoot.value = false;
1040
1311
  }
@@ -1182,6 +1453,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1182
1453
  stickyTop.value = top;
1183
1454
  }
1184
1455
  onMounted(() => {
1456
+ syncRemembered();
1185
1457
  loadRoot();
1186
1458
  loadSiteTitle();
1187
1459
  measureStickyTop();
@@ -1193,10 +1465,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1193
1465
  });
1194
1466
  return (_ctx, _push, _parent, _attrs) => {
1195
1467
  _push(`<div${ssrRenderAttrs(mergeProps({
1196
- class: ["fl-root", { "fl-selecting": selected.value.size > 0 }],
1197
- style: { "--fl-sticky-top-auto": `${stickyTop.value}px` }
1468
+ ref_key: "rootEl",
1469
+ ref: rootEl,
1470
+ class: ["fl-root", {
1471
+ "fl-selecting": selected.value.size > 0,
1472
+ "fl-refreshing": refreshing.value
1473
+ }],
1474
+ style: { "--fl-sticky-top-auto": `${stickyTop.value}px` },
1475
+ "aria-busy": refreshing.value || loadingRoot.value
1198
1476
  }, _attrs))}><div class="fl-bar">`);
1199
- if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: unref(formatSize)(selectedBytes.value) }))}</span></div>`);
1477
+ if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: formatTotal(selectedBytes.value) }))}</span></div>`);
1200
1478
  else {
1201
1479
  _push(`<div class="fl-title"><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
1202
1480
  if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
@@ -1205,19 +1483,27 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1205
1483
  }
1206
1484
  if (selected.value.size) _push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}><svg class="fl-clear-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg><span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
1207
1485
  else _push(`<!---->`);
1208
- _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="fl-icon-btn" type="button"${ssrRenderAttr("aria-label", t("refresh"))}${ssrRenderAttr("title", t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
1486
+ _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="${ssrRenderClass([{ "fl-spinning": refreshing.value }, "fl-icon-btn fl-refresh"])}" type="button"${ssrIncludeBooleanAttr(refreshing.value || loadingRoot.value) ? " disabled" : ""}${ssrRenderAttr("aria-label", refreshing.value ? t("refreshing") : t("refresh"))}${ssrRenderAttr("title", refreshing.value ? t("refreshing") : t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
1209
1487
  if (selected.value.size) _push(`<button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch"))}</button>`);
1210
1488
  else _push(`<!---->`);
1211
- _push(`</div>`);
1212
- if (needsPassword.value) _push(`<div class="fl-password"><span>${ssrInterpolate(t("passwordPrompt"))}</span><input${ssrRenderAttr("value", passwordInput.value)} type="password"${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordPlaceholder"))}><button class="fl-btn fl-btn-primary" type="button">${ssrInterpolate(t("open"))}</button></div>`);
1489
+ _push(`</div><div class="fl-row fl-head">`);
1490
+ if (refreshing.value) _push(`<span class="fl-progress" aria-hidden="true"></span>`);
1213
1491
  else _push(`<!---->`);
1214
- _push(`<div class="fl-row fl-head"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
1492
+ _push(`<label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
1215
1493
  ssrRenderList(sortOptions.value, (option) => {
1216
1494
  _push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
1217
1495
  });
1218
1496
  _push(`<!--]--></select></label></span></div>`);
1219
1497
  if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
1220
- else if (errorMsg.value) {
1498
+ else if (rootLock.active) {
1499
+ _push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.error }, "fl-unlock-root"])}"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><label class="fl-unlock-label"${ssrRenderAttr("for", `fl-root-unlock-${__props.path}`)}>${ssrInterpolate(t("passwordPrompt"))}</label>`);
1500
+ if (rootLock.error) _push(`<span id="fl-root-unlock-error" class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(rootLock.error))}</span>`);
1501
+ else _push(`<!---->`);
1502
+ _push(`<span class="fl-unlock"><input${ssrRenderAttr("id", `fl-root-unlock-${__props.path}`)}${ssrRenderAttr("value", rootLock.input)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", __props.path)}${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-invalid", !!rootLock.error)}${ssrRenderAttr("aria-describedby", rootLock.error ? "fl-root-unlock-error" : void 0)}>`);
1503
+ if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", rootLock.remember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
1504
+ else _push(`<!---->`);
1505
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(rootLock.busy || !rootLock.input) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button></span></form>`);
1506
+ } else if (errorMsg.value) {
1221
1507
  _push(`<div class="fl-status fl-error">${ssrInterpolate(t("loadFailed", { message: errorMsg.value }))} `);
1222
1508
  if (/guest/i.test(errorMsg.value)) _push(`<p class="fl-hint">${ssrInterpolate(t("guestHint"))}</p>`);
1223
1509
  else _push(`<!---->`);
@@ -1231,35 +1517,53 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1231
1517
  _push(`<div class="${ssrRenderClass([{
1232
1518
  "fl-row-dir": node.is_dir,
1233
1519
  "fl-row-root": node.isRoot,
1234
- "fl-row-selected": !node.is_dir && selected.value.has(node.path)
1235
- }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
1236
- if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1520
+ "fl-row-selected": !node.is_dir && selected.value.has(node.path),
1521
+ "fl-row-locked": node.locked,
1522
+ "fl-row-prompting": node.locked && node.prompting,
1523
+ "fl-row-error": node.locked && node.prompting && !!node.unlockError
1524
+ }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(node.locked) ? " disabled" : ""}${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
1525
+ if (node.locked) _push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1526
+ else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1237
1527
  else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
1238
1528
  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>`);
1239
1529
  else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
1240
1530
  _push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}">${ssrInterpolate(node.name)}</span><span class="fl-meta">`);
1241
1531
  if (node.is_dir) {
1242
1532
  _push(`<!--[-->`);
1243
- if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(unref(formatSize)(unref(totalSize)(folderFiles(node))))}<!--]-->`);
1533
+ if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(formatTotal(unref(totalSize)(folderFiles(node))))}<!--]-->`);
1244
1534
  else _push(`<!---->`);
1245
1535
  _push(`<!--]-->`);
1246
1536
  } else _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
1247
1537
  _push(`</span></span>`);
1248
1538
  if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))}</span>`);
1249
1539
  else _push(`<!---->`);
1540
+ if (node.locked && node.prompting && node.unlockError) _push(`<span${ssrRenderAttr("id", `fl-unlock-error-${node.path}`)} class="fl-unlock-message" role="alert">${ssrInterpolate(unlockErrorText(node.unlockError))}</span>`);
1541
+ else _push(`<!---->`);
1542
+ if (!node.locked && rememberedPaths.value.has(node.path)) _push(`<button class="fl-remembered" type="button"${ssrRenderAttr("aria-label", `${t("forgetPassword")} ${node.name}`)}${ssrRenderAttr("title", t("forgetPassword"))}><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg> ${ssrInterpolate(t("remembered"))} · ${ssrInterpolate(t("forgetPassword"))}</button>`);
1543
+ else _push(`<!---->`);
1250
1544
  if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
1251
1545
  else _push(`<!---->`);
1252
- _push(`</span><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
1253
- if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1254
- else {
1255
- _push(`<!--[-->`);
1256
- if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
1546
+ _push(`</span>`);
1547
+ if (node.locked && node.prompting) {
1548
+ _push(`<form class="fl-unlock fl-unlock-row"><svg class="fl-unlock-lock" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg><input${ssrRenderAttr("value", node.unlockInput)} type="password" autocomplete="off"${ssrRenderAttr("data-unlock", node.path)}${ssrRenderAttr("placeholder", node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordFor", { name: node.name }))}${ssrRenderAttr("aria-invalid", !!node.unlockError)}${ssrRenderAttr("aria-describedby", node.unlockError ? `fl-unlock-error-${node.path}` : void 0)}>`);
1549
+ if (canRemember.value) _push(`<button class="fl-unlock-remember" type="button"${ssrRenderAttr("aria-pressed", !!node.unlockRemember)}${ssrRenderAttr("aria-label", t("rememberPassword"))}${ssrRenderAttr("title", t("rememberPassword"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"></circle><path d="m11 12 9-9"></path><path d="m17 6 3 3"></path></svg></button>`);
1257
1550
  else _push(`<!---->`);
1258
- _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
1551
+ _push(`<button class="fl-unlock-submit" type="submit"${ssrIncludeBooleanAttr(node.unlockBusy || !node.unlockInput) ? " disabled" : ""}>${ssrInterpolate(t("open"))}</button><button class="fl-unlock-cancel" type="button"${ssrRenderAttr("aria-label", t("cancel"))}${ssrRenderAttr("title", t("cancel"))}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg></button></form>`);
1552
+ } else {
1553
+ _push(`<!--[--><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
1554
+ if (node.locked) _push(`<button class="fl-action fl-arrow-end fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg></button>`);
1555
+ else if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
1556
+ else if (!node.locked) {
1557
+ _push(`<!--[-->`);
1558
+ if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
1559
+ else _push(`<!---->`);
1560
+ _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
1561
+ } else _push(`<!---->`);
1562
+ _push(`</span><!--]-->`);
1259
1563
  }
1260
- _push(`</span></div>`);
1564
+ _push(`</div>`);
1261
1565
  });
1262
- _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(loadedBytes.value) }))}</span></div>`);
1566
+ _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: formatTotal(loadedBytes.value) }))}</span></div>`);
1263
1567
  if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
1264
1568
  else _push(`<!---->`);
1265
1569
  _push(ssrRenderComponent(_sfc_main$1, {