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,5 +1,5 @@
1
1
  import "../style.css";
2
- import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys } from "vue";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
3
3
  // src/client/locales.ts
4
4
  const fileListLocales = {
5
5
  "zh-CN": {
@@ -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 = {}) {
@@ -875,37 +897,112 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
875
897
  };
876
898
  }
877
899
  });
900
+ // src/client/passwords.ts
901
+ /**
902
+ * Folder passwords remembered for the current browser tab.
903
+ *
904
+ * Stored in `sessionStorage`, so they live only in this tab and are gone when
905
+ * it closes. Keyed by OpenList base URL, then by folder path; a remembered
906
+ * folder also covers its sub-folders. Every access is guarded: private mode
907
+ * or blocked storage simply means nothing is remembered.
908
+ */
909
+ const PREFIX = "vitepress-plugin-filelist:passwords:";
910
+ function store() {
911
+ try {
912
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
913
+ } catch {
914
+ return null;
915
+ }
916
+ }
917
+ function read(base) {
918
+ try {
919
+ const raw = store()?.getItem(PREFIX + base);
920
+ const parsed = raw ? JSON.parse(raw) : {};
921
+ return parsed && typeof parsed === "object" ? parsed : {};
922
+ } catch {
923
+ return {};
924
+ }
925
+ }
926
+ function write(base, map) {
927
+ try {
928
+ const target = store();
929
+ if (!target) return;
930
+ if (Object.keys(map).length) target.setItem(PREFIX + base, JSON.stringify(map));
931
+ else target.removeItem(PREFIX + base);
932
+ } catch {}
933
+ }
934
+ function normalise(path) {
935
+ return path.replace(/\/+$/, "") || "/";
936
+ }
937
+ /** Whether `path` is `folder` itself or inside it. */
938
+ function isWithin(path, folder) {
939
+ const p = normalise(path);
940
+ const f = normalise(folder);
941
+ return f === "/" || p === f || p.startsWith(`${f}/`);
942
+ }
943
+ /** The closest remembered folder at or above `path`, if any. */
944
+ function findRememberedPassword(base, path) {
945
+ let best = null;
946
+ for (const [folder, password] of Object.entries(read(base))) {
947
+ if (typeof password !== "string" || !isWithin(path, folder)) continue;
948
+ if (!best || folder.length > best.path.length) best = {
949
+ path: folder,
950
+ password
951
+ };
952
+ }
953
+ return best;
954
+ }
955
+ function rememberFolderPassword(base, path, password) {
956
+ write(base, {
957
+ ...read(base),
958
+ [normalise(path)]: password
959
+ });
960
+ }
961
+ function forgetFolderPassword(base, path) {
962
+ const map = read(base);
963
+ delete map[normalise(path)];
964
+ write(base, map);
965
+ }
966
+ /** Folders with a remembered password, for showing the "remembered" tag. */
967
+ function rememberedFolders(base) {
968
+ return Object.keys(read(base));
969
+ }
878
970
  // src/client/FileList.vue
879
- const _hoisted_1 = { class: "fl-bar" };
880
- const _hoisted_2 = {
971
+ const _hoisted_1 = ["aria-busy"];
972
+ const _hoisted_2 = { class: "fl-bar" };
973
+ const _hoisted_3 = {
881
974
  key: 0,
882
975
  class: "fl-selection",
883
976
  role: "status"
884
977
  };
885
- const _hoisted_3 = { class: "fl-selection-count" };
886
- const _hoisted_4 = { class: "fl-selection-size" };
887
- const _hoisted_5 = {
978
+ const _hoisted_4 = { class: "fl-selection-count" };
979
+ const _hoisted_5 = { class: "fl-selection-size" };
980
+ const _hoisted_6 = {
888
981
  key: 1,
889
982
  class: "fl-title"
890
983
  };
891
- const _hoisted_6 = { class: "fl-title-text" };
892
- const _hoisted_7 = {
984
+ const _hoisted_7 = { class: "fl-title-text" };
985
+ const _hoisted_8 = {
893
986
  key: 0,
894
987
  class: "fl-demo-badge"
895
988
  };
896
- const _hoisted_8 = ["aria-label", "title"];
897
- const _hoisted_9 = { class: "fl-clear-text" };
898
- const _hoisted_10 = { class: "fl-tools" };
899
- const _hoisted_11 = { class: "fl-search" };
900
- const _hoisted_12 = ["placeholder", "aria-label"];
901
- const _hoisted_13 = ["aria-label", "title"];
902
- const _hoisted_14 = ["disabled"];
903
- const _hoisted_15 = {
989
+ const _hoisted_9 = ["aria-label", "title"];
990
+ const _hoisted_10 = { class: "fl-clear-text" };
991
+ const _hoisted_11 = { class: "fl-tools" };
992
+ const _hoisted_12 = { class: "fl-search" };
993
+ const _hoisted_13 = ["placeholder", "aria-label"];
994
+ const _hoisted_14 = [
995
+ "disabled",
996
+ "aria-label",
997
+ "title"
998
+ ];
999
+ const _hoisted_15 = ["disabled"];
1000
+ const _hoisted_16 = { class: "fl-row fl-head" };
1001
+ const _hoisted_17 = {
904
1002
  key: 0,
905
- class: "fl-password"
1003
+ class: "fl-progress",
1004
+ "aria-hidden": "true"
906
1005
  };
907
- const _hoisted_16 = ["placeholder", "aria-label"];
908
- const _hoisted_17 = { class: "fl-row fl-head" };
909
1006
  const _hoisted_18 = { class: "fl-cell fl-cell-check" };
910
1007
  const _hoisted_19 = [
911
1008
  "aria-label",
@@ -937,84 +1034,139 @@ const _hoisted_29 = {
937
1034
  const _hoisted_30 = ["aria-label"];
938
1035
  const _hoisted_31 = ["value"];
939
1036
  const _hoisted_32 = {
940
- key: 1,
1037
+ key: 0,
941
1038
  class: "fl-status"
942
1039
  };
943
- const _hoisted_33 = {
1040
+ const _hoisted_33 = ["for"];
1041
+ const _hoisted_34 = {
1042
+ key: 0,
1043
+ id: "fl-root-unlock-error",
1044
+ class: "fl-unlock-message",
1045
+ role: "alert"
1046
+ };
1047
+ const _hoisted_35 = { class: "fl-unlock" };
1048
+ const _hoisted_36 = [
1049
+ "id",
1050
+ "data-unlock",
1051
+ "placeholder",
1052
+ "aria-invalid",
1053
+ "aria-describedby"
1054
+ ];
1055
+ const _hoisted_37 = [
1056
+ "aria-pressed",
1057
+ "aria-label",
1058
+ "title"
1059
+ ];
1060
+ const _hoisted_38 = ["disabled"];
1061
+ const _hoisted_39 = {
944
1062
  key: 2,
945
1063
  class: "fl-status fl-error"
946
1064
  };
947
- const _hoisted_34 = {
1065
+ const _hoisted_40 = {
948
1066
  key: 0,
949
1067
  class: "fl-hint"
950
1068
  };
951
- const _hoisted_35 = {
1069
+ const _hoisted_41 = {
952
1070
  key: 1,
953
1071
  class: "fl-hint"
954
1072
  };
955
- const _hoisted_36 = {
1073
+ const _hoisted_42 = {
956
1074
  key: 3,
957
1075
  class: "fl-status"
958
1076
  };
959
- const _hoisted_37 = { class: "fl-cell fl-cell-check" };
960
- const _hoisted_38 = [
1077
+ const _hoisted_43 = { class: "fl-cell fl-cell-check" };
1078
+ const _hoisted_44 = [
1079
+ "disabled",
961
1080
  "aria-label",
962
1081
  "checked",
963
1082
  ".indeterminate",
964
1083
  "onChange"
965
1084
  ];
966
- const _hoisted_39 = ["title"];
967
- const _hoisted_40 = [
1085
+ const _hoisted_45 = ["title"];
1086
+ const _hoisted_46 = [
1087
+ "aria-label",
1088
+ "title",
1089
+ "onClick"
1090
+ ];
1091
+ const _hoisted_47 = [
968
1092
  "aria-label",
969
1093
  "aria-expanded",
970
1094
  "onClick"
971
1095
  ];
972
- const _hoisted_41 = {
973
- key: 1,
1096
+ const _hoisted_48 = {
1097
+ key: 2,
974
1098
  class: "fl-arrow-placeholder",
975
1099
  "aria-hidden": "true"
976
1100
  };
977
- const _hoisted_42 = {
978
- key: 2,
1101
+ const _hoisted_49 = {
1102
+ key: 3,
979
1103
  class: "fl-folder-icon",
980
1104
  viewBox: "0 0 24 24",
981
1105
  width: "18",
982
1106
  height: "18",
983
1107
  "aria-hidden": "true"
984
1108
  };
985
- const _hoisted_43 = ["data-kind"];
986
- const _hoisted_44 = { class: "fl-label" };
987
- const _hoisted_45 = ["onClick"];
988
- const _hoisted_46 = { class: "fl-meta" };
989
- const _hoisted_47 = {
990
- key: 4,
1109
+ const _hoisted_50 = ["data-kind"];
1110
+ const _hoisted_51 = { class: "fl-label" };
1111
+ const _hoisted_52 = ["onClick"];
1112
+ const _hoisted_53 = { class: "fl-meta" };
1113
+ const _hoisted_54 = {
1114
+ key: 5,
991
1115
  class: "fl-count"
992
1116
  };
993
- const _hoisted_48 = {
994
- key: 5,
1117
+ const _hoisted_55 = ["id"];
1118
+ const _hoisted_56 = [
1119
+ "aria-label",
1120
+ "title",
1121
+ "onClick"
1122
+ ];
1123
+ const _hoisted_57 = {
1124
+ key: 8,
995
1125
  class: "fl-loading",
996
1126
  "aria-hidden": "true"
997
1127
  };
998
- const _hoisted_49 = { class: "fl-cell fl-cell-size" };
999
- const _hoisted_50 = { class: "fl-cell fl-cell-date" };
1000
- const _hoisted_51 = { class: "fl-cell fl-cell-op" };
1001
- const _hoisted_52 = [
1128
+ const _hoisted_58 = ["onSubmit", "onKeydown"];
1129
+ const _hoisted_59 = [
1130
+ "onUpdate:modelValue",
1131
+ "data-unlock",
1132
+ "placeholder",
1133
+ "aria-label",
1134
+ "aria-invalid",
1135
+ "aria-describedby"
1136
+ ];
1137
+ const _hoisted_60 = [
1138
+ "aria-pressed",
1139
+ "aria-label",
1140
+ "title",
1141
+ "onClick"
1142
+ ];
1143
+ const _hoisted_61 = ["disabled"];
1144
+ const _hoisted_62 = [
1145
+ "aria-label",
1146
+ "title",
1147
+ "onClick"
1148
+ ];
1149
+ const _hoisted_63 = { class: "fl-cell fl-cell-size" };
1150
+ const _hoisted_64 = { class: "fl-cell fl-cell-date" };
1151
+ const _hoisted_65 = { class: "fl-cell fl-cell-op" };
1152
+ const _hoisted_66 = ["aria-label", "onClick"];
1153
+ const _hoisted_67 = [
1002
1154
  "aria-label",
1003
1155
  "aria-expanded",
1004
1156
  "onClick"
1005
1157
  ];
1006
- const _hoisted_53 = [
1158
+ const _hoisted_68 = [
1007
1159
  "aria-label",
1008
1160
  "title",
1009
1161
  "onClick"
1010
1162
  ];
1011
- const _hoisted_54 = [
1163
+ const _hoisted_69 = [
1012
1164
  "aria-label",
1013
1165
  "title",
1014
1166
  "onClick"
1015
1167
  ];
1016
- const _hoisted_55 = { class: "fl-foot" };
1017
- const _hoisted_56 = {
1168
+ const _hoisted_70 = { class: "fl-foot" };
1169
+ const _hoisted_71 = {
1018
1170
  key: 4,
1019
1171
  class: "fl-toast",
1020
1172
  role: "status"
@@ -1032,6 +1184,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1032
1184
  token: { default: "" },
1033
1185
  password: { default: "" },
1034
1186
  title: { default: "" },
1187
+ rememberPassword: {
1188
+ type: Boolean,
1189
+ default: true
1190
+ },
1035
1191
  locale: { default: "zh-CN" },
1036
1192
  labels: {}
1037
1193
  },
@@ -1050,12 +1206,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1050
1206
  }
1051
1207
  const demo = computed(() => !props.base);
1052
1208
  const baseUrl = computed(() => props.base.replace(/\/+$/, ""));
1053
- const activePassword = ref(props.password);
1209
+ const rootEl = ref();
1054
1210
  const roots = ref([]);
1055
1211
  const loadingRoot = ref(false);
1056
1212
  const errorMsg = ref("");
1057
- const needsPassword = ref(false);
1058
- const passwordInput = ref("");
1213
+ const refreshing = ref(false);
1214
+ let refreshSeq = 0;
1215
+ /** Single-path mode: the listed folder itself needs a password. */
1216
+ const rootLock = reactive({
1217
+ active: false,
1218
+ input: "",
1219
+ error: "",
1220
+ remember: false,
1221
+ busy: false
1222
+ });
1223
+ /** Password the visitor typed for the listed folder (single-path mode). */
1224
+ const rootPassword = ref("");
1059
1225
  const keyword = ref("");
1060
1226
  const selected = ref(/* @__PURE__ */ new Set());
1061
1227
  const toast = ref("");
@@ -1079,6 +1245,90 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1079
1245
  function isPasswordError(error) {
1080
1246
  return /password|密码/i.test(errorText(error));
1081
1247
  }
1248
+ function isNetworkError(error) {
1249
+ return /failed to fetch|networkerror|load failed|network request failed/i.test(errorText(error));
1250
+ }
1251
+ /** A failure toast; browser network errors are usually CORS, so say where to look. */
1252
+ function failureMessage(key, error) {
1253
+ const message = t(key, { message: errorText(error) });
1254
+ return isNetworkError(error) ? `${message} ${t("networkHint")}` : message;
1255
+ }
1256
+ /** Total size for display: an empty total reads `0 B`, not `-`. */
1257
+ function formatTotal(bytes) {
1258
+ return bytes > 0 ? formatSize(bytes) : "0 B";
1259
+ }
1260
+ const canRemember = computed(() => props.rememberPassword !== false);
1261
+ const passwordScope = computed(() => baseUrl.value || "demo");
1262
+ /** Passwords typed on this page (not necessarily remembered), so a refresh keeps folders open. */
1263
+ const typedPasswords = /* @__PURE__ */ new Map();
1264
+ function typedPasswordFor(path) {
1265
+ let best = "";
1266
+ let bestPath = "";
1267
+ for (const [folder, password] of typedPasswords) if (isWithin(path, folder) && folder.length > bestPath.length) {
1268
+ best = password;
1269
+ bestPath = folder;
1270
+ }
1271
+ return best;
1272
+ }
1273
+ /** Folders with a remembered password (mirrors sessionStorage so the template reacts). */
1274
+ const rememberedPaths = ref(/* @__PURE__ */ new Set());
1275
+ function syncRemembered() {
1276
+ rememberedPaths.value = new Set(canRemember.value ? rememberedFolders(passwordScope.value) : []);
1277
+ }
1278
+ function storedPassword(path) {
1279
+ return canRemember.value ? findRememberedPassword(passwordScope.value, path) : null;
1280
+ }
1281
+ /**
1282
+ * Password for an item: its own or inherited one first (site-configured or
1283
+ * typed), then one remembered in this tab, then the component's `password`.
1284
+ */
1285
+ function passwordFor(node) {
1286
+ if (node.password) return { password: node.password };
1287
+ const typed = typedPasswordFor(node.path);
1288
+ if (typed) return { password: typed };
1289
+ const stored = storedPassword(node.path);
1290
+ if (stored) return {
1291
+ password: stored.password,
1292
+ rememberedAt: stored.path
1293
+ };
1294
+ return { password: props.password };
1295
+ }
1296
+ function rootPasswordFor() {
1297
+ if (rootPassword.value) return { password: rootPassword.value };
1298
+ const stored = storedPassword(props.path);
1299
+ if (stored) return {
1300
+ password: stored.password,
1301
+ rememberedAt: stored.path
1302
+ };
1303
+ return { password: props.password };
1304
+ }
1305
+ /** A remembered password stopped working: drop it so we do not keep retrying it. */
1306
+ function dropStale(path) {
1307
+ if (!path) return false;
1308
+ forgetFolderPassword(passwordScope.value, path);
1309
+ syncRemembered();
1310
+ return true;
1311
+ }
1312
+ function focusUnlock(path) {
1313
+ nextTick(() => {
1314
+ (rootEl.value?.querySelector(`input[data-unlock="${CSS.escape(path)}"]`))?.focus();
1315
+ });
1316
+ }
1317
+ /** Show the same-row password input; `focus` only for user actions, never on page load. */
1318
+ function startPrompt(node, focus) {
1319
+ node.prompting = true;
1320
+ if (focus) focusUnlock(node.path);
1321
+ }
1322
+ function cancelUnlock(node) {
1323
+ node.prompting = false;
1324
+ node.unlockInput = "";
1325
+ node.unlockError = "";
1326
+ }
1327
+ function unlockErrorText(error) {
1328
+ if (error === "wrong") return t("passwordWrong");
1329
+ if (error === "stale") return t("passwordStale");
1330
+ return "";
1331
+ }
1082
1332
  const DEMO_TREE = {
1083
1333
  "/": [
1084
1334
  {
@@ -1110,6 +1360,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1110
1360
  size: 0,
1111
1361
  is_dir: true,
1112
1362
  modified: "2026-09-01T08:00:00+08:00"
1363
+ },
1364
+ {
1365
+ name: "加密示例(密码 1234)",
1366
+ size: 0,
1367
+ is_dir: true,
1368
+ modified: "2026-10-20T18:00:00+08:00"
1113
1369
  }
1114
1370
  ],
1115
1371
  "/assignments": [{
@@ -1200,6 +1456,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1200
1456
  is_dir: false,
1201
1457
  modified: "2026-01-05T19:02:00+08:00"
1202
1458
  }],
1459
+ "/加密示例(密码 1234)": [{
1460
+ name: "期末答案(教师版).pdf",
1461
+ size: 2621440,
1462
+ is_dir: false,
1463
+ modified: "2026-10-20T18:00:00+08:00"
1464
+ }, {
1465
+ name: "成绩登记表.xlsx",
1466
+ size: 48128,
1467
+ is_dir: false,
1468
+ modified: "2026-10-21T09:12:00+08:00"
1469
+ }],
1203
1470
  "/slides": [{
1204
1471
  name: "第一章 行列式.pptx",
1205
1472
  size: 3145728,
@@ -1212,6 +1479,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1212
1479
  modified: "2026-09-15T13:30:00+08:00"
1213
1480
  }]
1214
1481
  };
1482
+ /** Demo-only folder passwords, so the demo can show the password flow. */
1483
+ const DEMO_PASSWORDS = { "/加密示例(密码 1234)": "1234" };
1215
1484
  class ApiError extends Error {
1216
1485
  code;
1217
1486
  constructor(message, code) {
@@ -1219,16 +1488,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1219
1488
  this.code = code;
1220
1489
  }
1221
1490
  }
1222
- function authHeaders() {
1491
+ /** JSON content type only when there is a body: a bare GET then needs no CORS preflight. */
1492
+ function authHeaders(json) {
1223
1493
  return {
1224
- "Content-Type": "application/json",
1494
+ ...json ? { "Content-Type": "application/json" } : {},
1225
1495
  ...props.token ? { Authorization: props.token } : {}
1226
1496
  };
1227
1497
  }
1228
1498
  async function request(apiPath, body) {
1229
1499
  const response = await fetch(`${baseUrl.value}${apiPath}`, {
1230
1500
  method: body === void 0 ? "GET" : "POST",
1231
- headers: authHeaders(),
1501
+ headers: authHeaders(body !== void 0),
1232
1502
  body: body === void 0 ? void 0 : JSON.stringify(body)
1233
1503
  });
1234
1504
  const payload = await response.json().catch(() => null);
@@ -1236,9 +1506,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1236
1506
  if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
1237
1507
  return payload.data;
1238
1508
  }
1239
- async function listDir(path, password = activePassword.value) {
1509
+ async function listDir(path, password) {
1240
1510
  if (demo.value) {
1241
1511
  await new Promise((resolve) => setTimeout(resolve, 220));
1512
+ const guard = Object.entries(DEMO_PASSWORDS).find(([folder]) => isWithin(path, folder));
1513
+ if (guard && guard[1] !== password) throw new ApiError("password is incorrect or you have no permission", 403);
1242
1514
  return DEMO_TREE[path] ?? [];
1243
1515
  }
1244
1516
  return (await request("/api/fs/list", {
@@ -1268,7 +1540,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1268
1540
  try {
1269
1541
  url = absoluteUrl((await request("/api/fs/get", {
1270
1542
  path: node.path,
1271
- password: node.password || activePassword.value
1543
+ password: passwordFor(node).password
1272
1544
  }))?.raw_url ?? "");
1273
1545
  } catch {
1274
1546
  url = "";
@@ -1326,95 +1598,297 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1326
1598
  const parts = path.split("/").filter(Boolean);
1327
1599
  return parts[parts.length - 1] || path;
1328
1600
  }
1329
- async function loadChildren(node) {
1601
+ /**
1602
+ * Load a folder's children. A password error marks the folder locked instead of
1603
+ * toasting; `prompt` also shows its same-row input (and focuses it).
1604
+ */
1605
+ async function loadChildren(node, prompt = false) {
1330
1606
  if (node.loaded) return true;
1607
+ if (node.locked) {
1608
+ if (prompt) startPrompt(node, prompt === "focus");
1609
+ return false;
1610
+ }
1331
1611
  node.loading = true;
1612
+ const { password, rememberedAt } = passwordFor(node);
1332
1613
  try {
1333
- node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
1614
+ node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, node.password, node.zipPath);
1334
1615
  sortTree(node.children);
1335
1616
  node.loaded = true;
1336
1617
  return true;
1337
1618
  } catch (error) {
1338
- if (isPasswordError(error)) needsPassword.value = true;
1339
- showToast(t("folderFailed", { message: errorText(error) }));
1619
+ if (isPasswordError(error)) {
1620
+ node.locked = true;
1621
+ node.expanded = false;
1622
+ node.unlockError = dropStale(rememberedAt) ? "stale" : "";
1623
+ if (prompt) startPrompt(node, prompt === "focus");
1624
+ } else showToast(failureMessage("folderFailed", error));
1340
1625
  return false;
1341
1626
  } finally {
1342
1627
  node.loading = false;
1343
1628
  }
1344
1629
  }
1345
- async function loadSourceRoot(source) {
1630
+ /** Submit the same-row password for a locked folder (or a locked root of unknown type). */
1631
+ async function submitUnlock(node) {
1632
+ const password = node.unlockInput ?? "";
1633
+ if (!password || node.unlockBusy) return;
1634
+ node.unlockBusy = true;
1635
+ try {
1636
+ if (node.unresolved) {
1637
+ const data = await request("/api/fs/get", {
1638
+ path: node.path,
1639
+ password
1640
+ });
1641
+ const name = node.name;
1642
+ Object.assign(node, data, { name });
1643
+ if (node.is_dir) node.zipPath = [name];
1644
+ else node.zipPath = [data.name || node.zipPath[0]];
1645
+ node.unresolved = false;
1646
+ }
1647
+ if (node.is_dir) {
1648
+ node.children = toNodes(await listDir(node.path, password), node.path, node.depth + 1, password, node.zipPath);
1649
+ sortTree(node.children);
1650
+ node.loaded = true;
1651
+ node.expanded = true;
1652
+ }
1653
+ node.password = password;
1654
+ typedPasswords.set(node.path, password);
1655
+ if (node.unlockRemember && canRemember.value) {
1656
+ rememberFolderPassword(passwordScope.value, node.path, password);
1657
+ syncRemembered();
1658
+ }
1659
+ node.locked = false;
1660
+ node.prompting = false;
1661
+ node.unlockError = "";
1662
+ node.unlockInput = "";
1663
+ node.unlockRemember = false;
1664
+ } catch (error) {
1665
+ if (isPasswordError(error)) {
1666
+ node.unlockError = "wrong";
1667
+ node.unlockInput = "";
1668
+ focusUnlock(node.path);
1669
+ } else showToast(failureMessage("folderFailed", error));
1670
+ } finally {
1671
+ node.unlockBusy = false;
1672
+ }
1673
+ }
1674
+ /** Forget a remembered password: the folder folds up and asks again next time. */
1675
+ function forgetPassword(node) {
1676
+ forgetFolderPassword(passwordScope.value, node.path);
1677
+ typedPasswords.delete(node.path);
1678
+ syncRemembered();
1679
+ node.password = node.configuredPassword || "";
1680
+ node.expanded = false;
1681
+ node.loaded = false;
1682
+ node.children = [];
1683
+ node.locked = false;
1684
+ selected.value = new Set([...selected.value].filter((path) => !isWithin(path, node.path)));
1685
+ for (const key of [...downloadCache.keys()]) if (isWithin(key, node.path)) downloadCache.delete(key);
1686
+ }
1687
+ /** When OpenList cannot tell us, guess from the name: a known file extension means a file. */
1688
+ function looksLikeFile(path) {
1689
+ const name = sourceName(path);
1690
+ return fileKind(name) !== "other" || previewKind(name) !== "none";
1691
+ }
1692
+ async function loadSourceRoot(source, expanded) {
1346
1693
  const path = source.path;
1347
- const password = source.password ?? "";
1694
+ const configured = source.password ?? "";
1348
1695
  let item = {
1349
1696
  name: sourceName(path),
1350
1697
  size: 0,
1351
- is_dir: true,
1698
+ is_dir: !looksLikeFile(path),
1352
1699
  modified: ""
1353
1700
  };
1354
- if (demo.value) {
1355
- if (/\/[^/]+\.[^/]+$/.test(path)) item = {
1356
- ...item,
1357
- is_dir: false,
1358
- modified: "2026-09-01T08:00:00+08:00"
1359
- };
1360
- } else try {
1361
- const data = await request("/api/fs/get", {
1362
- path,
1363
- password: password || activePassword.value
1364
- });
1365
- item = {
1366
- ...item,
1367
- ...data
1368
- };
1369
- } catch {}
1701
+ let locked = false;
1702
+ let unlockError = "";
1703
+ if (!demo.value) {
1704
+ const { password, rememberedAt } = configured ? {
1705
+ password: configured,
1706
+ rememberedAt: void 0
1707
+ } : (() => {
1708
+ const stored = storedPassword(path);
1709
+ return stored ? {
1710
+ password: stored.password,
1711
+ rememberedAt: stored.path
1712
+ } : {
1713
+ password: props.password,
1714
+ rememberedAt: void 0
1715
+ };
1716
+ })();
1717
+ try {
1718
+ const data = await request("/api/fs/get", {
1719
+ path,
1720
+ password
1721
+ });
1722
+ item = {
1723
+ ...item,
1724
+ ...data
1725
+ };
1726
+ } catch (error) {
1727
+ if (isPasswordError(error)) {
1728
+ locked = true;
1729
+ unlockError = dropStale(rememberedAt) ? "stale" : "";
1730
+ }
1731
+ }
1732
+ }
1370
1733
  const name = source.title || item.name || sourceName(path);
1371
1734
  return {
1372
1735
  ...item,
1373
1736
  name,
1374
1737
  path,
1375
1738
  depth: 0,
1376
- expanded: source.expanded ?? props.expanded,
1739
+ expanded,
1377
1740
  loaded: false,
1378
1741
  loading: false,
1379
1742
  children: [],
1380
- password,
1743
+ password: configured,
1744
+ configuredPassword: configured,
1381
1745
  isRoot: true,
1746
+ locked,
1747
+ unresolved: locked,
1748
+ unlockError,
1749
+ prompting: locked && expanded,
1382
1750
  zipPath: [item.is_dir ? name : item.name || sourceName(path)]
1383
1751
  };
1384
1752
  }
1753
+ class RootLockedError extends Error {
1754
+ rememberedAt;
1755
+ constructor(rememberedAt) {
1756
+ super("root locked");
1757
+ this.rememberedAt = rememberedAt;
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Build a fresh tree. `expandedPaths` re-opens those folders (refresh keeps
1762
+ * what was open); without it the configured `expanded` defaults apply.
1763
+ */
1764
+ async function buildTree(expandedPaths) {
1765
+ let nodes;
1766
+ if (props.sources.length) nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source, expandedPaths ? expandedPaths.has(source.path) : source.expanded ?? props.expanded)));
1767
+ else {
1768
+ const { password, rememberedAt } = rootPasswordFor();
1769
+ let items;
1770
+ try {
1771
+ items = await listDir(props.path, password);
1772
+ } catch (error) {
1773
+ if (isPasswordError(error)) throw new RootLockedError(rememberedAt);
1774
+ throw error;
1775
+ }
1776
+ nodes = toNodes(items, props.path, 0, rootPassword.value);
1777
+ sortTree(nodes);
1778
+ }
1779
+ const open = async (list) => {
1780
+ for (const node of list) {
1781
+ if (!node.is_dir || node.locked || !node.expanded && !expandedPaths?.has(node.path)) continue;
1782
+ node.expanded = await loadChildren(node, "show");
1783
+ if (node.expanded && expandedPaths) await open(node.children);
1784
+ }
1785
+ };
1786
+ await open(nodes);
1787
+ return nodes;
1788
+ }
1789
+ /** First load, or a retry after an error: shows the loading state. */
1385
1790
  async function loadRoot() {
1791
+ refreshSeq += 1;
1792
+ refreshing.value = false;
1386
1793
  loadingRoot.value = true;
1387
1794
  errorMsg.value = "";
1388
- needsPassword.value = false;
1389
1795
  try {
1390
- if (props.sources.length) {
1391
- const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
1392
- roots.value = nodes;
1393
- for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
1796
+ roots.value = await buildTree();
1797
+ rootLock.active = false;
1798
+ selected.value = /* @__PURE__ */ new Set();
1799
+ downloadCache.clear();
1800
+ } catch (error) {
1801
+ if (error instanceof RootLockedError) {
1802
+ roots.value = [];
1803
+ rootLock.active = true;
1804
+ rootLock.error = dropStale(error.rememberedAt) ? "stale" : "";
1805
+ } else errorMsg.value = errorText(error);
1806
+ } finally {
1807
+ loadingRoot.value = false;
1808
+ }
1809
+ }
1810
+ function expandedPathsOf(nodes, out = /* @__PURE__ */ new Set()) {
1811
+ for (const node of nodes) if (node.is_dir && node.expanded) {
1812
+ out.add(node.path);
1813
+ expandedPathsOf(node.children, out);
1814
+ }
1815
+ return out;
1816
+ }
1817
+ /**
1818
+ * Refresh keeps the old rows in place (dimmed) until the new tree is ready, then
1819
+ * swaps it in. Open folders stay open; selected files that still exist stay selected.
1820
+ */
1821
+ async function refresh() {
1822
+ if (refreshing.value || loadingRoot.value) return;
1823
+ if (!roots.value.length || errorMsg.value || rootLock.active) {
1824
+ await loadRoot();
1825
+ return;
1826
+ }
1827
+ const seq = ++refreshSeq;
1828
+ refreshing.value = true;
1829
+ try {
1830
+ const fresh = await buildTree(expandedPathsOf(roots.value));
1831
+ if (seq !== refreshSeq) return;
1832
+ roots.value = fresh;
1833
+ const existing = new Set(fresh.flatMap(descendantFiles).map((file) => file.path));
1834
+ selected.value = new Set([...selected.value].filter((path) => existing.has(path)));
1835
+ downloadCache.clear();
1836
+ } catch (error) {
1837
+ if (seq !== refreshSeq) return;
1838
+ if (error instanceof RootLockedError) {
1839
+ roots.value = [];
1840
+ rootLock.active = true;
1841
+ rootLock.error = dropStale(error.rememberedAt) ? "stale" : "wrong";
1394
1842
  selected.value = /* @__PURE__ */ new Set();
1395
- downloadCache.clear();
1396
- return;
1843
+ } else showToast(failureMessage("refreshFailed", error));
1844
+ } finally {
1845
+ if (seq === refreshSeq) refreshing.value = false;
1846
+ }
1847
+ }
1848
+ /** Single-path mode: unlock the listed folder itself. */
1849
+ async function submitRootUnlock() {
1850
+ const password = rootLock.input;
1851
+ if (!password || rootLock.busy) return;
1852
+ rootLock.busy = true;
1853
+ try {
1854
+ const items = await listDir(props.path, password);
1855
+ rootPassword.value = password;
1856
+ if (rootLock.remember && canRemember.value) {
1857
+ rememberFolderPassword(passwordScope.value, props.path, password);
1858
+ syncRemembered();
1397
1859
  }
1398
- const items = await listDir(props.path);
1399
- roots.value = toNodes(items, props.path, 0, activePassword.value);
1400
- sortTree(roots.value);
1860
+ const nodes = toNodes(items, props.path, 0, password);
1861
+ sortTree(nodes);
1862
+ roots.value = nodes;
1863
+ Object.assign(rootLock, {
1864
+ active: false,
1865
+ input: "",
1866
+ error: "",
1867
+ remember: false
1868
+ });
1401
1869
  selected.value = /* @__PURE__ */ new Set();
1402
1870
  downloadCache.clear();
1403
1871
  } catch (error) {
1404
- const message = errorText(error);
1405
- if (isPasswordError(error)) needsPassword.value = true;
1406
- else errorMsg.value = message;
1872
+ if (isPasswordError(error)) {
1873
+ rootLock.error = "wrong";
1874
+ rootLock.input = "";
1875
+ focusUnlock(props.path);
1876
+ } else showToast(failureMessage("loadFailed", error));
1407
1877
  } finally {
1408
- loadingRoot.value = false;
1878
+ rootLock.busy = false;
1409
1879
  }
1410
1880
  }
1411
1881
  async function toggleExpand(node) {
1882
+ if (node.locked) {
1883
+ startPrompt(node, true);
1884
+ return;
1885
+ }
1412
1886
  if (!node.is_dir) return;
1413
1887
  if (node.expanded) {
1414
1888
  node.expanded = false;
1415
1889
  return;
1416
1890
  }
1417
- if (!await loadChildren(node)) return;
1891
+ if (!await loadChildren(node, "focus")) return;
1418
1892
  node.expanded = true;
1419
1893
  }
1420
1894
  const visible = computed(() => {
@@ -1444,18 +1918,33 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1444
1918
  const hit = files.filter((file) => selected.value.has(file.path)).length;
1445
1919
  return hit > 0 && hit < files.length;
1446
1920
  }
1447
- /** Load a directory and all of its sub-directories. */
1921
+ /**
1922
+ * Load a directory and all of its sub-directories. Stops at the first folder
1923
+ * that needs a password and returns it, so nothing is silently left out.
1924
+ */
1448
1925
  async function loadTree(node) {
1449
- if (!node.is_dir) return true;
1450
- if (!await loadChildren(node)) return false;
1451
- for (const child of node.children) if (child.is_dir && !await loadTree(child)) return false;
1452
- return true;
1926
+ if (!node.is_dir) return { ok: true };
1927
+ if (!await loadChildren(node)) return node.locked ? {
1928
+ ok: false,
1929
+ locked: node
1930
+ } : { ok: false };
1931
+ for (const child of node.children) {
1932
+ if (!child.is_dir) continue;
1933
+ const result = await loadTree(child);
1934
+ if (!result.ok) {
1935
+ node.expanded = true;
1936
+ return result;
1937
+ }
1938
+ }
1939
+ return { ok: true };
1453
1940
  }
1454
1941
  async function toggleCheck(node, event) {
1455
1942
  const input = event?.target;
1456
1943
  if (node.is_dir && !isChecked(node)) {
1457
- if (await loadTree(node)) node.expanded = true;
1944
+ const result = await loadTree(node);
1945
+ if (result.ok) node.expanded = true;
1458
1946
  else {
1947
+ if (result.locked) startPrompt(result.locked, true);
1459
1948
  if (input) input.checked = isChecked(node);
1460
1949
  return;
1461
1950
  }
@@ -1628,11 +2117,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1628
2117
  batchBusy.value = false;
1629
2118
  }
1630
2119
  }
1631
- function submitPassword() {
1632
- activePassword.value = passwordInput.value;
1633
- downloadCache.clear();
1634
- loadRoot();
1635
- }
1636
2120
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
1637
2121
  const stickyTop = ref(0);
1638
2122
  function measureStickyTop() {
@@ -1646,6 +2130,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1646
2130
  stickyTop.value = top;
1647
2131
  }
1648
2132
  onMounted(() => {
2133
+ syncRemembered();
1649
2134
  loadRoot();
1650
2135
  loadSiteTitle();
1651
2136
  measureStickyTop();
@@ -1657,11 +2142,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1657
2142
  });
1658
2143
  return (_ctx, _cache) => {
1659
2144
  return openBlock(), createElementBlock("div", {
1660
- class: normalizeClass(["fl-root", { "fl-selecting": selected.value.size > 0 }]),
1661
- style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` })
2145
+ ref_key: "rootEl",
2146
+ ref: rootEl,
2147
+ class: normalizeClass(["fl-root", {
2148
+ "fl-selecting": selected.value.size > 0,
2149
+ "fl-refreshing": refreshing.value
2150
+ }]),
2151
+ style: normalizeStyle({ "--fl-sticky-top-auto": `${stickyTop.value}px` }),
2152
+ "aria-busy": refreshing.value || loadingRoot.value
1662
2153
  }, [
1663
- createElementVNode("div", _hoisted_1, [
1664
- selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_2, [createElementVNode("span", _hoisted_3, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_4, toDisplayString(t("totalSize", { size: unref(formatSize)(selectedBytes.value) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_5, [createElementVNode("span", _hoisted_6, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
2154
+ createElementVNode("div", _hoisted_2, [
2155
+ selected.value.size ? (openBlock(), createElementBlock("div", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(t("selected", { count: selected.value.size })), 1), createElementVNode("span", _hoisted_5, toDisplayString(t("totalSize", { size: formatTotal(selectedBytes.value) })), 1)])) : (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("span", _hoisted_7, toDisplayString(displayTitle.value), 1), demo.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(t("demoBadge")), 1)) : createCommentVNode("v-if", true)])),
1665
2156
  selected.value.size ? (openBlock(), createElementBlock("button", {
1666
2157
  key: 2,
1667
2158
  class: "fl-clear",
@@ -1669,7 +2160,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1669
2160
  "aria-label": t("clearSelection"),
1670
2161
  title: t("clearSelection"),
1671
2162
  onClick: clearSelection
1672
- }, [_cache[9] || (_cache[9] = createElementVNode("svg", {
2163
+ }, [_cache[10] || (_cache[10] = createElementVNode("svg", {
1673
2164
  class: "fl-clear-icon",
1674
2165
  viewBox: "0 0 24 24",
1675
2166
  width: "18",
@@ -1679,8 +2170,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1679
2170
  "stroke-width": "2.2",
1680
2171
  "stroke-linecap": "round",
1681
2172
  "aria-hidden": "true"
1682
- }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)), createElementVNode("span", _hoisted_9, toDisplayString(t("clear")), 1)], 8, _hoisted_8)) : createCommentVNode("v-if", true),
1683
- createElementVNode("div", _hoisted_10, [createElementVNode("label", _hoisted_11, [_cache[10] || (_cache[10] = createElementVNode("svg", {
2173
+ }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)), createElementVNode("span", _hoisted_10, toDisplayString(t("clear")), 1)], 8, _hoisted_9)) : createCommentVNode("v-if", true),
2174
+ createElementVNode("div", _hoisted_11, [createElementVNode("label", _hoisted_12, [_cache[11] || (_cache[11] = createElementVNode("svg", {
1684
2175
  viewBox: "0 0 24 24",
1685
2176
  width: "15",
1686
2177
  height: "15",
@@ -1698,13 +2189,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1698
2189
  type: "text",
1699
2190
  placeholder: t("filter"),
1700
2191
  "aria-label": t("filter")
1701
- }, null, 8, _hoisted_12), [[vModelText, keyword.value]])]), createElementVNode("button", {
1702
- class: "fl-icon-btn",
2192
+ }, null, 8, _hoisted_13), [[vModelText, keyword.value]])]), createElementVNode("button", {
2193
+ class: normalizeClass(["fl-icon-btn fl-refresh", { "fl-spinning": refreshing.value }]),
1703
2194
  type: "button",
1704
- "aria-label": t("refresh"),
1705
- title: t("refresh"),
1706
- onClick: loadRoot
1707
- }, [..._cache[11] || (_cache[11] = [createElementVNode("svg", {
2195
+ disabled: refreshing.value || loadingRoot.value,
2196
+ "aria-label": refreshing.value ? t("refreshing") : t("refresh"),
2197
+ title: refreshing.value ? t("refreshing") : t("refresh"),
2198
+ onClick: refresh
2199
+ }, [..._cache[12] || (_cache[12] = [createElementVNode("svg", {
1708
2200
  viewBox: "0 0 24 24",
1709
2201
  width: "15",
1710
2202
  height: "15",
@@ -1714,14 +2206,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1714
2206
  "stroke-linecap": "round",
1715
2207
  "stroke-linejoin": "round",
1716
2208
  "aria-hidden": "true"
1717
- }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 8, _hoisted_13)]),
2209
+ }, [createElementVNode("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), createElementVNode("path", { d: "M21 4v5h-5" })], -1)])], 10, _hoisted_14)]),
1718
2210
  selected.value.size ? (openBlock(), createElementBlock("button", {
1719
2211
  key: 3,
1720
2212
  class: "fl-btn fl-btn-primary",
1721
2213
  type: "button",
1722
2214
  disabled: batchBusy.value,
1723
2215
  onClick: batchDownload
1724
- }, [_cache[12] || (_cache[12] = createElementVNode("svg", {
2216
+ }, [_cache[13] || (_cache[13] = createElementVNode("svg", {
1725
2217
  viewBox: "0 0 24 24",
1726
2218
  width: "15",
1727
2219
  height: "15",
@@ -1735,24 +2227,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1735
2227
  createElementVNode("path", { d: "M12 3v12" }),
1736
2228
  createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1737
2229
  createElementVNode("path", { d: "M5 21h14" })
1738
- ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_14)) : createCommentVNode("v-if", true)
2230
+ ], -1)), createTextVNode(" " + toDisplayString(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch")), 1)], 8, _hoisted_15)) : createCommentVNode("v-if", true)
1739
2231
  ]),
1740
- needsPassword.value ? (openBlock(), createElementBlock("div", _hoisted_15, [
1741
- createElementVNode("span", null, toDisplayString(t("passwordPrompt")), 1),
1742
- withDirectives(createElementVNode("input", {
1743
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => passwordInput.value = $event),
1744
- type: "password",
1745
- placeholder: t("passwordPlaceholder"),
1746
- "aria-label": t("passwordPlaceholder"),
1747
- onKeyup: withKeys(submitPassword, ["enter"])
1748
- }, null, 40, _hoisted_16), [[vModelText, passwordInput.value]]),
1749
- createElementVNode("button", {
1750
- class: "fl-btn fl-btn-primary",
1751
- type: "button",
1752
- onClick: submitPassword
1753
- }, toDisplayString(t("open")), 1)
1754
- ])) : createCommentVNode("v-if", true),
1755
- createElementVNode("div", _hoisted_17, [
2232
+ createElementVNode("div", _hoisted_16, [
2233
+ refreshing.value ? (openBlock(), createElementBlock("span", _hoisted_17)) : createCommentVNode("v-if", true),
1756
2234
  createElementVNode("label", _hoisted_18, [createElementVNode("input", {
1757
2235
  type: "checkbox",
1758
2236
  "aria-label": t("selectAll"),
@@ -1764,23 +2242,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1764
2242
  class: "fl-sort-btn",
1765
2243
  type: "button",
1766
2244
  "aria-sort": sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0,
1767
- onClick: _cache[2] || (_cache[2] = ($event) => setSort("name"))
2245
+ onClick: _cache[1] || (_cache[1] = ($event) => setSort("name"))
1768
2246
  }, [createTextVNode(toDisplayString(t("columnName")), 1), createElementVNode("span", _hoisted_22, toDisplayString(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : ""), 1)], 8, _hoisted_21)]),
1769
2247
  createElementVNode("span", _hoisted_23, [createElementVNode("button", {
1770
2248
  class: "fl-sort-btn",
1771
2249
  type: "button",
1772
- onClick: _cache[3] || (_cache[3] = ($event) => setSort("size"))
2250
+ onClick: _cache[2] || (_cache[2] = ($event) => setSort("size"))
1773
2251
  }, [createTextVNode(toDisplayString(t("columnSize")), 1), createElementVNode("span", _hoisted_24, toDisplayString(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1774
2252
  createElementVNode("span", _hoisted_25, [createElementVNode("button", {
1775
2253
  class: "fl-sort-btn",
1776
2254
  type: "button",
1777
- onClick: _cache[4] || (_cache[4] = ($event) => setSort("modified"))
2255
+ onClick: _cache[3] || (_cache[3] = ($event) => setSort("modified"))
1778
2256
  }, [createTextVNode(toDisplayString(t("columnDate")), 1), createElementVNode("span", _hoisted_26, toDisplayString(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : ""), 1)])]),
1779
2257
  createElementVNode("span", _hoisted_27, [createElementVNode("label", _hoisted_28, [
1780
2258
  createElementVNode("span", null, toDisplayString(t("sortBy")), 1),
1781
2259
  createElementVNode("span", _hoisted_29, toDisplayString(sortOptions.value.find((option) => option.value === sortValue.value)?.label), 1),
1782
2260
  withDirectives(createElementVNode("select", {
1783
- "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => sortValue.value = $event),
2261
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => sortValue.value = $event),
1784
2262
  "aria-label": t("sortBy")
1785
2263
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(sortOptions.value, (option) => {
1786
2264
  return openBlock(), createElementBlock("option", {
@@ -1790,49 +2268,148 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1790
2268
  }), 128))], 8, _hoisted_30), [[vModelSelect, sortValue.value]])
1791
2269
  ])])
1792
2270
  ]),
1793
- loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_33, [
2271
+ loadingRoot.value ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(t("loading")), 1)) : rootLock.active ? (openBlock(), createElementBlock("form", {
2272
+ key: 1,
2273
+ class: normalizeClass(["fl-unlock-root", { "fl-unlock-error": rootLock.error }]),
2274
+ onSubmit: withModifiers(submitRootUnlock, ["prevent"])
2275
+ }, [
2276
+ _cache[15] || (_cache[15] = createElementVNode("svg", {
2277
+ class: "fl-unlock-lock",
2278
+ viewBox: "0 0 24 24",
2279
+ width: "16",
2280
+ height: "16",
2281
+ fill: "none",
2282
+ stroke: "currentColor",
2283
+ "stroke-width": "2.2",
2284
+ "stroke-linecap": "round",
2285
+ "stroke-linejoin": "round",
2286
+ "aria-hidden": "true"
2287
+ }, [createElementVNode("rect", {
2288
+ x: "5",
2289
+ y: "11",
2290
+ width: "14",
2291
+ height: "10",
2292
+ rx: "2"
2293
+ }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
2294
+ createElementVNode("label", {
2295
+ class: "fl-unlock-label",
2296
+ for: `fl-root-unlock-${__props.path}`
2297
+ }, toDisplayString(t("passwordPrompt")), 9, _hoisted_33),
2298
+ rootLock.error ? (openBlock(), createElementBlock("span", _hoisted_34, toDisplayString(unlockErrorText(rootLock.error)), 1)) : createCommentVNode("v-if", true),
2299
+ createElementVNode("span", _hoisted_35, [
2300
+ withDirectives(createElementVNode("input", {
2301
+ id: `fl-root-unlock-${__props.path}`,
2302
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => rootLock.input = $event),
2303
+ type: "password",
2304
+ autocomplete: "off",
2305
+ "data-unlock": __props.path,
2306
+ placeholder: t("passwordPlaceholder"),
2307
+ "aria-invalid": !!rootLock.error,
2308
+ "aria-describedby": rootLock.error ? "fl-root-unlock-error" : void 0
2309
+ }, null, 8, _hoisted_36), [[vModelText, rootLock.input]]),
2310
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2311
+ key: 0,
2312
+ class: "fl-unlock-remember",
2313
+ type: "button",
2314
+ "aria-pressed": rootLock.remember,
2315
+ "aria-label": t("rememberPassword"),
2316
+ title: t("rememberPassword"),
2317
+ onClick: _cache[6] || (_cache[6] = ($event) => rootLock.remember = !rootLock.remember)
2318
+ }, [..._cache[14] || (_cache[14] = [createElementVNode("svg", {
2319
+ viewBox: "0 0 24 24",
2320
+ width: "16",
2321
+ height: "16",
2322
+ fill: "none",
2323
+ stroke: "currentColor",
2324
+ "stroke-width": "2",
2325
+ "stroke-linecap": "round",
2326
+ "stroke-linejoin": "round",
2327
+ "aria-hidden": "true"
2328
+ }, [
2329
+ createElementVNode("circle", {
2330
+ cx: "8",
2331
+ cy: "15",
2332
+ r: "4"
2333
+ }),
2334
+ createElementVNode("path", { d: "m11 12 9-9" }),
2335
+ createElementVNode("path", { d: "m17 6 3 3" })
2336
+ ], -1)])], 8, _hoisted_37)) : createCommentVNode("v-if", true),
2337
+ createElementVNode("button", {
2338
+ class: "fl-unlock-submit",
2339
+ type: "submit",
2340
+ disabled: rootLock.busy || !rootLock.input
2341
+ }, toDisplayString(t("open")), 9, _hoisted_38)
2342
+ ])
2343
+ ], 34)) : errorMsg.value ? (openBlock(), createElementBlock("div", _hoisted_39, [
1794
2344
  createTextVNode(toDisplayString(t("loadFailed", { message: errorMsg.value })) + " ", 1),
1795
- /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_34, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
1796
- /failed to fetch|networkerror/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_35, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
2345
+ /guest/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_40, toDisplayString(t("guestHint")), 1)) : createCommentVNode("v-if", true),
2346
+ /failed to fetch|networkerror/i.test(errorMsg.value) ? (openBlock(), createElementBlock("p", _hoisted_41, toDisplayString(t("corsHint")), 1)) : createCommentVNode("v-if", true),
1797
2347
  createElementVNode("button", {
1798
2348
  class: "fl-btn",
1799
2349
  type: "button",
1800
2350
  onClick: loadRoot
1801
2351
  }, toDisplayString(t("retry")), 1)
1802
- ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_36, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
2352
+ ])) : visible.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_42, toDisplayString(t("empty")), 1)) : createCommentVNode("v-if", true),
1803
2353
  (openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (node) => {
1804
2354
  return openBlock(), createElementBlock("div", {
1805
2355
  key: node.path,
1806
2356
  class: normalizeClass(["fl-row", {
1807
2357
  "fl-row-dir": node.is_dir,
1808
2358
  "fl-row-root": node.isRoot,
1809
- "fl-row-selected": !node.is_dir && selected.value.has(node.path)
2359
+ "fl-row-selected": !node.is_dir && selected.value.has(node.path),
2360
+ "fl-row-locked": node.locked,
2361
+ "fl-row-prompting": node.locked && node.prompting,
2362
+ "fl-row-error": node.locked && node.prompting && !!node.unlockError
1810
2363
  }]),
1811
2364
  style: normalizeStyle({ "--fl-depth": node.depth })
1812
2365
  }, [
1813
- createElementVNode("label", _hoisted_37, [createElementVNode("input", {
2366
+ createElementVNode("label", _hoisted_43, [createElementVNode("input", {
1814
2367
  type: "checkbox",
2368
+ disabled: node.locked,
1815
2369
  "aria-label": t("selectItem", { name: node.name }),
1816
2370
  checked: isChecked(node),
1817
2371
  ".indeterminate": isIndeterminate(node),
1818
2372
  onChange: ($event) => toggleCheck(node, $event)
1819
- }, null, 40, _hoisted_38)]),
2373
+ }, null, 40, _hoisted_44)]),
1820
2374
  createElementVNode("span", {
1821
2375
  class: "fl-cell fl-cell-name",
1822
2376
  title: node.isRoot ? node.path : void 0
1823
2377
  }, [
1824
- _cache[15] || (_cache[15] = createElementVNode("span", {
2378
+ _cache[20] || (_cache[20] = createElementVNode("span", {
1825
2379
  class: "fl-indent",
1826
2380
  "aria-hidden": "true"
1827
2381
  }, null, -1)),
1828
- node.is_dir ? (openBlock(), createElementBlock("button", {
2382
+ node.locked ? (openBlock(), createElementBlock("button", {
1829
2383
  key: 0,
2384
+ class: "fl-arrow fl-lock",
2385
+ type: "button",
2386
+ "aria-label": t("unlockFolder", { name: node.name }),
2387
+ title: t("unlockFolder", { name: node.name }),
2388
+ onClick: ($event) => toggleExpand(node)
2389
+ }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
2390
+ viewBox: "0 0 24 24",
2391
+ width: "14",
2392
+ height: "14",
2393
+ fill: "none",
2394
+ stroke: "currentColor",
2395
+ "stroke-width": "2.2",
2396
+ "stroke-linecap": "round",
2397
+ "stroke-linejoin": "round",
2398
+ "aria-hidden": "true"
2399
+ }, [createElementVNode("rect", {
2400
+ x: "5",
2401
+ y: "11",
2402
+ width: "14",
2403
+ height: "10",
2404
+ rx: "2"
2405
+ }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_46)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2406
+ key: 1,
1830
2407
  class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
1831
2408
  type: "button",
1832
2409
  "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1833
2410
  "aria-expanded": node.expanded,
1834
2411
  onClick: ($event) => toggleExpand(node)
1835
- }, [..._cache[13] || (_cache[13] = [createElementVNode("svg", {
2412
+ }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1836
2413
  viewBox: "0 0 24 24",
1837
2414
  width: "14",
1838
2415
  height: "14",
@@ -1842,85 +2419,223 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1842
2419
  "stroke-linecap": "round",
1843
2420
  "stroke-linejoin": "round",
1844
2421
  "aria-hidden": "true"
1845
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_40)) : (openBlock(), createElementBlock("span", _hoisted_41)),
1846
- node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_42, [..._cache[14] || (_cache[14] = [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", {
1847
- key: 3,
2422
+ }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_47)) : (openBlock(), createElementBlock("span", _hoisted_48)),
2423
+ node.is_dir ? (openBlock(), createElementBlock("svg", _hoisted_49, [..._cache[18] || (_cache[18] = [createElementVNode("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }, null, -1)])])) : (openBlock(), createElementBlock("span", {
2424
+ key: 4,
1848
2425
  class: "fl-type",
1849
2426
  "data-kind": unref(fileKind)(node.name),
1850
2427
  "aria-hidden": "true"
1851
- }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_43)),
1852
- createElementVNode("span", _hoisted_44, [createElementVNode("span", {
2428
+ }, toDisplayString(unref(extOf)(node.name) || "FILE"), 9, _hoisted_50)),
2429
+ createElementVNode("span", _hoisted_51, [createElementVNode("span", {
1853
2430
  class: normalizeClass(["fl-name", { "fl-name-dir": node.is_dir }]),
1854
- onClick: ($event) => node.is_dir ? toggleExpand(node) : download(node)
1855
- }, toDisplayString(node.name), 11, _hoisted_45), createElementVNode("span", _hoisted_46, [node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("folderItems", { count: folderFiles(node).length })) + " · " + toDisplayString(unref(formatSize)(unref(totalSize)(folderFiles(node)))), 1)], 64)) : createCommentVNode("v-if", true)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64))])]),
1856
- node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(t("folderItems", { count: folderFiles(node).length })), 1)) : createCommentVNode("v-if", true),
1857
- node.loading ? (openBlock(), createElementBlock("span", _hoisted_48, "…")) : createCommentVNode("v-if", true)
1858
- ], 8, _hoisted_39),
1859
- createElementVNode("span", _hoisted_49, toDisplayString(node.is_dir ? "—" : unref(formatSize)(node.size)), 1),
1860
- createElementVNode("span", _hoisted_50, toDisplayString(unref(formatDate)(node.modified)), 1),
1861
- createElementVNode("span", _hoisted_51, [node.is_dir ? (openBlock(), createElementBlock("button", {
1862
- key: 0,
1863
- class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
1864
- type: "button",
1865
- "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
1866
- "aria-expanded": node.expanded,
1867
- onClick: ($event) => toggleExpand(node)
1868
- }, [..._cache[16] || (_cache[16] = [createElementVNode("svg", {
1869
- viewBox: "0 0 24 24",
1870
- width: "18",
1871
- height: "18",
1872
- fill: "none",
1873
- stroke: "currentColor",
1874
- "stroke-width": "2.2",
1875
- "stroke-linecap": "round",
1876
- "stroke-linejoin": "round",
1877
- "aria-hidden": "true"
1878
- }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_52)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2431
+ onClick: ($event) => node.locked || node.is_dir ? toggleExpand(node) : download(node)
2432
+ }, toDisplayString(node.name), 11, _hoisted_52), createElementVNode("span", _hoisted_53, [node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [node.loaded ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(t("folderItems", { count: folderFiles(node).length })) + " · " + toDisplayString(formatTotal(unref(totalSize)(folderFiles(node)))), 1)], 64)) : createCommentVNode("v-if", true)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(unref(formatSize)(node.size)) + " · " + toDisplayString(unref(formatDate)(node.modified)), 1)], 64))])]),
2433
+ node.is_dir && node.loaded ? (openBlock(), createElementBlock("span", _hoisted_54, toDisplayString(t("folderItems", { count: folderFiles(node).length })), 1)) : createCommentVNode("v-if", true),
2434
+ node.locked && node.prompting && node.unlockError ? (openBlock(), createElementBlock("span", {
2435
+ key: 6,
2436
+ id: `fl-unlock-error-${node.path}`,
2437
+ class: "fl-unlock-message",
2438
+ role: "alert"
2439
+ }, toDisplayString(unlockErrorText(node.unlockError)), 9, _hoisted_55)) : createCommentVNode("v-if", true),
2440
+ !node.locked && rememberedPaths.value.has(node.path) ? (openBlock(), createElementBlock("button", {
2441
+ key: 7,
2442
+ class: "fl-remembered",
2443
+ type: "button",
2444
+ "aria-label": `${t("forgetPassword")} ${node.name}`,
2445
+ title: t("forgetPassword"),
2446
+ onClick: ($event) => forgetPassword(node)
2447
+ }, [_cache[19] || (_cache[19] = createElementVNode("svg", {
2448
+ viewBox: "0 0 24 24",
2449
+ width: "11",
2450
+ height: "11",
2451
+ fill: "none",
2452
+ stroke: "currentColor",
2453
+ "stroke-width": "2.2",
2454
+ "stroke-linecap": "round",
2455
+ "stroke-linejoin": "round",
2456
+ "aria-hidden": "true"
2457
+ }, [
2458
+ createElementVNode("circle", {
2459
+ cx: "8",
2460
+ cy: "15",
2461
+ r: "4"
2462
+ }),
2463
+ createElementVNode("path", { d: "m11 12 9-9" }),
2464
+ createElementVNode("path", { d: "m17 6 3 3" })
2465
+ ], -1)), createTextVNode(" " + toDisplayString(t("remembered")) + " · " + toDisplayString(t("forgetPassword")), 1)], 8, _hoisted_56)) : createCommentVNode("v-if", true),
2466
+ node.loading ? (openBlock(), createElementBlock("span", _hoisted_57, "…")) : createCommentVNode("v-if", true)
2467
+ ], 8, _hoisted_45),
2468
+ node.locked && node.prompting ? (openBlock(), createElementBlock("form", {
1879
2469
  key: 0,
1880
- class: "fl-action fl-download",
1881
- type: "button",
1882
- "aria-label": `${t("preview")} ${node.name}`,
1883
- title: t("preview"),
1884
- onClick: ($event) => preview(node)
1885
- }, [..._cache[17] || (_cache[17] = [createElementVNode("svg", {
1886
- viewBox: "0 0 24 24",
1887
- width: "16",
1888
- height: "16",
1889
- fill: "none",
1890
- stroke: "currentColor",
1891
- "stroke-width": "2",
1892
- "stroke-linecap": "round",
1893
- "stroke-linejoin": "round",
1894
- "aria-hidden": "true"
1895
- }, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
1896
- cx: "12",
1897
- cy: "12",
1898
- r: "3"
1899
- })], -1)])], 8, _hoisted_53)) : createCommentVNode("v-if", true), createElementVNode("button", {
1900
- class: "fl-action fl-download",
1901
- type: "button",
1902
- "aria-label": `${t("download")} ${node.name}`,
1903
- title: t("download"),
1904
- onClick: ($event) => download(node)
1905
- }, [..._cache[18] || (_cache[18] = [createElementVNode("svg", {
1906
- viewBox: "0 0 24 24",
1907
- width: "16",
1908
- height: "16",
1909
- fill: "none",
1910
- stroke: "currentColor",
1911
- "stroke-width": "2",
1912
- "stroke-linecap": "round",
1913
- "stroke-linejoin": "round",
1914
- "aria-hidden": "true"
2470
+ class: "fl-unlock fl-unlock-row",
2471
+ onSubmit: withModifiers(($event) => submitUnlock(node), ["prevent"]),
2472
+ onKeydown: withKeys(withModifiers(($event) => cancelUnlock(node), ["prevent"]), ["esc"])
1915
2473
  }, [
1916
- createElementVNode("path", { d: "M12 3v12" }),
1917
- createElementVNode("path", { d: "m7 10 5 5 5-5" }),
1918
- createElementVNode("path", { d: "M5 21h14" })
1919
- ], -1)])], 8, _hoisted_54)], 64))])
2474
+ _cache[23] || (_cache[23] = createElementVNode("svg", {
2475
+ class: "fl-unlock-lock",
2476
+ viewBox: "0 0 24 24",
2477
+ width: "18",
2478
+ height: "18",
2479
+ fill: "none",
2480
+ stroke: "currentColor",
2481
+ "stroke-width": "2.2",
2482
+ "stroke-linecap": "round",
2483
+ "stroke-linejoin": "round",
2484
+ "aria-hidden": "true"
2485
+ }, [createElementVNode("rect", {
2486
+ x: "5",
2487
+ y: "11",
2488
+ width: "14",
2489
+ height: "10",
2490
+ rx: "2"
2491
+ }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)),
2492
+ withDirectives(createElementVNode("input", {
2493
+ "onUpdate:modelValue": ($event) => node.unlockInput = $event,
2494
+ type: "password",
2495
+ autocomplete: "off",
2496
+ "data-unlock": node.path,
2497
+ placeholder: node.unlockError ? unlockErrorText(node.unlockError) : t("passwordPlaceholder"),
2498
+ "aria-label": t("passwordFor", { name: node.name }),
2499
+ "aria-invalid": !!node.unlockError,
2500
+ "aria-describedby": node.unlockError ? `fl-unlock-error-${node.path}` : void 0
2501
+ }, null, 8, _hoisted_59), [[vModelText, node.unlockInput]]),
2502
+ canRemember.value ? (openBlock(), createElementBlock("button", {
2503
+ key: 0,
2504
+ class: "fl-unlock-remember",
2505
+ type: "button",
2506
+ "aria-pressed": !!node.unlockRemember,
2507
+ "aria-label": t("rememberPassword"),
2508
+ title: t("rememberPassword"),
2509
+ onClick: ($event) => node.unlockRemember = !node.unlockRemember
2510
+ }, [..._cache[21] || (_cache[21] = [createElementVNode("svg", {
2511
+ viewBox: "0 0 24 24",
2512
+ width: "16",
2513
+ height: "16",
2514
+ fill: "none",
2515
+ stroke: "currentColor",
2516
+ "stroke-width": "2",
2517
+ "stroke-linecap": "round",
2518
+ "stroke-linejoin": "round",
2519
+ "aria-hidden": "true"
2520
+ }, [
2521
+ createElementVNode("circle", {
2522
+ cx: "8",
2523
+ cy: "15",
2524
+ r: "4"
2525
+ }),
2526
+ createElementVNode("path", { d: "m11 12 9-9" }),
2527
+ createElementVNode("path", { d: "m17 6 3 3" })
2528
+ ], -1)])], 8, _hoisted_60)) : createCommentVNode("v-if", true),
2529
+ createElementVNode("button", {
2530
+ class: "fl-unlock-submit",
2531
+ type: "submit",
2532
+ disabled: node.unlockBusy || !node.unlockInput
2533
+ }, toDisplayString(t("open")), 9, _hoisted_61),
2534
+ createElementVNode("button", {
2535
+ class: "fl-unlock-cancel",
2536
+ type: "button",
2537
+ "aria-label": t("cancel"),
2538
+ title: t("cancel"),
2539
+ onClick: ($event) => cancelUnlock(node)
2540
+ }, [..._cache[22] || (_cache[22] = [createElementVNode("svg", {
2541
+ viewBox: "0 0 24 24",
2542
+ width: "14",
2543
+ height: "14",
2544
+ fill: "none",
2545
+ stroke: "currentColor",
2546
+ "stroke-width": "2.2",
2547
+ "stroke-linecap": "round",
2548
+ "aria-hidden": "true"
2549
+ }, [createElementVNode("path", { d: "M6 6l12 12" }), createElementVNode("path", { d: "M18 6 6 18" })], -1)])], 8, _hoisted_62)
2550
+ ], 40, _hoisted_58)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2551
+ createElementVNode("span", _hoisted_63, toDisplayString(node.is_dir || node.locked ? "—" : unref(formatSize)(node.size)), 1),
2552
+ createElementVNode("span", _hoisted_64, toDisplayString(unref(formatDate)(node.modified)), 1),
2553
+ createElementVNode("span", _hoisted_65, [node.locked ? (openBlock(), createElementBlock("button", {
2554
+ key: 0,
2555
+ class: "fl-action fl-arrow-end fl-lock",
2556
+ type: "button",
2557
+ "aria-label": t("unlockFolder", { name: node.name }),
2558
+ onClick: ($event) => toggleExpand(node)
2559
+ }, [..._cache[24] || (_cache[24] = [createElementVNode("svg", {
2560
+ viewBox: "0 0 24 24",
2561
+ width: "18",
2562
+ height: "18",
2563
+ fill: "none",
2564
+ stroke: "currentColor",
2565
+ "stroke-width": "2.2",
2566
+ "stroke-linecap": "round",
2567
+ "stroke-linejoin": "round",
2568
+ "aria-hidden": "true"
2569
+ }, [createElementVNode("rect", {
2570
+ x: "5",
2571
+ y: "11",
2572
+ width: "14",
2573
+ height: "10",
2574
+ rx: "2"
2575
+ }), createElementVNode("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })], -1)])], 8, _hoisted_66)) : node.is_dir ? (openBlock(), createElementBlock("button", {
2576
+ key: 1,
2577
+ class: normalizeClass(["fl-action fl-arrow-end", { "fl-arrow-open": node.expanded }]),
2578
+ type: "button",
2579
+ "aria-label": t(node.expanded ? "collapse" : "expand", { name: node.name }),
2580
+ "aria-expanded": node.expanded,
2581
+ onClick: ($event) => toggleExpand(node)
2582
+ }, [..._cache[25] || (_cache[25] = [createElementVNode("svg", {
2583
+ viewBox: "0 0 24 24",
2584
+ width: "18",
2585
+ height: "18",
2586
+ fill: "none",
2587
+ stroke: "currentColor",
2588
+ "stroke-width": "2.2",
2589
+ "stroke-linecap": "round",
2590
+ "stroke-linejoin": "round",
2591
+ "aria-hidden": "true"
2592
+ }, [createElementVNode("path", { d: "m9 6 6 6-6 6" })], -1)])], 10, _hoisted_67)) : !node.locked ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
2593
+ key: 0,
2594
+ class: "fl-action fl-download",
2595
+ type: "button",
2596
+ "aria-label": `${t("preview")} ${node.name}`,
2597
+ title: t("preview"),
2598
+ onClick: ($event) => preview(node)
2599
+ }, [..._cache[26] || (_cache[26] = [createElementVNode("svg", {
2600
+ viewBox: "0 0 24 24",
2601
+ width: "16",
2602
+ height: "16",
2603
+ fill: "none",
2604
+ stroke: "currentColor",
2605
+ "stroke-width": "2",
2606
+ "stroke-linecap": "round",
2607
+ "stroke-linejoin": "round",
2608
+ "aria-hidden": "true"
2609
+ }, [createElementVNode("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), createElementVNode("circle", {
2610
+ cx: "12",
2611
+ cy: "12",
2612
+ r: "3"
2613
+ })], -1)])], 8, _hoisted_68)) : createCommentVNode("v-if", true), createElementVNode("button", {
2614
+ class: "fl-action fl-download",
2615
+ type: "button",
2616
+ "aria-label": `${t("download")} ${node.name}`,
2617
+ title: t("download"),
2618
+ onClick: ($event) => download(node)
2619
+ }, [..._cache[27] || (_cache[27] = [createElementVNode("svg", {
2620
+ viewBox: "0 0 24 24",
2621
+ width: "16",
2622
+ height: "16",
2623
+ fill: "none",
2624
+ stroke: "currentColor",
2625
+ "stroke-width": "2",
2626
+ "stroke-linecap": "round",
2627
+ "stroke-linejoin": "round",
2628
+ "aria-hidden": "true"
2629
+ }, [
2630
+ createElementVNode("path", { d: "M12 3v12" }),
2631
+ createElementVNode("path", { d: "m7 10 5 5 5-5" }),
2632
+ createElementVNode("path", { d: "M5 21h14" })
2633
+ ], -1)])], 8, _hoisted_69)], 64)) : createCommentVNode("v-if", true)])
2634
+ ], 64))
1920
2635
  ], 6);
1921
2636
  }), 128)),
1922
- createElementVNode("div", _hoisted_55, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: unref(formatSize)(loadedBytes.value) })), 1)]),
1923
- toast.value ? (openBlock(), createElementBlock("div", _hoisted_56, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
2637
+ createElementVNode("div", _hoisted_70, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("totalSize", { size: formatTotal(loadedBytes.value) })), 1)]),
2638
+ toast.value ? (openBlock(), createElementBlock("div", _hoisted_71, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true),
1924
2639
  createVNode(_sfc_main$1, {
1925
2640
  open: !!previewNode.value,
1926
2641
  file: previewNode.value,
@@ -1933,9 +2648,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1933
2648
  position: previewPosition.value,
1934
2649
  total: previewPosition.value ? previewList.value.length : 0,
1935
2650
  onClose: closePreview,
1936
- onPrev: _cache[6] || (_cache[6] = ($event) => stepPreview(-1)),
1937
- onNext: _cache[7] || (_cache[7] = ($event) => stepPreview(1)),
1938
- onDownload: _cache[8] || (_cache[8] = ($event) => previewNode.value && download(previewNode.value))
2651
+ onPrev: _cache[7] || (_cache[7] = ($event) => stepPreview(-1)),
2652
+ onNext: _cache[8] || (_cache[8] = ($event) => stepPreview(1)),
2653
+ onDownload: _cache[9] || (_cache[9] = ($event) => previewNode.value && download(previewNode.value))
1939
2654
  }, null, 8, [
1940
2655
  "open",
1941
2656
  "file",
@@ -1948,7 +2663,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1948
2663
  "position",
1949
2664
  "total"
1950
2665
  ])
1951
- ], 6);
2666
+ ], 14, _hoisted_1);
1952
2667
  };
1953
2668
  }
1954
2669
  });