@putkoff/abstract-securefiles 0.0.48 → 0.0.49

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.
Files changed (197) hide show
  1. package/dist/ConfirmButton.d.ts +5 -0
  2. package/dist/CopyButton.d.ts +4 -0
  3. package/dist/FolderSelect.d.ts +6 -0
  4. package/dist/SecureFiles.d.ts +3 -0
  5. package/dist/ServerFiles.d.ts +6 -0
  6. package/dist/ShareDialog.d.ts +6 -0
  7. package/dist/UploadPanel.d.ts +6 -0
  8. package/dist/api.d.ts +46 -0
  9. package/dist/index.cjs +537 -1004
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +4 -8
  12. package/dist/index.js +530 -951
  13. package/dist/index.js.map +1 -1
  14. package/dist/types.d.ts +54 -0
  15. package/package.json +15 -11
  16. package/src/ConfirmButton.tsx +35 -0
  17. package/src/CopyButton.tsx +24 -0
  18. package/src/FolderSelect.tsx +68 -0
  19. package/src/SecureFiles.tsx +344 -0
  20. package/src/ServerFiles.tsx +324 -0
  21. package/src/ShareDialog.tsx +148 -0
  22. package/src/UploadPanel.tsx +217 -0
  23. package/src/api.ts +119 -0
  24. package/src/index.ts +4 -8
  25. package/src/styles.css +432 -0
  26. package/src/types.ts +54 -0
  27. package/css.d.ts +0 -2
  28. package/dist/App/SecureFiles.d.ts +0 -4
  29. package/dist/App/index.d.ts +0 -1
  30. package/dist/FileHandlers/FileRow/imports.d.ts +0 -2
  31. package/dist/FileHandlers/FileRow/index.d.ts +0 -1
  32. package/dist/FileHandlers/FileRow/src/index.d.ts +0 -1
  33. package/dist/FileHandlers/FileRow/src/row_utils.d.ts +0 -2
  34. package/dist/FileHandlers/FileTable/imports.d.ts +0 -4
  35. package/dist/FileHandlers/FileTable/index.d.ts +0 -1
  36. package/dist/FileHandlers/FileTable/src/index.d.ts +0 -1
  37. package/dist/FileHandlers/FileTable/src/table_utils.d.ts +0 -2
  38. package/dist/FileHandlers/ShareModal/imports.d.ts +0 -2
  39. package/dist/FileHandlers/ShareModal/index.d.ts +0 -1
  40. package/dist/FileHandlers/ShareModal/src/index.d.ts +0 -1
  41. package/dist/FileHandlers/ShareModal/src/share_utils.d.ts +0 -2
  42. package/dist/FileHandlers/Upload/imports.d.ts +0 -2
  43. package/dist/FileHandlers/Upload/index.d.ts +0 -1
  44. package/dist/FileHandlers/Upload/src/index.d.ts +0 -2
  45. package/dist/FileHandlers/Upload/src/upload_utils.d.ts +0 -2
  46. package/dist/FileHandlers/imports.d.ts +0 -4
  47. package/dist/FileHandlers/index.d.ts +0 -4
  48. package/dist/UI/index.d.ts +0 -28
  49. package/dist/file_utils/imports.d.ts +0 -2
  50. package/dist/file_utils/index.d.ts +0 -1
  51. package/dist/file_utils/src/cookieFetch.d.ts +0 -33
  52. package/dist/file_utils/src/delete_utils/imports.d.ts +0 -1
  53. package/dist/file_utils/src/delete_utils/index.d.ts +0 -1
  54. package/dist/file_utils/src/delete_utils/src/delete_row.d.ts +0 -2
  55. package/dist/file_utils/src/delete_utils/src/delete_select.d.ts +0 -4
  56. package/dist/file_utils/src/delete_utils/src/delete_selected.d.ts +0 -2
  57. package/dist/file_utils/src/delete_utils/src/index.d.ts +0 -3
  58. package/dist/file_utils/src/download_utils/imports.d.ts +0 -2
  59. package/dist/file_utils/src/download_utils/index.d.ts +0 -1
  60. package/dist/file_utils/src/download_utils/src/direct_download.d.ts +0 -3
  61. package/dist/file_utils/src/download_utils/src/download_file.d.ts +0 -6
  62. package/dist/file_utils/src/download_utils/src/download_select.d.ts +0 -2
  63. package/dist/file_utils/src/download_utils/src/index.d.ts +0 -5
  64. package/dist/file_utils/src/download_utils/src/target_downloads.d.ts +0 -4
  65. package/dist/file_utils/src/download_utils/src/trigger_download.d.ts +0 -2
  66. package/dist/file_utils/src/imports.d.ts +0 -2
  67. package/dist/file_utils/src/index.d.ts +0 -6
  68. package/dist/file_utils/src/list_utils/imports.d.ts +0 -1
  69. package/dist/file_utils/src/list_utils/index.d.ts +0 -1
  70. package/dist/file_utils/src/list_utils/src/index.d.ts +0 -1
  71. package/dist/file_utils/src/list_utils/src/list_utils.d.ts +0 -2
  72. package/dist/file_utils/src/select_utils/index.d.ts +0 -1
  73. package/dist/file_utils/src/select_utils/src/index.d.ts +0 -3
  74. package/dist/file_utils/src/select_utils/src/init_select_all.d.ts +0 -2
  75. package/dist/file_utils/src/select_utils/src/sync_header_checkbox.d.ts +0 -5
  76. package/dist/file_utils/src/select_utils/src/toggle_checkboxes.d.ts +0 -2
  77. package/dist/file_utils/src/share_utils/imports.d.ts +0 -1
  78. package/dist/file_utils/src/share_utils/index.d.ts +0 -1
  79. package/dist/file_utils/src/share_utils/src/index.d.ts +0 -1
  80. package/dist/file_utils/src/share_utils/src/share_utils.d.ts +0 -7
  81. package/dist/file_utils/src/upload_utils/imports.d.ts +0 -0
  82. package/dist/file_utils/src/upload_utils/index.d.ts +0 -1
  83. package/dist/file_utils/src/upload_utils/src/index.d.ts +0 -3
  84. package/dist/file_utils/src/upload_utils/src/patch_file_settings.d.ts +0 -2
  85. package/dist/file_utils/src/upload_utils/src/upload_file.d.ts +0 -2
  86. package/dist/file_utils/src/upload_utils/src/upload_files.d.ts +0 -5
  87. package/dist/hooks/imports.d.ts +0 -3
  88. package/dist/hooks/index.d.ts +0 -1
  89. package/dist/hooks/useFiles/imports.d.ts +0 -2
  90. package/dist/hooks/useFiles/index.d.ts +0 -1
  91. package/dist/hooks/useFiles/src/deleteFile.d.ts +0 -3
  92. package/dist/hooks/useFiles/src/index.d.ts +0 -6
  93. package/dist/hooks/useFiles/src/load.d.ts +0 -3
  94. package/dist/hooks/useFiles/src/patchShareSettings.d.ts +0 -3
  95. package/dist/hooks/useFiles/src/toggleShare.d.ts +0 -3
  96. package/dist/hooks/useFiles/src/useFiles.d.ts +0 -13
  97. package/dist/hooks/useFiles/src/useSaveShareSettings.d.ts +0 -7
  98. package/dist/imports.d.ts +0 -6
  99. package/dist/index.css +0 -1
  100. package/dist/styles/index.d.ts +0 -1
  101. package/dist/types/index.d.ts +0 -1
  102. package/dist/types/src/index.d.ts +0 -1
  103. package/dist/types/src/types.d.ts +0 -75
  104. package/index.ts +0 -1
  105. package/postcss.config.cjs +0 -8
  106. package/rollup.config.js +0 -38
  107. package/rollup.config.mjs +0 -35
  108. package/src/App/SecureFiles.tsx +0 -45
  109. package/src/App/index.ts +0 -1
  110. package/src/FileHandlers/FileRow/imports.ts +0 -2
  111. package/src/FileHandlers/FileRow/index.ts +0 -1
  112. package/src/FileHandlers/FileRow/src/index.ts +0 -1
  113. package/src/FileHandlers/FileRow/src/row_utils.tsx +0 -143
  114. package/src/FileHandlers/FileTable/imports.ts +0 -15
  115. package/src/FileHandlers/FileTable/index.ts +0 -1
  116. package/src/FileHandlers/FileTable/src/index.ts +0 -1
  117. package/src/FileHandlers/FileTable/src/table_utils.tsx +0 -251
  118. package/src/FileHandlers/ShareModal/imports.ts +0 -2
  119. package/src/FileHandlers/ShareModal/index.ts +0 -1
  120. package/src/FileHandlers/ShareModal/src/index.ts +0 -1
  121. package/src/FileHandlers/ShareModal/src/share_utils.tsx +0 -155
  122. package/src/FileHandlers/Upload/imports.ts +0 -3
  123. package/src/FileHandlers/Upload/index.ts +0 -1
  124. package/src/FileHandlers/Upload/src/index.ts +0 -2
  125. package/src/FileHandlers/Upload/src/upload_utils.tsx +0 -369
  126. package/src/FileHandlers/imports.ts +0 -17
  127. package/src/FileHandlers/index.ts +0 -4
  128. package/src/UI/index.tsx +0 -122
  129. package/src/custom.d.ts +0 -7
  130. package/src/file_utils/imports.ts +0 -2
  131. package/src/file_utils/index.ts +0 -1
  132. package/src/file_utils/src/cookieFetch.ts +0 -109
  133. package/src/file_utils/src/delete_utils/imports.ts +0 -1
  134. package/src/file_utils/src/delete_utils/index.ts +0 -1
  135. package/src/file_utils/src/delete_utils/src/delete_row.ts +0 -11
  136. package/src/file_utils/src/delete_utils/src/delete_select.ts +0 -10
  137. package/src/file_utils/src/delete_utils/src/delete_selected.ts +0 -15
  138. package/src/file_utils/src/delete_utils/src/index.ts +0 -3
  139. package/src/file_utils/src/download_utils/imports.ts +0 -3
  140. package/src/file_utils/src/download_utils/index.ts +0 -1
  141. package/src/file_utils/src/download_utils/src/direct_download.ts +0 -22
  142. package/src/file_utils/src/download_utils/src/download_file.ts +0 -25
  143. package/src/file_utils/src/download_utils/src/download_select.ts +0 -25
  144. package/src/file_utils/src/download_utils/src/index.ts +0 -5
  145. package/src/file_utils/src/download_utils/src/target_downloads.ts +0 -21
  146. package/src/file_utils/src/download_utils/src/trigger_download.ts +0 -14
  147. package/src/file_utils/src/imports.ts +0 -3
  148. package/src/file_utils/src/index.ts +0 -6
  149. package/src/file_utils/src/list_utils/imports.ts +0 -2
  150. package/src/file_utils/src/list_utils/index.ts +0 -1
  151. package/src/file_utils/src/list_utils/src/index.ts +0 -1
  152. package/src/file_utils/src/list_utils/src/list_utils.ts +0 -28
  153. package/src/file_utils/src/select_utils/index.ts +0 -1
  154. package/src/file_utils/src/select_utils/src/index.ts +0 -3
  155. package/src/file_utils/src/select_utils/src/init_select_all.ts +0 -37
  156. package/src/file_utils/src/select_utils/src/sync_header_checkbox.ts +0 -18
  157. package/src/file_utils/src/select_utils/src/toggle_checkboxes.ts +0 -11
  158. package/src/file_utils/src/share_utils/imports.ts +0 -1
  159. package/src/file_utils/src/share_utils/index.ts +0 -1
  160. package/src/file_utils/src/share_utils/src/index.ts +0 -1
  161. package/src/file_utils/src/share_utils/src/share_utils.ts +0 -20
  162. package/src/file_utils/src/upload_utils/imports.ts +0 -1
  163. package/src/file_utils/src/upload_utils/index.ts +0 -1
  164. package/src/file_utils/src/upload_utils/src/index.ts +0 -3
  165. package/src/file_utils/src/upload_utils/src/patch_file_settings.ts +0 -20
  166. package/src/file_utils/src/upload_utils/src/upload_file.ts +0 -27
  167. package/src/file_utils/src/upload_utils/src/upload_files.ts +0 -19
  168. package/src/hooks/imports.ts +0 -3
  169. package/src/hooks/index.ts +0 -1
  170. package/src/hooks/useFiles/imports.ts +0 -3
  171. package/src/hooks/useFiles/index.ts +0 -1
  172. package/src/hooks/useFiles/src/deleteFile.ts +0 -23
  173. package/src/hooks/useFiles/src/index.ts +0 -6
  174. package/src/hooks/useFiles/src/load.ts +0 -39
  175. package/src/hooks/useFiles/src/logs/clipit_logs.log +0 -0
  176. package/src/hooks/useFiles/src/logs/routes_bp.log +0 -0
  177. package/src/hooks/useFiles/src/patchShareSettings.ts +0 -27
  178. package/src/hooks/useFiles/src/toggleShare.ts +0 -28
  179. package/src/hooks/useFiles/src/useFiles.ts +0 -35
  180. package/src/hooks/useFiles/src/useSaveShareSettings.ts +0 -31
  181. package/src/imports.ts +0 -18
  182. package/src/styles/SecureFiles/App.css +0 -42
  183. package/src/styles/SecureFiles/index.css +0 -10
  184. package/src/styles/SecureFiles/logs/clipit_logs.log +0 -0
  185. package/src/styles/SecureFiles/logs/routes_bp.log +0 -0
  186. package/src/styles/SecureFiles/tailwind.css +0 -3
  187. package/src/styles/SecureFiles/window-manager.css +0 -283
  188. package/src/styles/SecureFilesStyles.module.css +0 -287
  189. package/src/styles/index.ts +0 -1
  190. package/src/types/index.ts +0 -1
  191. package/src/types/src/index.ts +0 -1
  192. package/src/types/src/types.ts +0 -88
  193. package/tailwind.config.cjs +0 -7
  194. package/testit.mjs +0 -11
  195. package/testit.ts +0 -3
  196. package/tsconfig.json +0 -27
  197. /package/src/{global.d.ts → css.d.ts} +0 -0
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@putkoff/abstract-securefiles",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "description": "A reusable Secure file system",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/index.d.ts",
10
11
  "import": "./dist/index.js",
11
12
  "require": "./dist/index.cjs"
12
13
  },
13
14
  "./package.json": "./package.json"
14
15
  },
15
16
  "peerDependencies": {
16
- "react": "^18.3.1",
17
- "react-dom": "^17.0.0 || ^18.0.0"
17
+ "react": "^18.0.0",
18
+ "react-dom": "^18.0.0"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@putkoff/shared-scripts": "file:../../shared-scripts",
@@ -30,19 +31,14 @@
30
31
  "tailwindcss": "^4.1.11",
31
32
  "tsup": "^8.5.0",
32
33
  "typescript": "^5.8.3",
33
- "undici-types": "^7.11.0"
34
- },
35
- "dependencies": {
36
- "@heroicons/react": "^2.2.0",
37
- "@putkoff/abstract-apis": "0.0.472",
38
- "@putkoff/abstract-files": "0.0.316",
39
- "@putkoff/abstract-utilities": "1.0.158",
34
+ "undici-types": "^7.11.0",
40
35
  "@rollup/plugin-commonjs": "^28.0.6",
41
36
  "@rollup/plugin-node-resolve": "^16.0.1",
42
37
  "@tailwindcss/postcss": "^4.1.11",
43
38
  "rollup-plugin-dts": "^6.2.1",
44
39
  "tslib": "^2.8.1"
45
40
  },
41
+ "dependencies": {},
46
42
  "scripts": {
47
43
  "build": "tsc --emitDeclarationOnly && rollup -c",
48
44
  "test": "jest",
@@ -50,5 +46,13 @@
50
46
  },
51
47
  "type": "module",
52
48
  "author": "putkoff",
53
- "license": "MIT"
49
+ "license": "MIT",
50
+ "sideEffects": [
51
+ "*.css"
52
+ ],
53
+ "files": [
54
+ "dist",
55
+ "src",
56
+ "README.md"
57
+ ]
54
58
  }
@@ -0,0 +1,35 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ export function ConfirmButton({
4
+ label,
5
+ onConfirm,
6
+ disabled,
7
+ }: {
8
+ label: string;
9
+ onConfirm: () => void;
10
+ disabled?: boolean;
11
+ }) {
12
+ const [armed, setArmed] = useState(false);
13
+ useEffect(() => {
14
+ if (!armed) return;
15
+ const t = setTimeout(() => setArmed(false), 3000);
16
+ return () => clearTimeout(t);
17
+ }, [armed]);
18
+ return (
19
+ <button
20
+ type="button"
21
+ className={`sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`}
22
+ disabled={disabled}
23
+ onClick={() => {
24
+ if (armed) {
25
+ setArmed(false);
26
+ onConfirm();
27
+ } else {
28
+ setArmed(true);
29
+ }
30
+ }}
31
+ >
32
+ {armed ? "Confirm" : label}
33
+ </button>
34
+ );
35
+ }
@@ -0,0 +1,24 @@
1
+ import React, { useState } from "react";
2
+
3
+ export function CopyButton({ text, label = "Copy link" }: { text: string; label?: string }) {
4
+ const [copied, setCopied] = useState(false);
5
+ const copy = async () => {
6
+ try {
7
+ await navigator.clipboard.writeText(text);
8
+ } catch {
9
+ const ta = document.createElement("textarea");
10
+ ta.value = text;
11
+ document.body.appendChild(ta);
12
+ ta.select();
13
+ document.execCommand("copy");
14
+ ta.remove();
15
+ }
16
+ setCopied(true);
17
+ setTimeout(() => setCopied(false), 1500);
18
+ };
19
+ return (
20
+ <button type="button" className="sf-btn" onClick={copy}>
21
+ {copied ? "Copied" : label}
22
+ </button>
23
+ );
24
+ }
@@ -0,0 +1,68 @@
1
+ import React, { useState } from "react";
2
+
3
+ const NEW_FOLDER = "__new__";
4
+
5
+ export function FolderSelect({
6
+ label,
7
+ folders,
8
+ value,
9
+ onChange,
10
+ }: {
11
+ label: string;
12
+ folders: string[];
13
+ value: string;
14
+ onChange: (folder: string) => void;
15
+ }) {
16
+ const [creating, setCreating] = useState(false);
17
+ const [draft, setDraft] = useState("");
18
+ const [extra, setExtra] = useState<string[]>([]);
19
+ const options = [...new Set([...folders, ...extra, ...(value ? [value] : [])])].sort();
20
+
21
+ const accept = () => {
22
+ const clean = draft.trim().replace(/^\/+|\/+$/g, "");
23
+ if (clean) {
24
+ setExtra((e) => (e.includes(clean) ? e : [...e, clean]));
25
+ onChange(clean);
26
+ }
27
+ setCreating(false);
28
+ setDraft("");
29
+ };
30
+
31
+ return (
32
+ <div className="sf-field">
33
+ <span>{label}</span>
34
+ {creating ? (
35
+ <div className="sf-row">
36
+ <input
37
+ className="sf-input"
38
+ autoFocus
39
+ value={draft}
40
+ placeholder="new-folder-name (use / for subfolders)"
41
+ onChange={(e) => setDraft(e.target.value)}
42
+ onKeyDown={(e) => {
43
+ if (e.key === "Enter") accept();
44
+ if (e.key === "Escape") setCreating(false);
45
+ }}
46
+ />
47
+ <button type="button" className="sf-btn" onClick={accept}>
48
+ OK
49
+ </button>
50
+ </div>
51
+ ) : (
52
+ <select
53
+ className="sf-input"
54
+ value={value}
55
+ onChange={(e) => (e.target.value === NEW_FOLDER ? setCreating(true) : onChange(e.target.value))}
56
+ >
57
+ <option value="">/ (top level)</option>
58
+ {options.map((d) => (
59
+ <option key={d} value={d}>
60
+ {d}/
61
+ </option>
62
+ ))}
63
+ <option value={NEW_FOLDER}>+ Create new directory…</option>
64
+ </select>
65
+ )}
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,344 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { createApi } from "./api";
3
+ import type { SecureFile, SecureFilesProps, ShareUpdate } from "./types";
4
+ import { UploadPanel } from "./UploadPanel";
5
+ import { ShareDialog } from "./ShareDialog";
6
+ import { CopyButton } from "./CopyButton";
7
+ import { ConfirmButton } from "./ConfirmButton";
8
+ import { ServerFiles } from "./ServerFiles";
9
+ import "./styles.css";
10
+
11
+ interface FolderNode {
12
+ name: string;
13
+ path: string;
14
+ folders: Map<string, FolderNode>;
15
+ files: SecureFile[];
16
+ }
17
+
18
+ function buildTree(files: SecureFile[]): FolderNode {
19
+ const root: FolderNode = { name: "", path: "", folders: new Map(), files: [] };
20
+ for (const f of files) {
21
+ const parts = f.filepath.split("/");
22
+ let node = root;
23
+ parts.slice(0, -1).forEach((part, i) => {
24
+ if (!node.folders.has(part)) {
25
+ node.folders.set(part, { name: part, path: parts.slice(0, i + 1).join("/"), folders: new Map(), files: [] });
26
+ }
27
+ node = node.folders.get(part)!;
28
+ });
29
+ node.files.push(f);
30
+ }
31
+ return root;
32
+ }
33
+
34
+ function allFiles(node: FolderNode): SecureFile[] {
35
+ return [...node.files, ...[...node.folders.values()].flatMap(allFiles)];
36
+ }
37
+
38
+ export default function SecureFiles({ apiBase }: SecureFilesProps) {
39
+ const api = useMemo(() => createApi(apiBase), [apiBase]);
40
+ const [files, setFiles] = useState<SecureFile[]>([]);
41
+ const [user, setUser] = useState<string | null>(null);
42
+ const [isAdmin, setIsAdmin] = useState(false);
43
+ const [loading, setLoading] = useState(true);
44
+ const [error, setError] = useState<string | null>(null);
45
+ const [notice, setNotice] = useState<string | null>(null);
46
+ const [search, setSearch] = useState("");
47
+ const [selected, setSelected] = useState<Set<number>>(new Set());
48
+ const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
49
+ const [editing, setEditing] = useState<SecureFile | null>(null);
50
+ const [busyIds, setBusyIds] = useState<Set<number>>(new Set());
51
+
52
+ const load = useCallback(async () => {
53
+ setError(null);
54
+ try {
55
+ const data = await api.list();
56
+ setUser(data.user_name);
57
+ setIsAdmin(data.is_admin);
58
+ setFiles(data.files);
59
+ setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
60
+ } catch (err) {
61
+ setError(err instanceof Error ? err.message : String(err));
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ }, [api]);
66
+
67
+ useEffect(() => {
68
+ load();
69
+ const onVisible = () => document.visibilityState === "visible" && load();
70
+ document.addEventListener("visibilitychange", onVisible);
71
+ window.addEventListener("focus", onVisible);
72
+ return () => {
73
+ document.removeEventListener("visibilitychange", onVisible);
74
+ window.removeEventListener("focus", onVisible);
75
+ };
76
+ }, [load]);
77
+
78
+ const run = async (ids: number[], action: () => Promise<unknown>) => {
79
+ setBusyIds((s) => new Set([...s, ...ids]));
80
+ setNotice(null);
81
+ try {
82
+ await action();
83
+ } catch (err) {
84
+ setNotice(err instanceof Error ? err.message : String(err));
85
+ } finally {
86
+ setBusyIds((s) => new Set([...s].filter((id) => !ids.includes(id))));
87
+ }
88
+ };
89
+
90
+ const replaceFile = (file: SecureFile) => setFiles((fs) => fs.map((f) => (f.id === file.id ? file : f)));
91
+
92
+ const saveShare = async (update: ShareUpdate) => {
93
+ const { file } = await api.updateShare(update);
94
+ replaceFile(file);
95
+ };
96
+
97
+ const download = (items: SecureFile[]) => {
98
+ const ready = items.filter((f) => f.available);
99
+ if (!ready.length) return;
100
+ return run(
101
+ ready.map((f) => f.id),
102
+ () => api.download(ready.map((f) => f.id), ready.length === 1 ? ready[0].filename : "downloads.zip")
103
+ );
104
+ };
105
+
106
+ const setShared = (items: SecureFile[], shareable: boolean) =>
107
+ run(
108
+ items.map((f) => f.id),
109
+ async () => {
110
+ for (const f of items) await saveShare({ id: f.id, shareable });
111
+ }
112
+ );
113
+
114
+ const remove = (items: SecureFile[]) =>
115
+ run(
116
+ items.map((f) => f.id),
117
+ async () => {
118
+ const res = await api.remove(items.map((f) => f.id));
119
+ if (res.errors.length) setNotice(`${res.errors.length} file(s) could not be deleted`);
120
+ await load();
121
+ }
122
+ );
123
+
124
+ const filtered = useMemo(() => {
125
+ const q = search.trim().toLowerCase();
126
+ return q ? files.filter((f) => f.filepath.toLowerCase().includes(q)) : files;
127
+ }, [files, search]);
128
+
129
+ const tree = useMemo(() => buildTree(filtered), [filtered]);
130
+
131
+ const folders = useMemo(() => {
132
+ const set = new Set<string>();
133
+ for (const f of files) {
134
+ const parts = f.filepath.split("/").slice(0, -1);
135
+ parts.forEach((_, i) => set.add(parts.slice(0, i + 1).join("/")));
136
+ }
137
+ return [...set];
138
+ }, [files]);
139
+
140
+ const selectedFiles = files.filter((f) => selected.has(f.id));
141
+ const toggle = (ids: number[], on: boolean) =>
142
+ setSelected((prev) => {
143
+ const next = new Set(prev);
144
+ ids.forEach((id) => (on ? next.add(id) : next.delete(id)));
145
+ return next;
146
+ });
147
+ const toggleFolder = (path: string) =>
148
+ setCollapsed((c) => {
149
+ const next = new Set(c);
150
+ next.has(path) ? next.delete(path) : next.add(path);
151
+ return next;
152
+ });
153
+ const allVisibleSelected = filtered.length > 0 && filtered.every((f) => selected.has(f.id));
154
+ const searching = search.trim().length > 0;
155
+
156
+ const renderFile = (f: SecureFile, depth: number) => {
157
+ const busy = busyIds.has(f.id);
158
+ return (
159
+ <tr key={f.id} className={busy ? "sf-busy" : undefined}>
160
+ <td className="sf-col-check">
161
+ <input
162
+ type="checkbox"
163
+ aria-label={`Select ${f.filename}`}
164
+ checked={selected.has(f.id)}
165
+ onChange={(e) => toggle([f.id], e.target.checked)}
166
+ />
167
+ </td>
168
+ <td style={{ paddingLeft: 10 + depth * 20 }}>
169
+ <span className={f.available ? undefined : "sf-strike"}>{f.filename}</span>
170
+ {f.linked && (
171
+ <span className="sf-tag" title={f.source_path ?? undefined}>
172
+ linked
173
+ </span>
174
+ )}
175
+ {!f.available && (
176
+ <span
177
+ className="sf-tag sf-tag--warn"
178
+ title={f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk"}
179
+ >
180
+ missing
181
+ </span>
182
+ )}
183
+ </td>
184
+ <td>
185
+ <button
186
+ type="button"
187
+ className={`sf-pill${f.shareable ? " sf-pill--on" : ""}`}
188
+ disabled={busy || !f.available}
189
+ onClick={() => setShared([f], !f.shareable)}
190
+ title={f.shareable ? "Click to stop sharing" : "Click to share"}
191
+ >
192
+ {f.shareable ? "Shared" : "Private"}
193
+ </button>
194
+ {f.shareable && f.has_password && <span title="Password protected"> 🔒</span>}
195
+ </td>
196
+ <td>
197
+ {f.download_count} / {f.download_limit ?? "∞"}
198
+ </td>
199
+ <td className="sf-col-actions">
200
+ <div className="sf-row sf-row--end">
201
+ <button type="button" className="sf-btn" disabled={busy || !f.available} onClick={() => download([f])}>
202
+ Download
203
+ </button>
204
+ {f.shareable && f.available && <CopyButton text={f.share_url} />}
205
+ <button
206
+ type="button"
207
+ className="sf-icon-btn"
208
+ aria-label={`Share settings for ${f.filename}`}
209
+ title="Share settings"
210
+ onClick={() => setEditing(f)}
211
+ >
212
+ ⚙️
213
+ </button>
214
+ <ConfirmButton label={f.linked ? "Unlink" : "Delete"} disabled={busy} onConfirm={() => remove([f])} />
215
+ </div>
216
+ </td>
217
+ </tr>
218
+ );
219
+ };
220
+
221
+ const renderFolder = (node: FolderNode, depth: number): React.ReactNode[] => {
222
+ const rows: React.ReactNode[] = [];
223
+ const subFolders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name));
224
+ const ownFiles = [...node.files].sort((a, b) => a.filename.localeCompare(b.filename));
225
+ for (const folder of subFolders) {
226
+ const contents = allFiles(folder);
227
+ const ids = contents.map((f) => f.id);
228
+ const open = searching || !collapsed.has(folder.path);
229
+ rows.push(
230
+ <tr key={`dir:${folder.path}`} className="sf-dir">
231
+ <td className="sf-col-check">
232
+ <input
233
+ type="checkbox"
234
+ aria-label={`Select folder ${folder.path}`}
235
+ checked={ids.length > 0 && ids.every((id) => selected.has(id))}
236
+ onChange={(e) => toggle(ids, e.target.checked)}
237
+ />
238
+ </td>
239
+ <td colSpan={4} style={{ paddingLeft: 10 + depth * 20 }}>
240
+ <button type="button" className="sf-dir__toggle" onClick={() => toggleFolder(folder.path)}>
241
+ {open ? "▾" : "▸"} 📁 {folder.name} <span className="sf-muted">({contents.length})</span>
242
+ </button>
243
+ </td>
244
+ </tr>
245
+ );
246
+ if (open) rows.push(...renderFolder(folder, depth + 1));
247
+ }
248
+ rows.push(...ownFiles.map((f) => renderFile(f, depth)));
249
+ return rows;
250
+ };
251
+
252
+ return (
253
+ <div className="sf-root">
254
+ <header className="sf-head">
255
+ <h2>Secure Files</h2>
256
+ {user && <span className="sf-muted">Signed in as {user}</span>}
257
+ </header>
258
+
259
+ <UploadPanel api={api} folders={folders} onUploaded={load} />
260
+
261
+ {isAdmin && <ServerFiles api={api} folders={folders} onLinked={load} />}
262
+
263
+ <section className="sf-toolbar">
264
+ <input
265
+ className="sf-input sf-search"
266
+ type="search"
267
+ placeholder="Search files…"
268
+ value={search}
269
+ onChange={(e) => setSearch(e.target.value)}
270
+ />
271
+ <div className="sf-row">
272
+ <span className="sf-muted">{selected.size} selected</span>
273
+ {folders.length > 0 && (
274
+ <>
275
+ <button type="button" className="sf-btn" onClick={() => setCollapsed(new Set())}>
276
+ Expand all
277
+ </button>
278
+ <button type="button" className="sf-btn" onClick={() => setCollapsed(new Set(folders))}>
279
+ Collapse all
280
+ </button>
281
+ </>
282
+ )}
283
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => download(selectedFiles)}>
284
+ Download
285
+ </button>
286
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => setShared(selectedFiles, true)}>
287
+ Share
288
+ </button>
289
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => setShared(selectedFiles, false)}>
290
+ Unshare
291
+ </button>
292
+ <ConfirmButton label="Delete" disabled={!selected.size} onConfirm={() => remove(selectedFiles)} />
293
+ </div>
294
+ </section>
295
+
296
+ {notice && (
297
+ <p className="sf-error" role="alert">
298
+ {notice}
299
+ </p>
300
+ )}
301
+
302
+ {loading ? (
303
+ <p className="sf-muted">Loading…</p>
304
+ ) : error ? (
305
+ <p className="sf-error" role="alert">
306
+ {error}
307
+ </p>
308
+ ) : files.length === 0 ? (
309
+ <p className="sf-muted">No files yet — upload something above.</p>
310
+ ) : (
311
+ <div className="sf-table-wrap">
312
+ <table className="sf-table">
313
+ <thead>
314
+ <tr>
315
+ <th className="sf-col-check">
316
+ <input
317
+ type="checkbox"
318
+ aria-label="Select all"
319
+ checked={allVisibleSelected}
320
+ onChange={(e) => toggle(filtered.map((f) => f.id), e.target.checked)}
321
+ />
322
+ </th>
323
+ <th>Name</th>
324
+ <th>Sharing</th>
325
+ <th>Downloads</th>
326
+ <th className="sf-col-actions">Actions</th>
327
+ </tr>
328
+ </thead>
329
+ <tbody>{renderFolder(tree, 0)}</tbody>
330
+ </table>
331
+ </div>
332
+ )}
333
+
334
+ {editing && (
335
+ <ShareDialog
336
+ key={editing.id}
337
+ file={files.find((f) => f.id === editing.id) ?? editing}
338
+ onClose={() => setEditing(null)}
339
+ onSave={saveShare}
340
+ />
341
+ )}
342
+ </div>
343
+ );
344
+ }