@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/dist/index.cjs CHANGED
@@ -1,1091 +1,624 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var reactDom = require('react-dom');
5
- var React = require('react');
6
- var abstractUtilities = require('@putkoff/abstract-utilities');
4
+ var react = require('react');
7
5
 
8
- function _interopNamespaceDefault(e) {
9
- var n = Object.create(null);
10
- if (e) {
11
- Object.keys(e).forEach(function (k) {
12
- if (k !== 'default') {
13
- var d = Object.getOwnPropertyDescriptor(e, k);
14
- Object.defineProperty(n, k, d.get ? d : {
15
- enumerable: true,
16
- get: function () { return e[k]; }
17
- });
18
- }
19
- });
20
- }
21
- n.default = e;
22
- return Object.freeze(n);
23
- }
24
-
25
- var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
26
-
27
- function ClipboardIcon({
28
- title,
29
- titleId,
30
- ...props
31
- }, svgRef) {
32
- return /*#__PURE__*/React__namespace.createElement("svg", Object.assign({
33
- xmlns: "http://www.w3.org/2000/svg",
34
- fill: "none",
35
- viewBox: "0 0 24 24",
36
- strokeWidth: 1.5,
37
- stroke: "currentColor",
38
- "aria-hidden": "true",
39
- "data-slot": "icon",
40
- ref: svgRef,
41
- "aria-labelledby": titleId
42
- }, props), title ? /*#__PURE__*/React__namespace.createElement("title", {
43
- id: titleId
44
- }, title) : null, /*#__PURE__*/React__namespace.createElement("path", {
45
- strokeLinecap: "round",
46
- strokeLinejoin: "round",
47
- d: "M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
48
- }));
49
- }
50
- const ForwardRef$1 = /*#__PURE__*/ React__namespace.forwardRef(ClipboardIcon);
51
-
52
- function CloudArrowUpIcon({
53
- title,
54
- titleId,
55
- ...props
56
- }, svgRef) {
57
- return /*#__PURE__*/React__namespace.createElement("svg", Object.assign({
58
- xmlns: "http://www.w3.org/2000/svg",
59
- fill: "none",
60
- viewBox: "0 0 24 24",
61
- strokeWidth: 1.5,
62
- stroke: "currentColor",
63
- "aria-hidden": "true",
64
- "data-slot": "icon",
65
- ref: svgRef,
66
- "aria-labelledby": titleId
67
- }, props), title ? /*#__PURE__*/React__namespace.createElement("title", {
68
- id: titleId
69
- }, title) : null, /*#__PURE__*/React__namespace.createElement("path", {
70
- strokeLinecap: "round",
71
- strokeLinejoin: "round",
72
- d: "M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z"
73
- }));
74
- }
75
- const ForwardRef = /*#__PURE__*/ React__namespace.forwardRef(CloudArrowUpIcon);
76
-
77
- // Utility to convert input to array
78
- function ensure_list(obj) {
79
- if (Array.isArray(obj))
80
- return obj.filter(Boolean);
81
- if (typeof obj === 'string')
82
- return obj.split(' ').filter(Boolean);
83
- return [];
84
- }
85
- // Get list and length safely
86
- function getListAndLen(obj) {
87
- const return_obj = ensure_list(obj);
88
- const ob_len = return_obj.length;
89
- return { return_obj, ob_len };
90
- }
91
- // Combine class names with defaults
92
- function combineClassNames(classNames = [], defaultClasses = ['form-group', 'form-label', 'flex', 'gap-2']) {
93
- const { return_obj: inputClasses } = getListAndLen(classNames);
94
- const { return_obj: defaultClassList } = getListAndLen(defaultClasses);
95
- // Combine and remove duplicates
96
- const combined = [...new Set([...defaultClassList, ...inputClasses])];
97
- console.log(`Combined classes (length: ${combined.length}): ${combined.join(' ')}`);
98
- return combined;
99
- }
100
- function Button({ children, color = 'gray', variant = 'default', className = '', ...rest }) {
101
- const variantClasses = {
102
- default: 'btn',
103
- icon: 'btn-icon',
104
- primary: 'btn-primary',
105
- secondary: 'btn-secondary',
106
- };
107
- const colorClasses = {
108
- gray: '',
109
- green: variant === 'primary' ? 'btn-success' : 'btn-secondary',
110
- blue: variant === 'primary' ? 'btn-primary' : 'btn-secondary',
111
- };
112
- const classes = combineClassNames([variantClasses[variant], colorClasses[color], className]);
113
- return (jsxRuntime.jsx("button", { className: classes.join(' '), ...rest, children: children }));
114
- }
115
- function Checkbox({ label, className = '', classNames = [], ...rest }) {
116
- const classes = combineClassNames(classNames, ['checkbox', 'wm-checkbox']);
117
- return (jsxRuntime.jsxs("label", { className: classes.join(' '), children: [jsxRuntime.jsx("input", { type: "checkbox", className: "wm-checkbox", ...rest }), label && jsxRuntime.jsx("span", { children: label })] }));
118
- }
119
- function Input({ label, trailing, className = '', classNames = [], ...rest }) {
120
- const classes = combineClassNames(classNames, ['form-group', 'flex', 'gap-2']);
121
- const inputClasses = combineClassNames(className, ['form-input']);
122
- return (jsxRuntime.jsxs("label", { className: classes.join(' '), children: [label && jsxRuntime.jsx("span", { className: "form-label", children: label }), jsxRuntime.jsxs("div", { className: "flex gap-2", children: [jsxRuntime.jsx("input", { className: inputClasses.join(' '), ...rest }), trailing] })] }));
123
- }
124
- function Spinner() {
125
- const classes = combineClassNames('animate-pulse');
126
- return jsxRuntime.jsx("p", { className: classes.join(' '), children: "Loading\u2026" });
127
- }
128
-
129
- /** Helper function to trigger file download */
130
- function triggerDownload(blob, filename) {
131
- const url = URL.createObjectURL(blob);
132
- const a = Object.assign(document.createElement("a"), {
133
- href: url,
134
- download: filename,
135
- });
136
- document.body.appendChild(a);
137
- a.click();
138
- a.remove();
139
- URL.revokeObjectURL(url);
140
- console.log(`✅ Successfully downloaded: ${filename}`);
141
- }
142
-
143
- /** Non-React “direct download” used by some old links (kept for parity) */
144
- async function directDownload(item) {
145
- const token = localStorage.getItem("token");
146
- if (!token)
147
- throw new Error("No auth token found");
148
- const res = await fetch("https://abstractendeavors.com/api/secure-files/secure-download/", {
149
- method: "POST",
150
- headers: {
151
- Authorization: `Bearer ${token}`,
152
- "Content-Type": "application/json",
153
- Accept: "application/octet-stream",
154
- },
155
- body: JSON.stringify(item),
156
- });
157
- if (!res.ok)
158
- throw new Error(`Download failed: ${res.status}`);
159
- const blob = await res.blob();
160
- triggerDownload(blob, item.filename);
161
- }
162
-
163
- /**
164
- * Download a file using the current session cookie.
165
- * No JWT / localStorage token required.
166
- */
167
- async function downloadFile(item) {
168
- const res = await fetch("/api/secure-files/download", {
169
- method: "POST",
170
- credentials: "include",
171
- headers: { "Content-Type": "application/json" },
172
- body: JSON.stringify({
173
- id: item.id,
174
- filepath: item.filepath,
175
- filename: item.filename,
176
- }),
177
- });
178
- if (res.status === 401)
179
- throw new Error("Not authenticated – please log in.");
180
- if (!res.ok)
181
- throw new Error(`Download failed: HTTP ${res.status}`);
182
- const blob = await res.blob();
183
- triggerDownload(blob, item.filename || "download");
184
- }
185
-
186
- /**
187
- * Cookie-authenticated API helpers.
188
- * All requests include credentials: 'include' so the browser sends the
189
- * session cookie (auth_session) automatically. No JWT tokens are used.
190
- *
191
- * API base URL resolution order (first truthy value wins):
192
- * 1. window.__SECURE_FILES_API_BASE__ – runtime override (any bundler)
193
- * 2. VITE_SECURE_FILES_API_BASE – Vite env var
194
- * 3. REACT_APP_SECURE_FILES_API_BASE – Create React App env var
195
- * 4. /api/secure-files – same-origin default
196
- *
197
- * Subdomain API example (set one of the above to the full origin + path):
198
- * VITE_SECURE_FILES_API_BASE=https://api.example.com/secure-files
199
- */
200
- function resolveApiBase() {
201
- // 1. Runtime window config — works regardless of bundler
202
- if (typeof window !== "undefined" && window.__SECURE_FILES_API_BASE__) {
203
- return String(window.__SECURE_FILES_API_BASE__).replace(/\/+$/, "");
204
- }
205
- // 2. Vite (import.meta.env is replaced at build time; guard with try for non-Vite builds)
206
- try {
207
- const v = undefined?.VITE_SECURE_FILES_API_BASE;
208
- if (v)
209
- return String(v).replace(/\/+$/, "");
6
+ const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
7
+ class ApiError extends Error {
8
+ constructor(message, status) {
9
+ super(message);
10
+ this.status = status;
210
11
  }
211
- catch { /* not a Vite build */ }
212
- // 3. Create React App / plain Node
12
+ }
13
+ async function errorFrom(res) {
14
+ let message = `${res.status} ${res.statusText}`;
213
15
  try {
214
- const v = process.env?.REACT_APP_SECURE_FILES_API_BASE;
215
- if (v)
216
- return String(v).replace(/\/+$/, "");
16
+ const body = await res.json();
17
+ message = body?.message || body?.error || message;
217
18
  }
218
- catch { /* process not defined */ }
219
- // 4. Same-origin default
220
- return "/api/secure-files";
221
- }
222
- const API_BASE$1 = resolveApiBase();
223
- function apiUrl(path) {
224
- const p = path.startsWith("/") ? path : `/${path}`;
225
- return `${API_BASE$1}${p}`;
226
- }
227
- /**
228
- * Generic JSON fetch with session-cookie auth.
229
- * Throws Error("UNAUTHENTICATED") on 401 so callers can redirect to login.
230
- */
231
- async function cookieFetch(path, body, method) {
232
- const m = method ?? (body !== undefined ? "POST" : "GET");
233
- const init = { method: m, credentials: "include" };
234
- if (body !== undefined && body !== null) {
235
- init.headers = { "Content-Type": "application/json" };
236
- init.body = JSON.stringify(body);
19
+ catch {
20
+ /* non-JSON error body */
237
21
  }
238
- const res = await fetch(apiUrl(path), init);
239
22
  if (res.status === 401)
240
- throw new Error("UNAUTHENTICATED");
241
- if (!res.ok) {
242
- const msg = await res.text().catch(() => res.statusText);
243
- throw new Error(`HTTP ${res.status}: ${msg}`);
244
- }
245
- const ct = res.headers.get("Content-Type") ?? "";
246
- return (ct.includes("application/json") ? res.json() : res.text());
23
+ message = "Please log in to use Secure Files.";
24
+ return new ApiError(message, res.status);
247
25
  }
248
- /**
249
- * Check whether the current session is valid.
250
- * Calls /userInfo which requires authentication; returns true if the user
251
- * is logged in, false otherwise (no exception thrown).
252
- */
253
- async function checkAuth() {
254
- try {
255
- const res = await fetch(apiUrl("/userInfo"), {
256
- method: "GET",
26
+ function createApi(apiBase = DEFAULT_API_BASE) {
27
+ const root = `${apiBase.replace(/\/+$/, "")}/secure-files`;
28
+ async function json(path, init = {}) {
29
+ const res = await fetch(`${root}${path}`, {
257
30
  credentials: "include",
31
+ ...init,
32
+ headers: { Accept: "application/json", ...(init.body ? { "Content-Type": "application/json" } : {}) },
258
33
  });
259
- return res.ok;
260
- }
261
- catch {
262
- return false;
34
+ if (!res.ok)
35
+ throw await errorFrom(res);
36
+ return res.json();
263
37
  }
264
- }
265
- /**
266
- * Upload a single file using multipart/form-data with session-cookie auth.
267
- * @param file The File object to upload.
268
- * @param relPath Optional relative path within the user's directory.
269
- */
270
- async function cookieUpload(file, relPath = "") {
271
- const form = new FormData();
272
- form.append("file", file);
273
- if (relPath)
274
- form.append("rel_path", relPath);
275
- const res = await fetch(apiUrl("/upload"), {
276
- method: "POST",
277
- credentials: "include",
278
- body: form,
279
- });
280
- if (res.status === 401)
281
- throw new Error("UNAUTHENTICATED");
282
- if (!res.ok)
283
- throw new Error(`Upload failed: HTTP ${res.status}`);
284
- return res.json();
285
- }
286
- /** The resolved API base URL, exported for debugging / display purposes. */
287
- const SECURE_FILES_API_BASE = API_BASE$1;
288
-
289
- async function downloadSelect(item) {
290
- const res = await fetch(`${SECURE_FILES_API_BASE}/download`, {
291
- method: "POST",
292
- credentials: "include",
293
- headers: {
294
- "Content-Type": "application/json",
295
- Accept: "application/octet-stream",
38
+ return {
39
+ list: () => json("/userInfo"),
40
+ roots: () => json("/admin/roots"),
41
+ requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
42
+ confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
43
+ method: "POST",
44
+ body: JSON.stringify({ nonce, signature, label }),
45
+ }),
46
+ removeRoot: (id) => json(`/admin/roots/${id}`, { method: "DELETE" }),
47
+ browse: (path) => json(`/admin/browse?path=${encodeURIComponent(path)}`),
48
+ link: (paths, folder) => json("/admin/link", {
49
+ method: "POST",
50
+ body: JSON.stringify({ paths, folder }),
51
+ }),
52
+ updateShare: (update) => json("/files/share", { method: "PATCH", body: JSON.stringify(update) }),
53
+ remove: (ids) => json("/remove", {
54
+ method: "POST",
55
+ body: JSON.stringify({ ids }),
56
+ }),
57
+ async download(ids, fallbackName) {
58
+ const res = await fetch(`${root}/download`, {
59
+ method: "POST",
60
+ credentials: "include",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ ids }),
63
+ });
64
+ if (!res.ok)
65
+ throw await errorFrom(res);
66
+ const disposition = res.headers.get("Content-Disposition") || "";
67
+ const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
68
+ const name = match ? decodeURIComponent(match[1]) : fallbackName;
69
+ const url = URL.createObjectURL(await res.blob());
70
+ const a = document.createElement("a");
71
+ a.href = url;
72
+ a.download = name;
73
+ document.body.appendChild(a);
74
+ a.click();
75
+ a.remove();
76
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
296
77
  },
297
- body: JSON.stringify(item),
298
- });
299
- if (res.status === 401)
300
- throw new Error("Not authenticated – please log in.");
301
- if (!res.ok)
302
- throw new Error(`Download failed: HTTP ${res.status}`);
303
- const blob = await res.blob();
304
- if (!blob || blob.size === 0) {
305
- throw new Error(`Received empty file data for "${item.filename}"`);
306
- }
307
- triggerDownload(blob, item.filename);
308
- }
309
-
310
- /** Given a checkbox inside a table-row → download that row’s file. */
311
- async function downloadRow(cb) {
312
- const row = cb.closest("tr");
313
- const filepath = cb.value;
314
- const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
315
- if (filepath)
316
- await downloadSelect({ filepath, filename });
317
- }
318
- /** Bulk download: pass a NodeList or array of checkboxes. */
319
- async function downloadSelected(checkboxes) {
320
- const list = Array.from(checkboxes);
321
- if (!list.length) {
322
- alert("No files selected.");
323
- return;
324
- }
325
- for (const cb of list) {
326
- await downloadRow(cb);
327
- }
328
- }
329
-
330
- /**
331
- * Classic “select all rows” logic (still handy in non-React views
332
- * such as the admin dashboard).
333
- */
334
- function syncHeaderCheckbox() {
335
- const all = document.querySelectorAll(".file-checkbox");
336
- const checked = document.querySelectorAll(".file-checkbox:checked");
337
- const header = document.getElementById("select-all-checkbox");
338
- header.indeterminate =
339
- checked.length > 0 && checked.length < all.length;
340
- header.checked = checked.length === all.length;
78
+ upload(file, relPath, onProgress) {
79
+ return new Promise((resolve, reject) => {
80
+ const form = new FormData();
81
+ form.append("file", file);
82
+ form.append("rel_path", relPath);
83
+ const xhr = new XMLHttpRequest();
84
+ xhr.open("POST", `${root}/upload`);
85
+ xhr.withCredentials = true;
86
+ xhr.upload.onprogress = (e) => {
87
+ if (e.lengthComputable)
88
+ onProgress?.(e.loaded / e.total);
89
+ };
90
+ xhr.onload = () => {
91
+ if (xhr.status >= 200 && xhr.status < 300)
92
+ return resolve();
93
+ let message = `Upload failed (${xhr.status})`;
94
+ try {
95
+ message = JSON.parse(xhr.responseText)?.message || message;
96
+ }
97
+ catch {
98
+ /* non-JSON */
99
+ }
100
+ if (xhr.status === 401)
101
+ message = "Please log in to use Secure Files.";
102
+ reject(new ApiError(message, xhr.status));
103
+ };
104
+ xhr.onerror = () => reject(new ApiError("Network error during upload", 0));
105
+ xhr.send(form);
106
+ });
107
+ },
108
+ };
341
109
  }
342
110
 
343
- /** Wire up header + row checkboxes once the table is in the DOM. */
344
- function initSelectAll() {
345
- const header = document.getElementById("select-all-checkbox");
346
- const btn = document.getElementById("select-all");
347
- // 1) header → toggle all
348
- header.addEventListener("change", (e) => {
349
- const on = e.target.checked;
350
- document
351
- .querySelectorAll(".file-checkbox")
352
- .forEach((cb) => (cb.checked = on));
353
- });
354
- // 2) button → toggle all
355
- btn.addEventListener("click", () => {
356
- const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
357
- if (!cbs.length) {
358
- alert("No files to select.");
359
- return;
111
+ const NEW_FOLDER = "__new__";
112
+ function FolderSelect({ label, folders, value, onChange, }) {
113
+ const [creating, setCreating] = react.useState(false);
114
+ const [draft, setDraft] = react.useState("");
115
+ const [extra, setExtra] = react.useState([]);
116
+ const options = [...new Set([...folders, ...extra, ...(value ? [value] : [])])].sort();
117
+ const accept = () => {
118
+ const clean = draft.trim().replace(/^\/+|\/+$/g, "");
119
+ if (clean) {
120
+ setExtra((e) => (e.includes(clean) ? e : [...e, clean]));
121
+ onChange(clean);
360
122
  }
361
- const anyUnchecked = cbs.some((cb) => !cb.checked);
362
- cbs.forEach((cb) => (cb.checked = anyUnchecked));
363
- syncHeaderCheckbox();
364
- });
365
- // 3) row boxes → keep header state
366
- document
367
- .querySelectorAll(".file-checkbox")
368
- .forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
369
- syncHeaderCheckbox();
123
+ setCreating(false);
124
+ setDraft("");
125
+ };
126
+ return (jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: label }), creating ? (jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("input", { className: "sf-input", autoFocus: true, value: draft, placeholder: "new-folder-name (use / for subfolders)", onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => {
127
+ if (e.key === "Enter")
128
+ accept();
129
+ if (e.key === "Escape")
130
+ setCreating(false);
131
+ } }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: accept, children: "OK" })] })) : (jsxRuntime.jsxs("select", { className: "sf-input", value: value, onChange: (e) => (e.target.value === NEW_FOLDER ? setCreating(true) : onChange(e.target.value)), children: [jsxRuntime.jsx("option", { value: "", children: "/ (top level)" }), options.map((d) => (jsxRuntime.jsxs("option", { value: d, children: [d, "/"] }, d))), jsxRuntime.jsx("option", { value: NEW_FOLDER, children: "+ Create new directory\u2026" })] }))] }));
370
132
  }
371
133
 
372
- /** Utility for other screens: toggle a list of checkboxes on/off. */
373
- function toggleCheckboxes(cbs) {
374
- if (!cbs.length) {
375
- alert("No files to select.");
376
- return;
377
- }
378
- const anyUnchecked = cbs.some((cb) => !cb.checked);
379
- cbs.forEach((cb) => (cb.checked = anyUnchecked));
380
- syncHeaderCheckbox();
134
+ function readEntries(reader) {
135
+ return new Promise((resolve, reject) => reader.readEntries(resolve, reject));
381
136
  }
382
-
383
- const API_BASE = '/api/secure-files';
384
- /** Generic PATCH helper for file-level admin settings. */
385
- async function patchFileSetting(fileId, field, value) {
386
- const res = await fetch(`${API_BASE}/files/${fileId}/settings/${field}`, {
387
- method: 'PATCH',
388
- credentials: 'include',
389
- headers: { 'Content-Type': 'application/json' },
390
- body: JSON.stringify({ value }),
391
- });
392
- if (!res.ok) {
393
- const err = await res.json().catch(() => ({}));
394
- throw new Error(err.error || res.statusText);
137
+ async function walkEntry(entry, prefix, out) {
138
+ if (entry.isFile) {
139
+ const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
140
+ out.push({ file, relPath: prefix + file.name });
395
141
  }
396
- return (await res.json());
397
- }
398
-
399
- /** File-upload helper (used by non-React fallback pages). */
400
- async function uploadFile(file, subdir) {
401
- const fileInput = file || document.getElementById('file-browser');
402
- const subdirInput = subdir || document.getElementById('subdir');
403
- const banner = document.getElementById('banner');
404
- if (!fileInput.files?.length) {
405
- alert('No file selected.');
406
- return;
407
- }
408
- const f = fileInput.files[0];
409
- const dir = (subdirInput?.value ?? '').trim();
410
- const relPath = dir ? `${dir}/${f.name}` : f.name;
411
- try {
412
- const json = await cookieUpload(f, relPath);
413
- if (banner)
414
- banner.textContent = `Uploaded: ${json.filepath}`;
415
- fileInput.value = '';
416
- if (subdirInput)
417
- subdirInput.value = '';
418
- window.location.reload();
419
- }
420
- catch (err) {
421
- console.error('Upload failed', err);
422
- }
423
- }
424
-
425
- async function uploadFiles(file, subdir = '') {
426
- if (!(file instanceof File))
427
- throw new Error('Invalid file: must be a File object.');
428
- if (typeof subdir !== 'string')
429
- throw new Error('Invalid subdir: must be a string.');
430
- const relPath = subdir.trim() ? `${subdir.trim()}/${file.name}` : file.name;
431
- const json = await cookieUpload(file, relPath);
432
- if (!json?.filepath)
433
- throw new Error('Server response missing filepath.');
434
- return { filepath: json.filepath };
435
- }
436
-
437
- async function removeFile(item) {
438
- const resp = await cookieFetch("remove", { id: item.id }, "POST");
439
- if (!resp || resp === false || resp?.result === false) {
440
- throw new Error(`Could not delete "${item.filename ?? item.id}"`);
142
+ else if (entry.isDirectory) {
143
+ const reader = entry.createReader();
144
+ let batch;
145
+ do {
146
+ batch = await readEntries(reader);
147
+ for (const child of batch)
148
+ await walkEntry(child, `${prefix}${entry.name}/`, out);
149
+ } while (batch.length > 0);
441
150
  }
442
151
  }
443
-
444
- /** Bulk download: pass a NodeList or array of checkboxes. */
445
- async function removeSelected(checkboxes) {
446
- const list = Array.from(checkboxes);
447
- if (!list.length) {
448
- alert("No files selected.");
449
- return;
152
+ async function filesFromDrop(dt) {
153
+ const out = [];
154
+ const entries = Array.from(dt.items || [])
155
+ .map((item) => item.webkitGetAsEntry?.())
156
+ .filter(Boolean);
157
+ if (entries.length) {
158
+ for (const entry of entries)
159
+ await walkEntry(entry, "", out);
450
160
  }
451
- for (const cb of list) {
452
- alert(cb);
453
- await removeFile(cb);
161
+ else {
162
+ for (const file of Array.from(dt.files))
163
+ out.push({ file, relPath: file.name });
454
164
  }
455
- }
456
-
457
- /** Given a checkbox inside a table-row → download that row’s file. */
458
- async function removeRow(cb) {
459
- const row = cb.closest("tr");
460
- abstractUtilities.alertit(row);
461
- if (row && row?.id)
462
- await removeFile(row);
463
- }
464
-
465
- async function listFiles(setFiles = null) {
466
- const result = await cookieFetch("list", undefined, "GET");
467
- const apiRows = Array.isArray(result)
468
- ? result
469
- : (result?.result ?? []);
470
- const rows = apiRows.map((r) => ({
471
- id: r.id,
472
- filename: r.filename,
473
- uploader_id: r.uploader_id,
474
- filepath: r.filepath,
475
- created_at: r.created_at,
476
- download_count: r.download_count,
477
- download_limit: r.download_limit,
478
- shareable: Boolean(r.shareable),
479
- needsPassword: !!r.share_password,
480
- share_password: r.share_password,
481
- password_str: r.password_str,
482
- }));
483
- if (setFiles)
484
- setFiles(rows);
485
- return rows;
486
- }
487
-
488
- /**
489
- * Toggle sharing on a file.
490
- * Sends the updated settings to /api/secure-files/files/share.
491
- * Does NOT update local state – the caller is responsible for that.
492
- */
493
- async function toggleShare(file) {
494
- const updated = { ...file, shareable: !file.shareable };
495
- if (!updated.shareable)
496
- updated.needsPassword = false;
497
- await cookieFetch("files/share", {
498
- id: updated.id,
499
- shareable: updated.shareable,
500
- share_password: updated.share_password ?? null,
501
- password_str: updated.password_str ?? null,
502
- download_limit: updated.download_limit ?? null,
503
- }, "POST");
504
- }
505
-
506
- const useLoadFiles = (setLoading, setError, setFiles) => {
507
- return React.useCallback(async () => {
508
- setLoading(true);
509
- setError(null);
510
- try {
511
- const result = await cookieFetch("list", undefined, "GET");
512
- const apiRows = Array.isArray(result)
513
- ? result
514
- : (result?.result ?? []);
515
- setFiles(apiRows.map((r) => ({
516
- id: r.id,
517
- filename: r.filename,
518
- uploader_id: r.uploader_id,
519
- filepath: r.filepath,
520
- created_at: r.created_at,
521
- download_count: r.download_count,
522
- download_limit: r.download_limit,
523
- shareable: Boolean(r.shareable),
524
- needsPassword: !!r.share_password,
525
- share_password: r.share_password,
526
- password_str: r.password_str,
527
- })));
528
- }
529
- catch (err) {
530
- setError(err instanceof Error ? err : new Error('Failed to load files'));
531
- }
532
- finally {
533
- setLoading(false);
165
+ return out;
166
+ }
167
+ function joinTarget(target, relPath) {
168
+ const clean = target.trim().replace(/^\/+|\/+$/g, "");
169
+ return clean ? `${clean}/${relPath}` : relPath;
170
+ }
171
+ function CloudIcon() {
172
+ return (jsxRuntime.jsx("svg", { className: "sf-drop__icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z" }) }));
173
+ }
174
+ function UploadPanel({ api, folders, onUploaded, }) {
175
+ const [queue, setQueue] = react.useState([]);
176
+ const [mode, setMode] = react.useState("files");
177
+ const [target, setTarget] = react.useState("");
178
+ const [dragging, setDragging] = react.useState(false);
179
+ const [busy, setBusy] = react.useState(false);
180
+ const [message, setMessage] = react.useState(null);
181
+ const fileInput = react.useRef(null);
182
+ const folderInput = react.useRef(null);
183
+ const enqueue = (items) => {
184
+ setMessage(null);
185
+ setQueue((q) => {
186
+ const kept = q.filter((x) => !x.done);
187
+ const seen = new Set(kept.map((x) => x.relPath));
188
+ return [...kept, ...items.filter((i) => !seen.has(i.relPath)).map((i) => ({ ...i, progress: 0 }))];
189
+ });
190
+ };
191
+ const openPicker = () => (mode === "folder" ? folderInput : fileInput).current?.click();
192
+ const fromInput = (e) => {
193
+ const files = Array.from(e.target.files || []);
194
+ enqueue(files.map((file) => ({ file, relPath: file.webkitRelativePath || file.name })));
195
+ e.target.value = "";
196
+ };
197
+ const uploadAll = async () => {
198
+ setBusy(true);
199
+ setMessage(null);
200
+ let failed = 0;
201
+ for (let i = 0; i < queue.length; i++) {
202
+ const item = queue[i];
203
+ if (item.done)
204
+ continue;
205
+ const set = (patch) => setQueue((q) => q.map((x, idx) => (idx === i ? { ...x, ...patch } : x)));
206
+ try {
207
+ await api.upload(item.file, joinTarget(target, item.relPath), (p) => set({ progress: p }));
208
+ set({ progress: 1, done: true, error: undefined });
209
+ }
210
+ catch (err) {
211
+ failed++;
212
+ set({ error: err instanceof Error ? err.message : String(err) });
213
+ }
534
214
  }
535
- }, [setLoading, setError, setFiles]);
536
- };
537
-
538
- const useToggleShare = (setFiles) => {
539
- return React.useCallback(async (file) => {
540
- const updated = { ...file, shareable: !file.shareable };
541
- if (!updated.shareable)
542
- updated.needsPassword = false;
215
+ setBusy(false);
216
+ setMessage(failed ? `${failed} upload(s) failed` : "Upload complete");
217
+ if (!failed)
218
+ setQueue([]);
219
+ onUploaded();
220
+ };
221
+ const pending = queue.filter((q) => !q.done);
222
+ return (jsxRuntime.jsxs("section", { className: "sf-upload", children: [jsxRuntime.jsxs("div", { className: "sf-row sf-mode", role: "radiogroup", "aria-label": "Choose what clicking the upload area picks", children: [jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: mode === "files", onChange: () => setMode("files") }), " Files"] }), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: mode === "folder", onChange: () => setMode("folder") }), " Folder"] }), jsxRuntime.jsx("span", { className: "sf-muted", children: "(drag-and-drop accepts either)" })] }), jsxRuntime.jsxs("div", { className: `sf-drop${dragging ? " sf-drop--active" : ""}`, role: "button", tabIndex: 0, "aria-label": `Upload area: click to choose ${mode === "folder" ? "a folder" : "files"}, or drop files or folders`, onClick: openPicker, onKeyDown: (e) => {
223
+ if (e.key === "Enter" || e.key === " ") {
224
+ e.preventDefault();
225
+ openPicker();
226
+ }
227
+ }, onDragEnter: (e) => {
228
+ e.preventDefault();
229
+ setDragging(true);
230
+ }, onDragOver: (e) => e.preventDefault(), onDragLeave: (e) => {
231
+ if (!e.currentTarget.contains(e.relatedTarget))
232
+ setDragging(false);
233
+ }, onDrop: async (e) => {
234
+ e.preventDefault();
235
+ setDragging(false);
236
+ enqueue(await filesFromDrop(e.dataTransfer));
237
+ }, children: [jsxRuntime.jsx(CloudIcon, {}), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("p", { className: "sf-drop__title", children: "Drag & drop files or folders here" }), jsxRuntime.jsxs("p", { className: "sf-muted", children: ["Click anywhere to choose ", mode === "folder" ? "a folder" : "files"] }), pending.length > 0 && (jsxRuntime.jsxs("p", { className: "sf-drop__count", children: [pending.length, " item", pending.length === 1 ? "" : "s", " queued"] }))] })] }), jsxRuntime.jsx("input", { ref: fileInput, type: "file", multiple: true, hidden: true, onChange: fromInput }), jsxRuntime.jsx("input", { ref: folderInput, type: "file", multiple: true, hidden: true, onChange: fromInput, ...{ webkitdirectory: "", directory: "" } }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx(FolderSelect, { label: "Upload into", folders: folders, value: target, onChange: setTarget }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !pending.length, onClick: uploadAll, children: busy ? "Uploading…" : `Upload ${pending.length}` }), queue.length > 0 && !busy && (jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setQueue([]), children: "Clear" }))] }), message && (jsxRuntime.jsx("p", { className: message.includes("failed") ? "sf-error" : "sf-muted", role: "status", children: message })), queue.length > 0 && (jsxRuntime.jsx("ul", { className: "sf-queue", children: queue.map((q, i) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsx("span", { className: "sf-queue__name", children: joinTarget(target, q.relPath) }), q.error ? (jsxRuntime.jsx("span", { className: "sf-error", children: q.error })) : (jsxRuntime.jsxs("span", { className: "sf-row", children: [jsxRuntime.jsx("progress", { max: 1, value: q.progress }), jsxRuntime.jsxs("span", { className: "sf-muted", children: [Math.round(q.progress * 100), "%"] })] }))] }, `${q.relPath}-${i}`))) }))] }));
238
+ }
239
+
240
+ function CopyButton({ text, label = "Copy link" }) {
241
+ const [copied, setCopied] = react.useState(false);
242
+ const copy = async () => {
543
243
  try {
544
- await cookieFetch("files/share", {
545
- id: updated.id,
546
- shareable: updated.shareable,
547
- share_password: updated.share_password ?? null,
548
- password_str: updated.password_str ?? null,
549
- download_limit: updated.download_limit ?? null,
550
- }, "POST");
551
- setFiles(curr => curr.map(f => f.id === file.id ? updated : f));
244
+ await navigator.clipboard.writeText(text);
552
245
  }
553
- catch (err) {
554
- console.error('Failed to toggle share:', err);
246
+ catch {
247
+ const ta = document.createElement("textarea");
248
+ ta.value = text;
249
+ document.body.appendChild(ta);
250
+ ta.select();
251
+ document.execCommand("copy");
252
+ ta.remove();
555
253
  }
556
- }, [setFiles]);
557
- };
558
-
559
- const usePatchShareSettings = (setFiles) => {
560
- return React.useCallback(async (file) => {
561
- const body = {
562
- id: file.id,
563
- shareable: file.shareable,
564
- share_password: file.share_password ?? null,
565
- password_str: file.password_str ?? null,
566
- download_limit: file.download_limit ?? null,
567
- };
254
+ setCopied(true);
255
+ setTimeout(() => setCopied(false), 1500);
256
+ };
257
+ return (jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
258
+ }
259
+
260
+ function ShareDialog({ file, onClose, onSave, }) {
261
+ const [shareable, setShareable] = react.useState(file.shareable);
262
+ const [limit, setLimit] = react.useState(file.download_limit ? String(file.download_limit) : "");
263
+ const [pwMode, setPwMode] = react.useState(file.has_password ? "keep" : "remove");
264
+ const [password, setPassword] = react.useState("");
265
+ const [resetCount, setResetCount] = react.useState(false);
266
+ const [saving, setSaving] = react.useState(false);
267
+ const [error, setError] = react.useState(null);
268
+ react.useEffect(() => {
269
+ const onKey = (e) => e.key === "Escape" && onClose();
270
+ window.addEventListener("keydown", onKey);
271
+ return () => window.removeEventListener("keydown", onKey);
272
+ }, [onClose]);
273
+ const limitValid = limit === "" || /^\d+$/.test(limit);
274
+ const passwordValid = pwMode !== "set" || password.length > 0;
275
+ const save = async () => {
276
+ setSaving(true);
277
+ setError(null);
568
278
  try {
569
- await cookieFetch("files/share", body, "POST");
570
- setFiles(curr => curr.map(f => f.id === body.id ? { ...f, ...body } : f));
279
+ const update = { id: file.id, shareable };
280
+ if (shareable) {
281
+ update.download_limit = limit === "" ? null : Number(limit);
282
+ if (pwMode === "set")
283
+ update.password = password;
284
+ else if (pwMode === "remove" && file.has_password)
285
+ update.password = "";
286
+ if (resetCount)
287
+ update.reset_count = true;
288
+ }
289
+ await onSave(update);
290
+ onClose();
571
291
  }
572
292
  catch (err) {
573
- console.error('Failed to update share settings:', err);
574
- }
575
- }, [setFiles]);
576
- };
577
-
578
- /**
579
- * Hook alias used by useFiles.ts. Patches share settings via the session-cookie
580
- * authenticated /files/share endpoint.
581
- */
582
- const useSaveShareSettings = (setFiles) => {
583
- return React.useCallback(async (file) => {
584
- const body = {
585
- id: file.id,
586
- shareable: file.shareable,
587
- share_password: file.share_password ?? null,
588
- password_str: file.password_str ?? null,
589
- download_limit: file.download_limit ?? null,
590
- };
591
- try {
592
- await cookieFetch("files/share", body, "POST");
593
- setFiles(curr => curr.map(f => f.id === body.id ? { ...f, ...body } : f));
293
+ setError(err instanceof Error ? err.message : String(err));
594
294
  }
595
- catch (err) {
596
- console.error('Failed to save share settings:', err);
295
+ finally {
296
+ setSaving(false);
597
297
  }
598
- }, [setFiles]);
599
- };
298
+ };
299
+ return (jsxRuntime.jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxRuntime.jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxRuntime.jsxs("header", { className: "sf-dialog__head", children: [jsxRuntime.jsx("h3", { children: "Share settings" }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsxRuntime.jsx("p", { className: "sf-muted", children: file.filepath }), jsxRuntime.jsxs("label", { className: "sf-check", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxRuntime.jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Download limit (blank = unlimited)" }), jsxRuntime.jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsxRuntime.jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxRuntime.jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Password" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [file.has_password && (jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsxRuntime.jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Share link" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsxRuntime.jsx(CopyButton, { text: file.share_url })] }), !file.shareable && jsxRuntime.jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })] })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
300
+ }
600
301
 
601
- const useDeleteFile = (setFiles) => {
602
- return React.useCallback(async (file) => {
603
- const targets = Array.isArray(file) ? file : [file];
604
- for (const f of targets) {
605
- try {
606
- await cookieFetch("remove", { id: f.id }, "POST");
607
- setFiles(curr => curr.filter(item => item.id !== f.id));
302
+ function ConfirmButton({ label, onConfirm, disabled, }) {
303
+ const [armed, setArmed] = react.useState(false);
304
+ react.useEffect(() => {
305
+ if (!armed)
306
+ return;
307
+ const t = setTimeout(() => setArmed(false), 3000);
308
+ return () => clearTimeout(t);
309
+ }, [armed]);
310
+ return (jsxRuntime.jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
311
+ if (armed) {
312
+ setArmed(false);
313
+ onConfirm();
608
314
  }
609
- catch (err) {
610
- console.error(`Failed to delete "${f.filename}":`, err);
315
+ else {
316
+ setArmed(true);
611
317
  }
612
- }
613
- }, [setFiles]);
614
- };
615
-
616
- function useFiles() {
617
- const [files, setFiles] = React.useState([]);
618
- const [loading, setLoading] = React.useState(false);
619
- const [error, setError] = React.useState(null);
620
- const loadCallback = useLoadFiles(setLoading, setError, setFiles);
621
- const toggleShareCallback = useToggleShare(setFiles);
622
- const patchShareSettingsCallback = useSaveShareSettings(setFiles);
623
- const deleteFileCallback = useDeleteFile(setFiles);
624
- React.useEffect(() => {
625
- loadCallback();
626
- }, [loadCallback]);
627
- return {
628
- files,
629
- loading,
630
- error,
631
- load: loadCallback,
632
- toggleShare: toggleShareCallback,
633
- patchShareSettings: patchShareSettingsCallback,
634
- deleteFile: deleteFileCallback,
635
- setFiles,
636
- setLoading,
637
- setError,
638
- };
318
+ }, children: armed ? "Confirm" : label }));
639
319
  }
640
320
 
641
- function FileRow({ file, onDownload, onSettings, onToggleShare, onDelete, }) {
642
- const isOwner = file.uploader_id?.toLowerCase() === (abstractUtilities.currentUsername() ?? '').toLowerCase();
643
- const downloadAllowed = isOwner || file.shareable;
644
- const [isDownloading, setIsDownloading] = React.useState(false);
645
- const [isTogglingShare, setIsTogglingShare] = React.useState(false);
646
- const [isDeleting, setIsDeleting] = React.useState(false);
647
- // Mimic WindowManagerApp's unsaved heuristic
648
- const looksUnsaved = file.filename.toLowerCase().startsWith('untitled') ||
649
- file.filename.includes('*');
650
- const handleDownload = async () => {
651
- setIsDownloading(true);
321
+ function formatSize(bytes) {
322
+ const units = ["B", "KB", "MB", "GB", "TB"];
323
+ let n = bytes;
324
+ let i = 0;
325
+ while (n >= 1024 && i < units.length - 1) {
326
+ n /= 1024;
327
+ i++;
328
+ }
329
+ return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
330
+ }
331
+ const errText = (err) => (err instanceof Error ? err.message : String(err));
332
+ function AddRoot({ api, onAdded }) {
333
+ const [path, setPath] = react.useState("");
334
+ const [label, setLabel] = react.useState("");
335
+ const [challenge, setChallenge] = react.useState(null);
336
+ const [signature, setSignature] = react.useState("");
337
+ const [error, setError] = react.useState(null);
338
+ const [busy, setBusy] = react.useState(false);
339
+ const request = async () => {
340
+ setBusy(true);
341
+ setError(null);
652
342
  try {
653
- await onDownload();
343
+ setChallenge((await api.requestRoot(path.trim())).challenge);
344
+ setSignature("");
345
+ }
346
+ catch (err) {
347
+ setError(errText(err));
654
348
  }
655
349
  finally {
656
- setIsDownloading(false);
350
+ setBusy(false);
657
351
  }
658
352
  };
659
- const handleToggleShare = async () => {
660
- setIsTogglingShare(true);
353
+ const confirm = async () => {
354
+ if (!challenge)
355
+ return;
356
+ setBusy(true);
357
+ setError(null);
661
358
  try {
662
- await onToggleShare();
359
+ onAdded((await api.confirmRoot(challenge.nonce, signature, label)).roots);
360
+ setChallenge(null);
361
+ setPath("");
362
+ setLabel("");
363
+ setSignature("");
364
+ }
365
+ catch (err) {
366
+ setError(errText(err));
663
367
  }
664
368
  finally {
665
- setIsTogglingShare(false);
369
+ setBusy(false);
666
370
  }
667
371
  };
668
- const handleDeleteFile = async () => {
669
- setIsDeleting(true);
372
+ const minutesLeft = challenge ? Math.max(0, Math.round((challenge.expires * 1000 - Date.now()) / 60000)) : 0;
373
+ return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Allow a directory" }), !challenge ? (jsxRuntime.jsxs("div", { className: "sf-row sf-row--bottom", children: [jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Absolute path on the server" }), jsxRuntime.jsx("input", { className: "sf-input", value: path, onChange: (e) => setPath(e.target.value), placeholder: "/srv/shared/docs", onKeyDown: (e) => e.key === "Enter" && path.trim() && request() })] }), jsxRuntime.jsxs("label", { className: "sf-field sf-field--narrow", children: [jsxRuntime.jsx("span", { children: "Label (optional)" }), jsxRuntime.jsx("input", { className: "sf-input", value: label, onChange: (e) => setLabel(e.target.value) })] }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !path.trim(), onClick: request, children: "Request" })] })) : (jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsxs("p", { children: ["Allowing ", jsxRuntime.jsx("code", { children: challenge.message.split("|")[3] }), " requires a signature from a trusted SSH key. Run this where that key lives (expires in about ", minutesLeft, " min):"] }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("textarea", { className: "sf-input sf-code", readOnly: true, rows: 3, value: challenge.command }), jsxRuntime.jsx(CopyButton, { text: challenge.command, label: "Copy command" })] }), jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Paste the full output (BEGIN \u2026 END SSH SIGNATURE)" }), jsxRuntime.jsx("textarea", { className: "sf-input sf-code", rows: 6, value: signature, onChange: (e) => setSignature(e.target.value), placeholder: "-----BEGIN SSH SIGNATURE-----" })] }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !signature.includes("END SSH SIGNATURE"), onClick: confirm, children: "Verify & allow" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setChallenge(null), children: "Cancel" })] })] })), error && jsxRuntime.jsx("p", { className: "sf-error", children: error })] }));
374
+ }
375
+ function Browser({ api, roots, folders, onLinked, }) {
376
+ const [listing, setListing] = react.useState(null);
377
+ const [picked, setPicked] = react.useState(new Set());
378
+ const [target, setTarget] = react.useState("");
379
+ const [error, setError] = react.useState(null);
380
+ const [result, setResult] = react.useState(null);
381
+ const [busy, setBusy] = react.useState(false);
382
+ const open = react.useCallback(async (path) => {
383
+ setError(null);
670
384
  try {
671
- await onDelete();
672
- }
673
- finally {
674
- setIsDeleting(false);
385
+ setListing(await api.browse(path));
386
+ setPicked(new Set());
675
387
  }
676
- };
677
- return (jsxRuntime.jsxs("tr", { className: "wm-row transition-colors duration-75", children: [jsxRuntime.jsx("td", { className: "wm-cell w-12 align-middle", children: jsxRuntime.jsx("input", { type: "checkbox", className: "wm-checkbox", "aria-label": `Select ${file.filename}` }) }), jsxRuntime.jsx("td", { className: `wm-cell ${looksUnsaved ? 'unsaved' : ''}`, children: file.filename }), jsxRuntime.jsx("td", { className: "wm-cell w-12 align-middle", children: downloadAllowed ? (jsxRuntime.jsx(Button, { color: "blue", variant: "secondary", onClick: handleDownload, disabled: isDownloading, "aria-label": `Download ${file.filename}`, className: "wm-btn-link", children: isDownloading ? 'Downloading...' : 'Download' })) : (jsxRuntime.jsx("span", { className: "text-gray-500 text-lg", "aria-label": "Download restricted", children: "\uD83D\uDD12" })) }), jsxRuntime.jsx("td", { className: "wm-cell w-12 align-middle", children: isOwner && (jsxRuntime.jsx(Button, { color: "gray", variant: "icon", onClick: () => {
678
- console.log('⚙︎ clicked', file.id);
679
- onSettings();
680
- }, "aria-label": "Open share settings", className: "wm-btn-icon text-lg", children: "\u2699\uFE0F" })) }), jsxRuntime.jsx("td", { className: "wm-cell w-12 align-middle", children: isOwner && (jsxRuntime.jsx(Button, { color: file.shareable ? 'blue' : 'gray', variant: file.shareable ? 'primary' : 'secondary', onClick: handleToggleShare, disabled: isTogglingShare, className: "btn", children: isTogglingShare
681
- ? 'Toggling...'
682
- : file.shareable
683
- ? 'Sharing'
684
- : 'Share' })) }), jsxRuntime.jsx("td", { className: "wm-cell w-12 align-middle", children: isOwner && (jsxRuntime.jsx(Button, { color: "gray", variant: "icon", onClick: handleDeleteFile, disabled: isDeleting, className: "wm-btn-icon text-lg", children: "\uD83D\uDDD1" })) }), jsxRuntime.jsxs("td", { className: "wm-cell", children: [file.download_count, " / ", file.download_limit ?? "∞"] })] }));
685
- }
686
-
687
- function ShareModal({ file, onClose, onSave }) {
688
- const [fileId, setFileId] = React.useState("");
689
- const [enabled, setEnabled] = React.useState(false);
690
- const [password, setPassword] = React.useState(undefined);
691
- const [strPassword, setStrPassword] = React.useState("");
692
- const [download_limit, setDownloadLimit] = React.useState(0);
693
- const [error, setError] = React.useState("");
694
- const [busy, setBusy] = React.useState(false);
695
- const modalRef = React.useRef(null);
696
- const shareLink = file && enabled
697
- ? `/api/secure-files/secure-download/${file.id}`
698
- : "";
699
- React.useEffect(() => {
700
- if (!file)
701
- return;
702
- setFileId(file.id);
703
- setEnabled(file.shareable);
704
- setPassword(file.share_password);
705
- setStrPassword(file.password_str ?? "");
706
- setDownloadLimit(file.download_limit ?? 0);
707
- setError("");
708
- modalRef.current?.focus();
709
- }, [file]);
710
- React.useEffect(() => {
711
- const h = (e) => e.key === "Escape" && onClose();
712
- window.addEventListener("keydown", h);
713
- return () => window.removeEventListener("keydown", h);
714
- }, [onClose]);
715
- if (!file)
716
- return null;
717
- async function handleSave() {
718
- if (strPassword && strPassword.length < 4) {
719
- setError("Password must be at least 4 characters");
720
- return;
388
+ catch (err) {
389
+ setError(errText(err));
721
390
  }
391
+ }, [api]);
392
+ react.useEffect(() => {
393
+ if (!listing && roots.length)
394
+ open(roots[0].path);
395
+ if (listing && !roots.some((r) => listing.path.startsWith(r.path)))
396
+ setListing(null);
397
+ }, [roots, listing, open]);
398
+ const link = async () => {
722
399
  setBusy(true);
723
- const save_dict = {
724
- id: fileId,
725
- shareable: enabled,
726
- needsPassword: !!strPassword,
727
- share_password: strPassword || null,
728
- password_str: strPassword || null,
729
- download_limit: download_limit === "" ? null : Number(download_limit),
730
- };
400
+ setResult(null);
731
401
  try {
732
- const resp = await onSave(save_dict);
733
- onClose();
402
+ const res = await api.link([...picked], target);
403
+ const parts = [`Linked ${res.linked.length} file(s)`];
404
+ if (res.errors.length)
405
+ parts.push(`${res.errors.length} skipped: ${res.errors.map((e) => e.error).join("; ")}`);
406
+ setResult(parts.join(". "));
407
+ onLinked();
408
+ if (listing)
409
+ await open(listing.path);
734
410
  }
735
411
  catch (err) {
736
- setError(`${err} Failed to save settings. Please try again.`);
412
+ setResult(errText(err));
737
413
  }
738
414
  finally {
739
415
  setBusy(false);
740
416
  }
417
+ };
418
+ if (!roots.length)
419
+ return jsxRuntime.jsx("p", { className: "sf-muted", children: "Allow a directory above to browse and link its files." });
420
+ const linkable = listing?.files.filter((f) => !f.linked_id) ?? [];
421
+ const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
422
+ return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Link files" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsxRuntime.jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsxRuntime.jsx("code", { className: "sf-muted", children: relative })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), listing && (jsxRuntime.jsxs("div", { className: "sf-browser", children: [listing.parent && (jsxRuntime.jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxRuntime.jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(`${listing.path}/${d}`), children: ["\uD83D\uDCC1 ", d] }, d))), listing.files.length > 0 && (jsxRuntime.jsxs("label", { className: "sf-check sf-browser__all", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: linkable.length > 0 && linkable.every((f) => picked.has(f.path)), disabled: !linkable.length, onChange: (e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set()) }), "Select all files here"] })), listing.files.map((f) => (jsxRuntime.jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsxRuntime.jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => setPicked((p) => {
423
+ const next = new Set(p);
424
+ e.target.checked ? next.add(f.path) : next.delete(f.path);
425
+ return next;
426
+ }) }), jsxRuntime.jsx("span", { className: "sf-browser__name", children: f.name }), jsxRuntime.jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsxRuntime.jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsxRuntime.jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxRuntime.jsxs("div", { className: "sf-row sf-row--bottom", children: [jsxRuntime.jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsxRuntime.jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
427
+ }
428
+ function ServerFiles({ api, folders, onLinked, }) {
429
+ const [roots, setRoots] = react.useState(null);
430
+ const [error, setError] = react.useState(null);
431
+ const load = react.useCallback(async () => {
432
+ try {
433
+ setRoots((await api.roots()).roots);
434
+ setError(null);
435
+ }
436
+ catch (err) {
437
+ setError(errText(err));
438
+ }
439
+ }, [api]);
440
+ return (jsxRuntime.jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsxRuntime.jsx("summary", { children: "Server files (admin)" }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Link files that already live on the server instead of uploading copies. Only files inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original file. Available from the local network only." }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), roots && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "None yet." })) : (jsxRuntime.jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("code", { children: r.path }), " ", r.label && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsxRuntime.jsx("span", { className: "sf-error", children: " (missing)" })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
441
+ try {
442
+ setRoots((await api.removeRoot(r.id)).roots);
443
+ onLinked();
444
+ }
445
+ catch (err) {
446
+ setError(errText(err));
447
+ }
448
+ } })] }, r.id))) }))] }), jsxRuntime.jsx(AddRoot, { api: api, onAdded: setRoots }), jsxRuntime.jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
449
+ }
450
+
451
+ function styleInject(css, ref) {
452
+ if ( ref === void 0 ) ref = {};
453
+ var insertAt = ref.insertAt;
454
+
455
+ if (typeof document === 'undefined') { return; }
456
+
457
+ var head = document.head || document.getElementsByTagName('head')[0];
458
+ var style = document.createElement('style');
459
+ style.type = 'text/css';
460
+
461
+ if (insertAt === 'top') {
462
+ if (head.firstChild) {
463
+ head.insertBefore(style, head.firstChild);
464
+ } else {
465
+ head.appendChild(style);
741
466
  }
742
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "modal-backdrop z-100", onClick: (e) => e.target === e.currentTarget && onClose() }), jsxRuntime.jsx("div", { className: "modal-wrapper z-110", children: jsxRuntime.jsxs("div", { ref: modalRef, role: "dialog", "aria-modal": "true", tabIndex: -1, className: "modal-panel", children: [jsxRuntime.jsxs("h3", { className: "form-title", children: ["Share settings for ", jsxRuntime.jsx("span", { className: "font-mono", children: file.filename })] }), error && jsxRuntime.jsx("p", { className: "form-error", children: error }), jsxRuntime.jsx(Checkbox, { label: "Enable sharing", checked: enabled, onChange: (e) => setEnabled(e.target.checked), className: "checkbox" }), jsxRuntime.jsx(Input, { label: "Password (optional)", type: "text", value: strPassword, onChange: (e) => setStrPassword(e.target.value), disabled: !enabled, className: "form-input" }), jsxRuntime.jsx(Input, { label: "Download limit (0 = \u221E)", type: "number", min: 0, value: download_limit, onChange: (e) => setDownloadLimit(e.target.value === "" ? "" : Number(e.target.value)), disabled: !enabled, className: "form-input" }), enabled && shareLink && (jsxRuntime.jsx(Input, { label: "Share link", readOnly: true, value: shareLink, className: "form-input", trailing: jsxRuntime.jsx(Button, { variant: "icon", className: "btn-icon", onClick: () => navigator.clipboard.writeText(shareLink), children: jsxRuntime.jsx(ForwardRef$1, { className: "h-5 w-5" }) }) })), jsxRuntime.jsxs("div", { className: "mt-6 flex justify-end gap-2", children: [jsxRuntime.jsx(Button, { color: "green", variant: "primary", onClick: handleSave, disabled: busy, className: "btn-primary", children: busy ? "Saving…" : "Save" }), jsxRuntime.jsx(Button, { variant: "secondary", onClick: onClose, disabled: busy, className: "btn-secondary", children: "Cancel" })] })] }) })] }), document.body);
467
+ } else {
468
+ head.appendChild(style);
469
+ }
470
+
471
+ if (style.styleSheet) {
472
+ style.styleSheet.cssText = css;
473
+ } else {
474
+ style.appendChild(document.createTextNode(css));
475
+ }
743
476
  }
744
477
 
745
- // Build a directory tree from a flat file list
478
+ var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}";
479
+ styleInject(css_248z);
480
+
746
481
  function buildTree(files) {
747
- const root = { name: '', path: '', children: new Map(), files: [] };
748
- files.forEach(file => {
749
- const parts = file.filepath.split('/');
482
+ const root = { name: "", path: "", folders: new Map(), files: [] };
483
+ for (const f of files) {
484
+ const parts = f.filepath.split("/");
750
485
  let node = root;
751
- parts.slice(0, -1).forEach((seg, idx) => {
752
- const subPath = parts.slice(0, idx + 1).join('/');
753
- if (!node.children.has(seg)) {
754
- node.children.set(seg, { name: seg, path: subPath, children: new Map(), files: [] });
486
+ parts.slice(0, -1).forEach((part, i) => {
487
+ if (!node.folders.has(part)) {
488
+ node.folders.set(part, { name: part, path: parts.slice(0, i + 1).join("/"), folders: new Map(), files: [] });
755
489
  }
756
- node = node.children.get(seg);
490
+ node = node.folders.get(part);
757
491
  });
758
- node.files.push(file);
759
- });
492
+ node.files.push(f);
493
+ }
760
494
  return root;
761
495
  }
762
- // Collect all file IDs under a node (recursive)
763
- function collectNodeIds(node) {
764
- let ids = [];
765
- node.files.forEach(f => ids.push(f.id));
766
- node.children.forEach(child => {
767
- ids = ids.concat(collectNodeIds(child));
768
- });
769
- return ids;
770
- }
771
- function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelete }) {
772
- const { load } = useFiles();
773
- const [search, setSearch] = React.useState('');
774
- const [modalFile, setModalFile] = React.useState(null);
775
- const [selected, setSelected] = React.useState(new Set());
776
- const [expanded, setExpanded] = React.useState(new Set());
777
- // Upload controls
778
- const [uploadDir, setUploadDir] = React.useState('');
779
- const [uploadFile, setUploadFile] = React.useState(null);
780
- // Filter files by search term
781
- const filtered = React.useMemo(() => files.filter(f => f.filename.toLowerCase().includes(search.trim().toLowerCase())), [files, search]);
782
- // Build tree and directory paths list
783
- const tree = React.useMemo(() => buildTree(filtered), [filtered]);
784
- const allDirPaths = React.useMemo(() => {
785
- const paths = [''];
786
- function collect(node) {
787
- node.children.forEach(child => {
788
- paths.push(child.path);
789
- collect(child);
790
- });
496
+ function allFiles(node) {
497
+ return [...node.files, ...[...node.folders.values()].flatMap(allFiles)];
498
+ }
499
+ function SecureFiles({ apiBase }) {
500
+ const api = react.useMemo(() => createApi(apiBase), [apiBase]);
501
+ const [files, setFiles] = react.useState([]);
502
+ const [user, setUser] = react.useState(null);
503
+ const [isAdmin, setIsAdmin] = react.useState(false);
504
+ const [loading, setLoading] = react.useState(true);
505
+ const [error, setError] = react.useState(null);
506
+ const [notice, setNotice] = react.useState(null);
507
+ const [search, setSearch] = react.useState("");
508
+ const [selected, setSelected] = react.useState(new Set());
509
+ const [collapsed, setCollapsed] = react.useState(new Set());
510
+ const [editing, setEditing] = react.useState(null);
511
+ const [busyIds, setBusyIds] = react.useState(new Set());
512
+ const load = react.useCallback(async () => {
513
+ setError(null);
514
+ try {
515
+ const data = await api.list();
516
+ setUser(data.user_name);
517
+ setIsAdmin(data.is_admin);
518
+ setFiles(data.files);
519
+ setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
791
520
  }
792
- collect(tree);
793
- return paths;
794
- }, [tree]);
795
- // Selection & expand/collapse logic
796
- const allIds = filtered.map(f => f.id);
797
- const toggleAllSelection = () => selected.size < allIds.length ? setSelected(new Set(allIds)) : setSelected(new Set());
798
- const toggleDirOpen = (path) => setExpanded(prev => {
799
- const next = new Set(prev);
800
- next.has(path) ? next.delete(path) : next.add(path);
801
- return next;
802
- });
803
- const expandAll = () => setExpanded(new Set(allDirPaths));
804
- const collapseAll = () => setExpanded(new Set());
805
- // Recursive renderer: directory row with select-all checkbox, then its files, then subdirectories
806
- const renderNode = (node, depth = 0) => {
807
- const rows = [];
808
- if (node.path) {
809
- const isOpen = expanded.has(node.path);
810
- const nodeIds = collectNodeIds(node);
811
- const allSelected = nodeIds.every(id => selected.has(id));
812
- const someSelected = nodeIds.some(id => selected.has(id));
813
- rows.push(jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("td", { className: "px-4 py-2 bg-gray-100", children: jsxRuntime.jsx("input", { type: "checkbox", checked: allSelected, ref: el => { if (el)
814
- el.indeterminate = !allSelected && someSelected; }, onChange: () => {
815
- setSelected(prev => {
816
- const next = new Set(prev);
817
- if (allSelected) {
818
- nodeIds.forEach(id => next.delete(id));
819
- }
820
- else {
821
- nodeIds.forEach(id => next.add(id));
822
- }
823
- return next;
824
- });
825
- } }) }), jsxRuntime.jsx("td", { className: "px-4 py-2 bg-gray-100 font-bold", colSpan: 5, style: { textAlign: 'left' }, children: node.path }), jsxRuntime.jsx("td", { className: "px-4 py-2 bg-gray-100 text-right", children: jsxRuntime.jsx("button", { onClick: () => toggleDirOpen(node.path), children: isOpen ? '▾' : '▸' }) })] }, node.path));
826
- if (!isOpen)
827
- return rows;
521
+ catch (err) {
522
+ setError(err instanceof Error ? err.message : String(err));
828
523
  }
829
- // Render files in this directory
830
- node.files.sort((a, b) => a.filename.localeCompare(b.filename)).forEach(f => {
831
- const isOwner = f.uploader_id.toLowerCase() === (abstractUtilities.currentUsername() ?? '').toLowerCase();
832
- const canDownload = isOwner || f.shareable;
833
- rows.push(jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("td", { className: "px-4 py-2", children: jsxRuntime.jsx("input", { type: "checkbox", checked: selected.has(f.id), onChange: () => setSelected(prev => {
834
- const next = new Set(prev);
835
- next.has(f.id) ? next.delete(f.id) : next.add(f.id);
836
- return next;
837
- }) }) }), jsxRuntime.jsx("td", { className: "px-4 py-2", style: { paddingLeft: (depth + 1) * 20 }, children: f.filename }), jsxRuntime.jsx("td", { className: "px-4 py-2", children: canDownload ? jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => downloadFile(f), children: "Download" }) : jsxRuntime.jsx("span", { children: "\uD83D\uDD12" }) }), jsxRuntime.jsx("td", { className: "px-4 py-2", children: isOwner && jsxRuntime.jsx(Button, { variant: "icon", onClick: () => setModalFile(f), children: "\u2699\uFE0F" }) }), jsxRuntime.jsx("td", { className: "px-4 py-2", children: isOwner && jsxRuntime.jsx(Button, { variant: f.shareable ? 'primary' : 'secondary', onClick: () => onToggleShare(f), children: f.shareable ? 'Sharing' : 'Share' }) }), jsxRuntime.jsx("td", { className: "px-4 py-2", children: isOwner && jsxRuntime.jsx(Button, { variant: "icon", onClick: () => onDelete(f).then(load), children: "\uD83D\uDDD1" }) }), jsxRuntime.jsxs("td", { className: "px-4 py-2", children: [f.download_count, " / ", f.download_limit ?? '∞'] })] }, f.id));
838
- });
839
- // Render subdirectories
840
- Array.from(node.children.values())
841
- .sort((a, b) => a.name.localeCompare(b.name))
842
- .forEach(child => rows.push(...renderNode(child, depth + 1)));
843
- return rows;
844
- };
845
- // Batch actions
846
- const handleDownloadAll = async () => {
847
- const inputs = filtered.filter(f => selected.has(f.id)).map(f => document.createElement('input'));
848
- inputs.forEach((cb, idx) => cb.value = filtered.filter(f => selected.has(f.id))[idx].filepath);
849
- await downloadSelected(inputs);
850
- };
851
- const handleDeleteAll = async () => { await onDelete(filtered.filter(f => selected.has(f.id))); setSelected(new Set()); await load(); };
852
- const handleShareAll = async () => { for (const f of filtered.filter(f => selected.has(f.id)))
853
- await onToggleShare(f); };
854
- if (loading)
855
- return jsxRuntime.jsx(Spinner, {});
856
- if (error)
857
- return jsxRuntime.jsx("p", { className: "text-red-600", children: error.message });
858
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "mb-4 flex flex-col md:flex-row md:justify-between gap-4", children: [jsxRuntime.jsx(Input, { label: "Search files", type: "text", value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Enter filename\u2026", className: "form-input flex-1" }), jsxRuntime.jsxs("div", { className: "flex gap-2", children: [jsxRuntime.jsx(Button, { onClick: expandAll, disabled: !allDirPaths.length, children: "Expand All" }), jsxRuntime.jsx(Button, { onClick: collapseAll, disabled: !allDirPaths.length, children: "Collapse All" }), jsxRuntime.jsx(Button, { onClick: handleDownloadAll, disabled: !selected.size, children: "Download Selected" }), jsxRuntime.jsx(Button, { color: "gray", onClick: handleDeleteAll, disabled: !selected.size, children: "Delete Selected" }), jsxRuntime.jsx(Button, { onClick: handleShareAll, disabled: !selected.size, children: "Share Selected" })] })] }), jsxRuntime.jsx("div", { className: "overflow-x-auto wm-container", children: jsxRuntime.jsxs("table", { className: "file-table w-full", children: [jsxRuntime.jsx("thead", { className: "wm-thead", children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "px-4 py-2 w-12", children: jsxRuntime.jsx("input", { type: "checkbox", checked: selected.size === filtered.length && filtered.length > 0, onChange: toggleAllSelection }) }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Filename" }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Download" }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Settings" }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Share" }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Delete" }), jsxRuntime.jsx("th", { className: "px-4 py-2", children: "Downloads" })] }) }), jsxRuntime.jsx("tbody", { children: renderNode(tree) })] }) }), jsxRuntime.jsx(ShareModal, { file: modalFile, onClose: () => setModalFile(null), onSave: async (patch) => {
859
- if (!modalFile)
860
- return;
861
- const updated = { ...modalFile, ...patch };
862
- await onPatchShare(updated);
863
- setModalFile(null);
864
- } })] }));
865
- }
866
-
867
- // ── helpers ─────────────────────────────────────────────────────────────────
868
- /** Strip filename from a webkitRelativePath, keep the trailing slash. */
869
- function deriveRelPath(f) {
870
- const wrp = f.webkitRelativePath;
871
- if (!wrp)
872
- return '';
873
- const idx = wrp.lastIndexOf('/');
874
- return idx >= 0 ? wrp.slice(0, idx + 1) : '';
875
- }
876
- /**
877
- * Drain a DirectoryReader completely. The browser only returns ~100 entries
878
- * per readEntries() call; you must call it repeatedly until it returns [].
879
- */
880
- function readAllEntries(reader) {
881
- return new Promise((resolve, reject) => {
882
- const all = [];
883
- const pump = () => {
884
- reader.readEntries((batch) => {
885
- if (batch.length === 0)
886
- resolve(all);
887
- else {
888
- all.push(...batch);
889
- pump();
890
- }
891
- }, reject);
892
- };
893
- pump();
894
- });
895
- }
896
- // ── component ───────────────────────────────────────────────────────────────
897
- function Upload({ onUploaded }) {
898
- const [queue, setQueue] = React.useState([]);
899
- const [busy, setBusy] = React.useState(false);
900
- const [message, setMessage] = React.useState(null);
901
- const [drag, setDrag] = React.useState(false);
902
- // Two refs: one for plain file picking, one for folder picking.
903
- const fileInputRef = React.useRef(null);
904
- const dirInputRef = React.useRef(null);
905
- const [pickMode, setPickMode] = React.useState('files');
906
- const [dirs, setDirs] = React.useState([]);
907
- // — existing files → directory list —
908
- const { files, load } = useFiles();
909
- React.useEffect(() => {
910
- const dirSet = new Set();
911
- files.forEach(f => {
912
- const parts = f.filepath.split('/');
913
- if (parts.length > 1) {
914
- parts.pop();
915
- dirSet.add(parts.join('/') + '/');
916
- }
917
- });
918
- setDirs(Array.from(dirSet).sort());
919
- }, [files]);
920
- // — directory selection state —
921
- const [uploadDir, setUploadDir] = React.useState('');
922
- const [creatingNew, setCreatingNew] = React.useState(false);
923
- const [newDirName, setNewDirName] = React.useState('');
924
- // — queue logic —
925
- const enqueue = React.useCallback((incoming) => {
926
- setQueue(prev => {
927
- const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
928
- incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
929
- return Array.from(map.values());
930
- });
931
- }, []);
932
- const extractFromDataTransfer = React.useCallback(async (dt) => {
933
- const items = Array.from(dt.items ?? []);
934
- const hasEntryApi = items.length > 0 &&
935
- typeof items[0].webkitGetAsEntry === 'function';
936
- // Fallback: no entry API → flat files only, no folder traversal.
937
- if (!hasEntryApi) {
938
- return Array.from(dt.files ?? []).map(file => ({
939
- file,
940
- relPath: '',
941
- progress: 0,
942
- }));
524
+ finally {
525
+ setLoading(false);
943
526
  }
944
- const out = [];
945
- const walk = async (entry, path = '') => {
946
- if (!entry)
947
- return;
948
- if (entry.isFile) {
949
- const file = await new Promise((res, rej) => entry.file(res, rej));
950
- out.push({ file, relPath: path, progress: 0 });
951
- return;
952
- }
953
- if (entry.isDirectory) {
954
- const reader = entry.createReader();
955
- const entries = await readAllEntries(reader);
956
- await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
957
- }
527
+ }, [api]);
528
+ react.useEffect(() => {
529
+ load();
530
+ const onVisible = () => document.visibilityState === "visible" && load();
531
+ document.addEventListener("visibilitychange", onVisible);
532
+ window.addEventListener("focus", onVisible);
533
+ return () => {
534
+ document.removeEventListener("visibilitychange", onVisible);
535
+ window.removeEventListener("focus", onVisible);
958
536
  };
959
- await Promise.all(items.map(i => walk(i.webkitGetAsEntry?.())));
960
- return out;
961
- }, []);
962
- const handleSelect = (e) => {
963
- const picked = Array.from(e.target.files ?? []);
964
- const queued = picked.map(f => ({
965
- file: f,
966
- relPath: deriveRelPath(f),
967
- progress: 0,
968
- }));
969
- enqueue(queued);
970
- e.target.value = '';
971
- };
972
- const handleDrop = async (e) => {
973
- e.preventDefault();
974
- e.stopPropagation();
975
- const dropped = await extractFromDataTransfer(e.dataTransfer);
976
- if (dropped.length)
977
- enqueue(dropped);
978
- setDrag(false);
979
- };
980
- const openPicker = (e) => {
981
- // Only fire when the dropzone background itself is clicked, not children.
982
- if (e.target !== e.currentTarget && !e.target.closest('[data-dz-passthrough]')) ;
983
- const ref = pickMode === 'folder' ? dirInputRef : fileInputRef;
984
- ref.current?.click();
985
- };
986
- const handleUploadAll = async () => {
987
- if (!queue.length) {
988
- setMessage('No files queued.');
989
- return;
990
- }
991
- setBusy(true);
992
- setMessage(null);
993
- const targetDir = creatingNew
994
- ? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
995
- : uploadDir;
537
+ }, [load]);
538
+ const run = async (ids, action) => {
539
+ setBusyIds((s) => new Set([...s, ...ids]));
540
+ setNotice(null);
996
541
  try {
997
- for (const { file, relPath } of queue) {
998
- await uploadFiles(file, targetDir + relPath);
999
- setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
1000
- }
1001
- setQueue([]);
1002
- await onUploaded(); // SecureFiles passes filesHook.load here
1003
- setMessage('Upload complete');
1004
- // (removed the duplicate load() — onUploaded already does it)
542
+ await action();
1005
543
  }
1006
- catch {
1007
- setMessage('Upload failed');
544
+ catch (err) {
545
+ setNotice(err instanceof Error ? err.message : String(err));
1008
546
  }
1009
547
  finally {
1010
- setBusy(false);
548
+ setBusyIds((s) => new Set([...s].filter((id) => !ids.includes(id))));
1011
549
  }
1012
550
  };
1013
- // — directory selector handlers —
1014
- const handleDirChange = (e) => {
1015
- const v = e.target.value;
1016
- if (v === '__new__') {
1017
- setCreatingNew(true);
1018
- setNewDirName('');
1019
- }
1020
- else {
1021
- setCreatingNew(false);
1022
- setUploadDir(v);
1023
- }
551
+ const replaceFile = (file) => setFiles((fs) => fs.map((f) => (f.id === file.id ? file : f)));
552
+ const saveShare = async (update) => {
553
+ const { file } = await api.updateShare(update);
554
+ replaceFile(file);
1024
555
  };
1025
- const acceptNewDir = () => {
1026
- const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
1027
- if (!cleanName) {
1028
- setCreatingNew(false);
556
+ const download = (items) => {
557
+ const ready = items.filter((f) => f.available);
558
+ if (!ready.length)
1029
559
  return;
560
+ return run(ready.map((f) => f.id), () => api.download(ready.map((f) => f.id), ready.length === 1 ? ready[0].filename : "downloads.zip"));
561
+ };
562
+ const setShared = (items, shareable) => run(items.map((f) => f.id), async () => {
563
+ for (const f of items)
564
+ await saveShare({ id: f.id, shareable });
565
+ });
566
+ const remove = (items) => run(items.map((f) => f.id), async () => {
567
+ const res = await api.remove(items.map((f) => f.id));
568
+ if (res.errors.length)
569
+ setNotice(`${res.errors.length} file(s) could not be deleted`);
570
+ await load();
571
+ });
572
+ const filtered = react.useMemo(() => {
573
+ const q = search.trim().toLowerCase();
574
+ return q ? files.filter((f) => f.filepath.toLowerCase().includes(q)) : files;
575
+ }, [files, search]);
576
+ const tree = react.useMemo(() => buildTree(filtered), [filtered]);
577
+ const folders = react.useMemo(() => {
578
+ const set = new Set();
579
+ for (const f of files) {
580
+ const parts = f.filepath.split("/").slice(0, -1);
581
+ parts.forEach((_, i) => set.add(parts.slice(0, i + 1).join("/")));
1030
582
  }
1031
- const normalized = `${uploadDir}${cleanName}/`;
1032
- setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
1033
- setUploadDir(normalized);
1034
- setCreatingNew(false);
1035
- setNewDirName('');
583
+ return [...set];
584
+ }, [files]);
585
+ const selectedFiles = files.filter((f) => selected.has(f.id));
586
+ const toggle = (ids, on) => setSelected((prev) => {
587
+ const next = new Set(prev);
588
+ ids.forEach((id) => (on ? next.add(id) : next.delete(id)));
589
+ return next;
590
+ });
591
+ const toggleFolder = (path) => setCollapsed((c) => {
592
+ const next = new Set(c);
593
+ next.has(path) ? next.delete(path) : next.add(path);
594
+ return next;
595
+ });
596
+ const allVisibleSelected = filtered.length > 0 && filtered.every((f) => selected.has(f.id));
597
+ const searching = search.trim().length > 0;
598
+ const renderFile = (f, depth) => {
599
+ const busy = busyIds.has(f.id);
600
+ return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
1036
601
  };
1037
- return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxRuntime.jsxs("div", { className: "flex gap-4 text-xs text-gray-700 mb-1", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'files', onChange: () => setPickMode('files') }), "Files"] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'folder', onChange: () => setPickMode('folder') }), "Folder"] }), jsxRuntime.jsx("span", { className: "text-gray-400", children: "(drag-and-drop accepts either)" })] }), jsxRuntime.jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
1038
- ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: openPicker, onDragEnter: e => { e.preventDefault(); e.stopPropagation(); setDrag(true); }, onDragOver: e => { e.preventDefault(); e.stopPropagation(); }, onDragLeave: e => { e.preventDefault(); e.stopPropagation(); setDrag(false); }, onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsxRuntime.jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 mt-1", children: ["Click to ", pickMode === 'folder' ? 'choose a folder' : 'choose files'] }), queue.length > 0 && (jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: handleSelect }), jsxRuntime.jsx("input", { ref: dirInputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: '', directory: '' }, onChange: handleSelect }), jsxRuntime.jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsxRuntime.jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxRuntime.jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsxRuntime.jsx("option", { value: d, children: d }, d))), jsxRuntime.jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxRuntime.jsxs("div", { className: "flex", children: [jsxRuntime.jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsxRuntime.jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsxRuntime.jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsxRuntime.jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxRuntime.jsxs("div", { className: "wm-container mt-4", children: [jsxRuntime.jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsxRuntime.jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxRuntime.jsxs("li", { className: "space-y-1", children: [jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsxRuntime.jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
602
+ const renderFolder = (node, depth) => {
603
+ const rows = [];
604
+ const subFolders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name));
605
+ const ownFiles = [...node.files].sort((a, b) => a.filename.localeCompare(b.filename));
606
+ for (const folder of subFolders) {
607
+ const contents = allFiles(folder);
608
+ const ids = contents.map((f) => f.id);
609
+ const open = searching || !collapsed.has(folder.path);
610
+ rows.push(jsxRuntime.jsxs("tr", { className: "sf-dir", children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select folder ${folder.path}`, checked: ids.length > 0 && ids.every((id) => selected.has(id)), onChange: (e) => toggle(ids, e.target.checked) }) }), jsxRuntime.jsx("td", { colSpan: 4, style: { paddingLeft: 10 + depth * 20 }, children: jsxRuntime.jsxs("button", { type: "button", className: "sf-dir__toggle", onClick: () => toggleFolder(folder.path), children: [open ? "▾" : "▸", " \uD83D\uDCC1 ", folder.name, " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: ["(", contents.length, ")"] })] }) })] }, `dir:${folder.path}`));
611
+ if (open)
612
+ rows.push(...renderFolder(folder, depth + 1));
613
+ }
614
+ rows.push(...ownFiles.map((f) => renderFile(f, depth)));
615
+ return rows;
616
+ };
617
+ return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
1039
618
  }
1040
619
 
1041
- const SecureFiles = () => {
1042
- const [authChecked, setAuthChecked] = React.useState(false);
1043
- const [loggedIn, setLoggedIn] = React.useState(false);
1044
- const filesHook = useFiles();
1045
- React.useEffect(() => {
1046
- checkAuth().then(ok => {
1047
- setLoggedIn(ok);
1048
- setAuthChecked(true);
1049
- });
1050
- }, []);
1051
- if (!authChecked)
1052
- return null;
1053
- if (!loggedIn) {
1054
- return (jsxRuntime.jsx("p", { style: { textAlign: 'center', marginTop: '2rem', color: '#6b7280' }, children: "Please log in to access your files." }));
1055
- }
1056
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Upload, { onUploaded: filesHook.load }), jsxRuntime.jsx(FileTable, { files: filesHook.files, loading: filesHook.loading, error: filesHook.error, onDownload: filesHook.toggleShare, onToggleShare: filesHook.toggleShare, onPatchShare: filesHook.patchShareSettings, onDelete: filesHook.deleteFile })] }));
1057
- };
1058
-
1059
- exports.Button = Button;
1060
- exports.Checkbox = Checkbox;
1061
- exports.FileRow = FileRow;
1062
- exports.FileTable = FileTable;
1063
- exports.Input = Input;
620
+ exports.ApiError = ApiError;
621
+ exports.DEFAULT_API_BASE = DEFAULT_API_BASE;
1064
622
  exports.SecureFiles = SecureFiles;
1065
- exports.ShareModal = ShareModal;
1066
- exports.Spinner = Spinner;
1067
- exports.Upload = Upload;
1068
- exports.directDownload = directDownload;
1069
- exports.downloadFile = downloadFile;
1070
- exports.downloadRow = downloadRow;
1071
- exports.downloadSelect = downloadSelect;
1072
- exports.downloadSelected = downloadSelected;
1073
- exports.initSelectAll = initSelectAll;
1074
- exports.listFiles = listFiles;
1075
- exports.patchFileSetting = patchFileSetting;
1076
- exports.removeFile = removeFile;
1077
- exports.removeRow = removeRow;
1078
- exports.removeSelected = removeSelected;
1079
- exports.syncHeaderCheckbox = syncHeaderCheckbox;
1080
- exports.toggleCheckboxes = toggleCheckboxes;
1081
- exports.toggleShare = toggleShare;
1082
- exports.triggerDownload = triggerDownload;
1083
- exports.uploadFile = uploadFile;
1084
- exports.uploadFiles = uploadFiles;
1085
- exports.useDeleteFile = useDeleteFile;
1086
- exports.useFiles = useFiles;
1087
- exports.useLoadFiles = useLoadFiles;
1088
- exports.usePatchShareSettings = usePatchShareSettings;
1089
- exports.useSaveShareSettings = useSaveShareSettings;
1090
- exports.useToggleShare = useToggleShare;
623
+ exports.createApi = createApi;
1091
624
  //# sourceMappingURL=index.cjs.map