@putkoff/abstract-securefiles 0.0.48 → 0.0.50

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 +47 -0
  9. package/dist/index.cjs +529 -1004
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +4 -8
  12. package/dist/index.js +522 -951
  13. package/dist/index.js.map +1 -1
  14. package/dist/types.d.ts +60 -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 +346 -0
  20. package/src/ServerFiles.tsx +339 -0
  21. package/src/ShareDialog.tsx +148 -0
  22. package/src/UploadPanel.tsx +217 -0
  23. package/src/api.ts +108 -0
  24. package/src/index.ts +4 -8
  25. package/src/styles.css +442 -0
  26. package/src/types.ts +56 -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.50",
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,346 @@
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
+ api.download(ready.map((f) => f.id));
101
+ };
102
+
103
+ const setShared = (items: SecureFile[], shareable: boolean) =>
104
+ run(
105
+ items.map((f) => f.id),
106
+ async () => {
107
+ for (const f of items) await saveShare({ id: f.id, shareable });
108
+ }
109
+ );
110
+
111
+ const remove = (items: SecureFile[]) =>
112
+ run(
113
+ items.map((f) => f.id),
114
+ async () => {
115
+ const res = await api.remove(items.map((f) => f.id));
116
+ if (res.errors.length) setNotice(`${res.errors.length} file(s) could not be deleted`);
117
+ await load();
118
+ }
119
+ );
120
+
121
+ const filtered = useMemo(() => {
122
+ const q = search.trim().toLowerCase();
123
+ return q ? files.filter((f) => f.filepath.toLowerCase().includes(q)) : files;
124
+ }, [files, search]);
125
+
126
+ const tree = useMemo(() => buildTree(filtered), [filtered]);
127
+
128
+ const folders = useMemo(() => {
129
+ const set = new Set<string>();
130
+ for (const f of files) {
131
+ const parts = f.filepath.split("/").slice(0, -1);
132
+ parts.forEach((_, i) => set.add(parts.slice(0, i + 1).join("/")));
133
+ }
134
+ return [...set];
135
+ }, [files]);
136
+
137
+ const selectedFiles = files.filter((f) => selected.has(f.id));
138
+ const toggle = (ids: number[], on: boolean) =>
139
+ setSelected((prev) => {
140
+ const next = new Set(prev);
141
+ ids.forEach((id) => (on ? next.add(id) : next.delete(id)));
142
+ return next;
143
+ });
144
+ const toggleFolder = (path: string) =>
145
+ setCollapsed((c) => {
146
+ const next = new Set(c);
147
+ next.has(path) ? next.delete(path) : next.add(path);
148
+ return next;
149
+ });
150
+ const allVisibleSelected = filtered.length > 0 && filtered.every((f) => selected.has(f.id));
151
+ const searching = search.trim().length > 0;
152
+
153
+ const renderFile = (f: SecureFile, depth: number) => {
154
+ const busy = busyIds.has(f.id);
155
+ return (
156
+ <tr key={f.id} className={busy ? "sf-busy" : undefined}>
157
+ <td className="sf-col-check">
158
+ <input
159
+ type="checkbox"
160
+ aria-label={`Select ${f.filename}`}
161
+ checked={selected.has(f.id)}
162
+ onChange={(e) => toggle([f.id], e.target.checked)}
163
+ />
164
+ </td>
165
+ <td style={{ paddingLeft: 10 + depth * 20 }}>
166
+ <span className={f.available ? undefined : "sf-strike"}>{f.filename}</span>
167
+ {f.kind === "folder" && (
168
+ <span className="sf-tag" title="Downloads as one uncompressed, resumable .tar of the whole folder">
169
+ folder · .tar
170
+ </span>
171
+ )}
172
+ {f.linked && (
173
+ <span className="sf-tag" title={f.source_path ?? undefined}>
174
+ linked
175
+ </span>
176
+ )}
177
+ {!f.available && (
178
+ <span
179
+ className="sf-tag sf-tag--warn"
180
+ title={f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk"}
181
+ >
182
+ missing
183
+ </span>
184
+ )}
185
+ </td>
186
+ <td>
187
+ <button
188
+ type="button"
189
+ className={`sf-pill${f.shareable ? " sf-pill--on" : ""}`}
190
+ disabled={busy || !f.available}
191
+ onClick={() => setShared([f], !f.shareable)}
192
+ title={f.shareable ? "Click to stop sharing" : "Click to share"}
193
+ >
194
+ {f.shareable ? "Shared" : "Private"}
195
+ </button>
196
+ {f.shareable && f.has_password && <span title="Password protected"> 🔒</span>}
197
+ </td>
198
+ <td>
199
+ {f.download_count} / {f.download_limit ?? "∞"}
200
+ </td>
201
+ <td className="sf-col-actions">
202
+ <div className="sf-row sf-row--end">
203
+ <button type="button" className="sf-btn" disabled={busy || !f.available} onClick={() => download([f])}>
204
+ Download
205
+ </button>
206
+ {f.shareable && f.available && <CopyButton text={f.share_url} />}
207
+ <button
208
+ type="button"
209
+ className="sf-icon-btn"
210
+ aria-label={`Share settings for ${f.filename}`}
211
+ title="Share settings"
212
+ onClick={() => setEditing(f)}
213
+ >
214
+ ⚙️
215
+ </button>
216
+ <ConfirmButton label={f.linked ? "Unlink" : "Delete"} disabled={busy} onConfirm={() => remove([f])} />
217
+ </div>
218
+ </td>
219
+ </tr>
220
+ );
221
+ };
222
+
223
+ const renderFolder = (node: FolderNode, depth: number): React.ReactNode[] => {
224
+ const rows: React.ReactNode[] = [];
225
+ const subFolders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name));
226
+ const ownFiles = [...node.files].sort((a, b) => a.filename.localeCompare(b.filename));
227
+ for (const folder of subFolders) {
228
+ const contents = allFiles(folder);
229
+ const ids = contents.map((f) => f.id);
230
+ const open = searching || !collapsed.has(folder.path);
231
+ rows.push(
232
+ <tr key={`dir:${folder.path}`} className="sf-dir">
233
+ <td className="sf-col-check">
234
+ <input
235
+ type="checkbox"
236
+ aria-label={`Select folder ${folder.path}`}
237
+ checked={ids.length > 0 && ids.every((id) => selected.has(id))}
238
+ onChange={(e) => toggle(ids, e.target.checked)}
239
+ />
240
+ </td>
241
+ <td colSpan={4} style={{ paddingLeft: 10 + depth * 20 }}>
242
+ <button type="button" className="sf-dir__toggle" onClick={() => toggleFolder(folder.path)}>
243
+ {open ? "▾" : "▸"} 📁 {folder.name} <span className="sf-muted">({contents.length})</span>
244
+ </button>
245
+ </td>
246
+ </tr>
247
+ );
248
+ if (open) rows.push(...renderFolder(folder, depth + 1));
249
+ }
250
+ rows.push(...ownFiles.map((f) => renderFile(f, depth)));
251
+ return rows;
252
+ };
253
+
254
+ return (
255
+ <div className="sf-root">
256
+ <header className="sf-head">
257
+ <h2>Secure Files</h2>
258
+ {user && <span className="sf-muted">Signed in as {user}</span>}
259
+ </header>
260
+
261
+ <UploadPanel api={api} folders={folders} onUploaded={load} />
262
+
263
+ {isAdmin && <ServerFiles api={api} folders={folders} onLinked={load} />}
264
+
265
+ <section className="sf-toolbar">
266
+ <input
267
+ className="sf-input sf-search"
268
+ type="search"
269
+ placeholder="Search files…"
270
+ value={search}
271
+ onChange={(e) => setSearch(e.target.value)}
272
+ />
273
+ <div className="sf-row">
274
+ <span className="sf-muted">{selected.size} selected</span>
275
+ {folders.length > 0 && (
276
+ <>
277
+ <button type="button" className="sf-btn" onClick={() => setCollapsed(new Set())}>
278
+ Expand all
279
+ </button>
280
+ <button type="button" className="sf-btn" onClick={() => setCollapsed(new Set(folders))}>
281
+ Collapse all
282
+ </button>
283
+ </>
284
+ )}
285
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => download(selectedFiles)}>
286
+ Download
287
+ </button>
288
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => setShared(selectedFiles, true)}>
289
+ Share
290
+ </button>
291
+ <button type="button" className="sf-btn" disabled={!selected.size} onClick={() => setShared(selectedFiles, false)}>
292
+ Unshare
293
+ </button>
294
+ <ConfirmButton label="Delete" disabled={!selected.size} onConfirm={() => remove(selectedFiles)} />
295
+ </div>
296
+ </section>
297
+
298
+ {notice && (
299
+ <p className="sf-error" role="alert">
300
+ {notice}
301
+ </p>
302
+ )}
303
+
304
+ {loading ? (
305
+ <p className="sf-muted">Loading…</p>
306
+ ) : error ? (
307
+ <p className="sf-error" role="alert">
308
+ {error}
309
+ </p>
310
+ ) : files.length === 0 ? (
311
+ <p className="sf-muted">No files yet — upload something above.</p>
312
+ ) : (
313
+ <div className="sf-table-wrap">
314
+ <table className="sf-table">
315
+ <thead>
316
+ <tr>
317
+ <th className="sf-col-check">
318
+ <input
319
+ type="checkbox"
320
+ aria-label="Select all"
321
+ checked={allVisibleSelected}
322
+ onChange={(e) => toggle(filtered.map((f) => f.id), e.target.checked)}
323
+ />
324
+ </th>
325
+ <th>Name</th>
326
+ <th>Sharing</th>
327
+ <th>Downloads</th>
328
+ <th className="sf-col-actions">Actions</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>{renderFolder(tree, 0)}</tbody>
332
+ </table>
333
+ </div>
334
+ )}
335
+
336
+ {editing && (
337
+ <ShareDialog
338
+ key={editing.id}
339
+ file={files.find((f) => f.id === editing.id) ?? editing}
340
+ onClose={() => setEditing(null)}
341
+ onSave={saveShare}
342
+ />
343
+ )}
344
+ </div>
345
+ );
346
+ }