@putkoff/abstract-securefiles 0.0.33 → 0.0.34

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 (181) hide show
  1. package/css.d.ts +0 -0
  2. package/dist/App/SecureFiles.d.ts +4 -0
  3. package/dist/App/index.d.ts +1 -0
  4. package/dist/FileHandlers/FileRow/imports.d.ts +2 -0
  5. package/dist/FileHandlers/FileRow/index.d.ts +1 -0
  6. package/dist/FileHandlers/FileRow/src/index.d.ts +1 -0
  7. package/dist/FileHandlers/FileRow/src/row_utils.d.ts +2 -0
  8. package/dist/FileHandlers/FileTable/imports.d.ts +4 -0
  9. package/dist/FileHandlers/FileTable/index.d.ts +1 -0
  10. package/dist/FileHandlers/FileTable/src/index.d.ts +1 -0
  11. package/dist/FileHandlers/FileTable/src/table_utils.d.ts +2 -0
  12. package/dist/FileHandlers/ShareModal/imports.d.ts +2 -0
  13. package/dist/FileHandlers/ShareModal/index.d.ts +1 -0
  14. package/dist/FileHandlers/ShareModal/src/index.d.ts +1 -0
  15. package/dist/FileHandlers/ShareModal/src/share_utils.d.ts +2 -0
  16. package/dist/FileHandlers/Upload/imports.d.ts +2 -0
  17. package/dist/FileHandlers/Upload/index.d.ts +1 -0
  18. package/dist/FileHandlers/Upload/src/index.d.ts +2 -0
  19. package/dist/FileHandlers/Upload/src/upload_utils.d.ts +2 -0
  20. package/dist/FileHandlers/imports.d.ts +4 -0
  21. package/dist/FileHandlers/index.d.ts +4 -0
  22. package/dist/UI/index.d.ts +28 -0
  23. package/dist/file_utils/imports.d.ts +2 -0
  24. package/dist/file_utils/index.d.ts +1 -0
  25. package/dist/file_utils/src/cookieFetch.d.ts +22 -0
  26. package/dist/file_utils/src/delete_utils/imports.d.ts +1 -0
  27. package/dist/file_utils/src/delete_utils/index.d.ts +1 -0
  28. package/dist/file_utils/src/delete_utils/src/delete_row.d.ts +2 -0
  29. package/dist/file_utils/src/delete_utils/src/delete_select.d.ts +4 -0
  30. package/dist/file_utils/src/delete_utils/src/delete_selected.d.ts +2 -0
  31. package/dist/file_utils/src/delete_utils/src/index.d.ts +3 -0
  32. package/dist/file_utils/src/download_utils/imports.d.ts +2 -0
  33. package/dist/file_utils/src/download_utils/index.d.ts +1 -0
  34. package/dist/file_utils/src/download_utils/src/direct_download.d.ts +3 -0
  35. package/dist/file_utils/src/download_utils/src/download_file.d.ts +6 -0
  36. package/dist/file_utils/src/download_utils/src/download_select.d.ts +2 -0
  37. package/dist/file_utils/src/download_utils/src/index.d.ts +5 -0
  38. package/dist/file_utils/src/download_utils/src/target_downloads.d.ts +4 -0
  39. package/dist/file_utils/src/download_utils/src/trigger_download.d.ts +2 -0
  40. package/dist/file_utils/src/imports.d.ts +2 -0
  41. package/dist/file_utils/src/index.d.ts +6 -0
  42. package/dist/file_utils/src/list_utils/imports.d.ts +2 -0
  43. package/dist/file_utils/src/list_utils/index.d.ts +1 -0
  44. package/dist/file_utils/src/list_utils/src/index.d.ts +1 -0
  45. package/dist/file_utils/src/list_utils/src/list_utils.d.ts +2 -0
  46. package/dist/file_utils/src/select_utils/index.d.ts +1 -0
  47. package/dist/file_utils/src/select_utils/src/index.d.ts +3 -0
  48. package/dist/file_utils/src/select_utils/src/init_select_all.d.ts +2 -0
  49. package/dist/file_utils/src/select_utils/src/sync_header_checkbox.d.ts +5 -0
  50. package/dist/file_utils/src/select_utils/src/toggle_checkboxes.d.ts +2 -0
  51. package/dist/file_utils/src/share_utils/imports.d.ts +2 -0
  52. package/dist/file_utils/src/share_utils/index.d.ts +1 -0
  53. package/dist/file_utils/src/share_utils/src/index.d.ts +1 -0
  54. package/dist/file_utils/src/share_utils/src/share_utils.d.ts +7 -0
  55. package/{package.tmp.json → dist/file_utils/src/upload_utils/imports.d.ts} +0 -0
  56. package/dist/file_utils/src/upload_utils/index.d.ts +1 -0
  57. package/dist/file_utils/src/upload_utils/src/index.d.ts +3 -0
  58. package/dist/file_utils/src/upload_utils/src/patch_file_settings.d.ts +2 -0
  59. package/dist/file_utils/src/upload_utils/src/upload_file.d.ts +2 -0
  60. package/dist/file_utils/src/upload_utils/src/upload_files.d.ts +5 -0
  61. package/dist/hooks/imports.d.ts +3 -0
  62. package/dist/hooks/index.d.ts +1 -0
  63. package/dist/hooks/useFiles/imports.d.ts +2 -0
  64. package/dist/hooks/useFiles/index.d.ts +1 -0
  65. package/dist/hooks/useFiles/src/deleteFile.d.ts +3 -0
  66. package/dist/hooks/useFiles/src/index.d.ts +6 -0
  67. package/dist/hooks/useFiles/src/load.d.ts +3 -0
  68. package/dist/hooks/useFiles/src/patchShareSettings.d.ts +3 -0
  69. package/dist/hooks/useFiles/src/toggleShare.d.ts +3 -0
  70. package/dist/hooks/useFiles/src/useFiles.d.ts +13 -0
  71. package/dist/hooks/useFiles/src/useSaveShareSettings.d.ts +7 -0
  72. package/dist/imports.d.ts +7 -0
  73. package/dist/index.cjs +992 -8
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.css +1 -1
  76. package/dist/index.d.ts +8 -1
  77. package/dist/index.js +945 -10
  78. package/dist/index.js.map +1 -1
  79. package/dist/styles/index.d.ts +1 -0
  80. package/dist/types/index.d.ts +1 -0
  81. package/dist/types/src/index.d.ts +1 -0
  82. package/dist/types/src/types.d.ts +75 -0
  83. package/index.ts +0 -0
  84. package/package.json +4 -4
  85. package/postcss.config.cjs +0 -0
  86. package/rollup.config.js +0 -0
  87. package/rollup.config.mjs +0 -0
  88. package/src/App/SecureFiles.tsx +45 -0
  89. package/src/App/index.ts +1 -0
  90. package/src/FileHandlers/FileRow/imports.ts +2 -0
  91. package/src/FileHandlers/FileRow/index.ts +1 -0
  92. package/src/FileHandlers/FileRow/src/index.ts +1 -0
  93. package/src/FileHandlers/FileRow/src/row_utils.tsx +143 -0
  94. package/src/FileHandlers/FileTable/imports.ts +16 -0
  95. package/src/FileHandlers/FileTable/index.ts +1 -0
  96. package/src/FileHandlers/FileTable/src/index.ts +1 -0
  97. package/src/FileHandlers/FileTable/src/table_utils.tsx +251 -0
  98. package/src/FileHandlers/ShareModal/imports.ts +2 -0
  99. package/src/FileHandlers/ShareModal/index.ts +1 -0
  100. package/src/FileHandlers/ShareModal/src/index.ts +1 -0
  101. package/src/FileHandlers/ShareModal/src/share_utils.tsx +155 -0
  102. package/src/FileHandlers/Upload/imports.ts +3 -0
  103. package/src/FileHandlers/Upload/index.ts +1 -0
  104. package/src/FileHandlers/Upload/src/index.ts +2 -0
  105. package/src/FileHandlers/Upload/src/upload_utils.tsx +267 -0
  106. package/src/FileHandlers/imports.ts +18 -0
  107. package/src/FileHandlers/index.ts +4 -0
  108. package/src/UI/index.tsx +122 -0
  109. package/src/custom.d.ts +7 -0
  110. package/src/file_utils/imports.ts +2 -0
  111. package/src/file_utils/index.ts +1 -0
  112. package/src/file_utils/src/cookieFetch.ts +78 -0
  113. package/src/file_utils/src/delete_utils/imports.ts +1 -0
  114. package/src/file_utils/src/delete_utils/index.ts +1 -0
  115. package/src/file_utils/src/delete_utils/src/delete_row.ts +11 -0
  116. package/src/file_utils/src/delete_utils/src/delete_select.ts +10 -0
  117. package/src/file_utils/src/delete_utils/src/delete_selected.ts +15 -0
  118. package/src/file_utils/src/delete_utils/src/index.ts +3 -0
  119. package/src/file_utils/src/download_utils/imports.ts +3 -0
  120. package/src/file_utils/src/download_utils/index.ts +1 -0
  121. package/src/file_utils/src/download_utils/src/direct_download.ts +22 -0
  122. package/src/file_utils/src/download_utils/src/download_file.ts +25 -0
  123. package/src/file_utils/src/download_utils/src/download_select.ts +20 -0
  124. package/src/file_utils/src/download_utils/src/index.ts +5 -0
  125. package/src/file_utils/src/download_utils/src/target_downloads.ts +21 -0
  126. package/src/file_utils/src/download_utils/src/trigger_download.ts +14 -0
  127. package/src/file_utils/src/imports.ts +3 -0
  128. package/src/file_utils/src/index.ts +6 -0
  129. package/src/file_utils/src/list_utils/imports.ts +2 -0
  130. package/src/file_utils/src/list_utils/index.ts +1 -0
  131. package/src/file_utils/src/list_utils/src/index.ts +1 -0
  132. package/src/file_utils/src/list_utils/src/list_utils.ts +28 -0
  133. package/src/file_utils/src/select_utils/index.ts +1 -0
  134. package/src/file_utils/src/select_utils/src/index.ts +3 -0
  135. package/src/file_utils/src/select_utils/src/init_select_all.ts +37 -0
  136. package/src/file_utils/src/select_utils/src/sync_header_checkbox.ts +18 -0
  137. package/src/file_utils/src/select_utils/src/toggle_checkboxes.ts +11 -0
  138. package/src/file_utils/src/share_utils/imports.ts +2 -0
  139. package/src/file_utils/src/share_utils/index.ts +1 -0
  140. package/src/file_utils/src/share_utils/src/index.ts +1 -0
  141. package/src/file_utils/src/share_utils/src/share_utils.ts +20 -0
  142. package/src/file_utils/src/upload_utils/imports.ts +1 -0
  143. package/src/file_utils/src/upload_utils/index.ts +1 -0
  144. package/src/file_utils/src/upload_utils/src/index.ts +3 -0
  145. package/src/file_utils/src/upload_utils/src/patch_file_settings.ts +20 -0
  146. package/src/file_utils/src/upload_utils/src/upload_file.ts +27 -0
  147. package/src/file_utils/src/upload_utils/src/upload_files.ts +19 -0
  148. package/src/global.d.ts +1 -0
  149. package/src/hooks/imports.ts +3 -0
  150. package/src/hooks/index.ts +1 -0
  151. package/src/hooks/useFiles/imports.ts +3 -0
  152. package/src/hooks/useFiles/index.ts +1 -0
  153. package/src/hooks/useFiles/src/deleteFile.ts +23 -0
  154. package/src/hooks/useFiles/src/index.ts +6 -0
  155. package/src/hooks/useFiles/src/load.ts +39 -0
  156. package/src/hooks/useFiles/src/logs/clipit_logs.log +0 -0
  157. package/src/hooks/useFiles/src/logs/routes_bp.log +0 -0
  158. package/src/hooks/useFiles/src/patchShareSettings.ts +27 -0
  159. package/src/hooks/useFiles/src/toggleShare.ts +28 -0
  160. package/src/hooks/useFiles/src/useFiles.ts +35 -0
  161. package/src/hooks/useFiles/src/useSaveShareSettings.ts +31 -0
  162. package/src/imports.ts +19 -0
  163. package/src/index.ts +8 -1
  164. package/src/styles/{App.css → SecureFiles/App.css} +0 -0
  165. package/src/styles/{index.css → SecureFiles/index.css} +0 -0
  166. package/src/styles/SecureFiles/logs/clipit_logs.log +0 -0
  167. package/src/styles/SecureFiles/logs/routes_bp.log +0 -0
  168. package/src/styles/{tailwind.css → SecureFiles/tailwind.css} +0 -0
  169. package/src/styles/{window-manager.css → SecureFiles/window-manager.css} +0 -0
  170. package/src/styles/SecureFilesStyles.module.css +287 -0
  171. package/src/styles/index.ts +1 -0
  172. package/src/types/index.ts +1 -0
  173. package/src/types/src/index.ts +1 -0
  174. package/src/types/src/types.ts +88 -0
  175. package/tailwind.config.cjs +0 -0
  176. package/testit.mjs +0 -0
  177. package/testit.ts +0 -0
  178. package/tsconfig.json +0 -0
  179. package/.github/workflows/release.yml +0 -17
  180. package/dist/secure_files.d.ts +0 -4
  181. package/src/secure_files.tsx +0 -41
package/dist/index.cjs CHANGED
@@ -1,19 +1,1003 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var react = require('react');
5
- var abstractFiles = require('@putkoff/abstract-files');
4
+ var reactDom = require('react-dom');
5
+ var React = require('react');
6
6
  var abstractUtilities = require('@putkoff/abstract-utilities');
7
+ var abstractApis = require('@putkoff/abstract-apis');
7
8
 
8
- function checkTokenValid() {
9
- const tok = abstractUtilities.getToken();
10
- return !!tok && !abstractUtilities.isTokenExpired(tok);
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
11
24
  }
25
+
26
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
27
+
28
+ function ClipboardIcon({
29
+ title,
30
+ titleId,
31
+ ...props
32
+ }, svgRef) {
33
+ return /*#__PURE__*/React__namespace.createElement("svg", Object.assign({
34
+ xmlns: "http://www.w3.org/2000/svg",
35
+ fill: "none",
36
+ viewBox: "0 0 24 24",
37
+ strokeWidth: 1.5,
38
+ stroke: "currentColor",
39
+ "aria-hidden": "true",
40
+ "data-slot": "icon",
41
+ ref: svgRef,
42
+ "aria-labelledby": titleId
43
+ }, props), title ? /*#__PURE__*/React__namespace.createElement("title", {
44
+ id: titleId
45
+ }, title) : null, /*#__PURE__*/React__namespace.createElement("path", {
46
+ strokeLinecap: "round",
47
+ strokeLinejoin: "round",
48
+ 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"
49
+ }));
50
+ }
51
+ const ForwardRef$1 = /*#__PURE__*/ React__namespace.forwardRef(ClipboardIcon);
52
+
53
+ function CloudArrowUpIcon({
54
+ title,
55
+ titleId,
56
+ ...props
57
+ }, svgRef) {
58
+ return /*#__PURE__*/React__namespace.createElement("svg", Object.assign({
59
+ xmlns: "http://www.w3.org/2000/svg",
60
+ fill: "none",
61
+ viewBox: "0 0 24 24",
62
+ strokeWidth: 1.5,
63
+ stroke: "currentColor",
64
+ "aria-hidden": "true",
65
+ "data-slot": "icon",
66
+ ref: svgRef,
67
+ "aria-labelledby": titleId
68
+ }, props), title ? /*#__PURE__*/React__namespace.createElement("title", {
69
+ id: titleId
70
+ }, title) : null, /*#__PURE__*/React__namespace.createElement("path", {
71
+ strokeLinecap: "round",
72
+ strokeLinejoin: "round",
73
+ 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"
74
+ }));
75
+ }
76
+ const ForwardRef = /*#__PURE__*/ React__namespace.forwardRef(CloudArrowUpIcon);
77
+
78
+ // Utility to convert input to array
79
+ function ensure_list(obj) {
80
+ if (Array.isArray(obj))
81
+ return obj.filter(Boolean);
82
+ if (typeof obj === 'string')
83
+ return obj.split(' ').filter(Boolean);
84
+ return [];
85
+ }
86
+ // Get list and length safely
87
+ function getListAndLen(obj) {
88
+ const return_obj = ensure_list(obj);
89
+ const ob_len = return_obj.length;
90
+ return { return_obj, ob_len };
91
+ }
92
+ // Combine class names with defaults
93
+ function combineClassNames(classNames = [], defaultClasses = ['form-group', 'form-label', 'flex', 'gap-2']) {
94
+ const { return_obj: inputClasses } = getListAndLen(classNames);
95
+ const { return_obj: defaultClassList } = getListAndLen(defaultClasses);
96
+ // Combine and remove duplicates
97
+ const combined = [...new Set([...defaultClassList, ...inputClasses])];
98
+ console.log(`Combined classes (length: ${combined.length}): ${combined.join(' ')}`);
99
+ return combined;
100
+ }
101
+ function Button({ children, color = 'gray', variant = 'default', className = '', ...rest }) {
102
+ const variantClasses = {
103
+ default: 'btn',
104
+ icon: 'btn-icon',
105
+ primary: 'btn-primary',
106
+ secondary: 'btn-secondary',
107
+ };
108
+ const colorClasses = {
109
+ gray: '',
110
+ green: variant === 'primary' ? 'btn-success' : 'btn-secondary',
111
+ blue: variant === 'primary' ? 'btn-primary' : 'btn-secondary',
112
+ };
113
+ const classes = combineClassNames([variantClasses[variant], colorClasses[color], className]);
114
+ return (jsxRuntime.jsx("button", { className: classes.join(' '), ...rest, children: children }));
115
+ }
116
+ function Checkbox({ label, className = '', classNames = [], ...rest }) {
117
+ const classes = combineClassNames(classNames, ['checkbox', 'wm-checkbox']);
118
+ return (jsxRuntime.jsxs("label", { className: classes.join(' '), children: [jsxRuntime.jsx("input", { type: "checkbox", className: "wm-checkbox", ...rest }), label && jsxRuntime.jsx("span", { children: label })] }));
119
+ }
120
+ function Input({ label, trailing, className = '', classNames = [], ...rest }) {
121
+ const classes = combineClassNames(classNames, ['form-group', 'flex', 'gap-2']);
122
+ const inputClasses = combineClassNames(className, ['form-input']);
123
+ 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] })] }));
124
+ }
125
+ function Spinner() {
126
+ const classes = combineClassNames('animate-pulse');
127
+ return jsxRuntime.jsx("p", { className: classes.join(' '), children: "Loading\u2026" });
128
+ }
129
+
130
+ /** Helper function to trigger file download */
131
+ function triggerDownload(blob, filename) {
132
+ const url = URL.createObjectURL(blob);
133
+ const a = Object.assign(document.createElement("a"), {
134
+ href: url,
135
+ download: filename,
136
+ });
137
+ document.body.appendChild(a);
138
+ a.click();
139
+ a.remove();
140
+ URL.revokeObjectURL(url);
141
+ console.log(`✅ Successfully downloaded: ${filename}`);
142
+ }
143
+
144
+ /** Non-React “direct download” used by some old links (kept for parity) */
145
+ async function directDownload(item) {
146
+ const token = localStorage.getItem("token");
147
+ if (!token)
148
+ throw new Error("No auth token found");
149
+ const res = await fetch("https://abstractendeavors.com/api/secure-files/secure-download/", {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${token}`,
153
+ "Content-Type": "application/json",
154
+ Accept: "application/octet-stream",
155
+ },
156
+ body: JSON.stringify(item),
157
+ });
158
+ if (!res.ok)
159
+ throw new Error(`Download failed: ${res.status}`);
160
+ const blob = await res.blob();
161
+ triggerDownload(blob, item.filename);
162
+ }
163
+
164
+ /**
165
+ * Download a file using the current session cookie.
166
+ * No JWT / localStorage token required.
167
+ */
168
+ async function downloadFile(item) {
169
+ const res = await fetch("/api/secure-files/download", {
170
+ method: "POST",
171
+ credentials: "include",
172
+ headers: { "Content-Type": "application/json" },
173
+ body: JSON.stringify({
174
+ id: item.id,
175
+ filepath: item.filepath,
176
+ filename: item.filename,
177
+ }),
178
+ });
179
+ if (res.status === 401)
180
+ throw new Error("Not authenticated – please log in.");
181
+ if (!res.ok)
182
+ throw new Error(`Download failed: HTTP ${res.status}`);
183
+ const blob = await res.blob();
184
+ triggerDownload(blob, item.filename || "download");
185
+ }
186
+
187
+ async function downloadSelect(item) {
188
+ const token = abstractUtilities.requireToken();
189
+ const body = item;
190
+ const headers = {
191
+ Authorization: `Bearer ${token}`,
192
+ Accept: "application/octet-stream",
193
+ "Content-Type": "application/json"
194
+ };
195
+ const blob = await abstractApis.secureFetchIt("download", body, "POST", headers, true, true);
196
+ if (!blob || blob.size === 0) {
197
+ throw new Error(`Received empty file data for “${item.filename}”`);
198
+ }
199
+ triggerDownload(blob, item.filename);
200
+ }
201
+
202
+ /** Given a checkbox inside a table-row → download that row’s file. */
203
+ async function downloadRow(cb) {
204
+ const row = cb.closest("tr");
205
+ const filepath = cb.value;
206
+ const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
207
+ if (filepath)
208
+ await downloadSelect({ filepath, filename });
209
+ }
210
+ /** Bulk download: pass a NodeList or array of checkboxes. */
211
+ async function downloadSelected(checkboxes) {
212
+ const list = Array.from(checkboxes);
213
+ if (!list.length) {
214
+ alert("No files selected.");
215
+ return;
216
+ }
217
+ for (const cb of list) {
218
+ await downloadRow(cb);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Classic “select all rows” logic (still handy in non-React views
224
+ * such as the admin dashboard).
225
+ */
226
+ function syncHeaderCheckbox() {
227
+ const all = document.querySelectorAll(".file-checkbox");
228
+ const checked = document.querySelectorAll(".file-checkbox:checked");
229
+ const header = document.getElementById("select-all-checkbox");
230
+ header.indeterminate =
231
+ checked.length > 0 && checked.length < all.length;
232
+ header.checked = checked.length === all.length;
233
+ }
234
+
235
+ /** Wire up header + row checkboxes once the table is in the DOM. */
236
+ function initSelectAll() {
237
+ const header = document.getElementById("select-all-checkbox");
238
+ const btn = document.getElementById("select-all");
239
+ // 1) header → toggle all
240
+ header.addEventListener("change", (e) => {
241
+ const on = e.target.checked;
242
+ document
243
+ .querySelectorAll(".file-checkbox")
244
+ .forEach((cb) => (cb.checked = on));
245
+ });
246
+ // 2) button → toggle all
247
+ btn.addEventListener("click", () => {
248
+ const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
249
+ if (!cbs.length) {
250
+ alert("No files to select.");
251
+ return;
252
+ }
253
+ const anyUnchecked = cbs.some((cb) => !cb.checked);
254
+ cbs.forEach((cb) => (cb.checked = anyUnchecked));
255
+ syncHeaderCheckbox();
256
+ });
257
+ // 3) row boxes → keep header state
258
+ document
259
+ .querySelectorAll(".file-checkbox")
260
+ .forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
261
+ syncHeaderCheckbox();
262
+ }
263
+
264
+ /** Utility for other screens: toggle a list of checkboxes on/off. */
265
+ function toggleCheckboxes(cbs) {
266
+ if (!cbs.length) {
267
+ alert("No files to select.");
268
+ return;
269
+ }
270
+ const anyUnchecked = cbs.some((cb) => !cb.checked);
271
+ cbs.forEach((cb) => (cb.checked = anyUnchecked));
272
+ syncHeaderCheckbox();
273
+ }
274
+
275
+ const API_BASE$1 = '/api/secure-files';
276
+ /** Generic PATCH helper for file-level admin settings. */
277
+ async function patchFileSetting(fileId, field, value) {
278
+ const res = await fetch(`${API_BASE$1}/files/${fileId}/settings/${field}`, {
279
+ method: 'PATCH',
280
+ credentials: 'include',
281
+ headers: { 'Content-Type': 'application/json' },
282
+ body: JSON.stringify({ value }),
283
+ });
284
+ if (!res.ok) {
285
+ const err = await res.json().catch(() => ({}));
286
+ throw new Error(err.error || res.statusText);
287
+ }
288
+ return (await res.json());
289
+ }
290
+
291
+ /**
292
+ * Cookie-authenticated API helpers.
293
+ * All requests include credentials: 'include' so the browser sends the
294
+ * session cookie (auth_session) automatically. No JWT tokens are used.
295
+ */
296
+ const API_BASE = "/api/secure-files";
297
+ function apiUrl(path) {
298
+ const p = path.startsWith("/") ? path : `/${path}`;
299
+ return `${API_BASE}${p}`;
300
+ }
301
+ /**
302
+ * Generic JSON fetch with session-cookie auth.
303
+ * Throws Error("UNAUTHENTICATED") on 401 so callers can redirect to login.
304
+ */
305
+ async function cookieFetch(path, body, method) {
306
+ const m = method ?? (body !== undefined ? "POST" : "GET");
307
+ const init = { method: m, credentials: "include" };
308
+ if (body !== undefined && body !== null) {
309
+ init.headers = { "Content-Type": "application/json" };
310
+ init.body = JSON.stringify(body);
311
+ }
312
+ const res = await fetch(apiUrl(path), init);
313
+ if (res.status === 401)
314
+ throw new Error("UNAUTHENTICATED");
315
+ if (!res.ok) {
316
+ const msg = await res.text().catch(() => res.statusText);
317
+ throw new Error(`HTTP ${res.status}: ${msg}`);
318
+ }
319
+ const ct = res.headers.get("Content-Type") ?? "";
320
+ return (ct.includes("application/json") ? res.json() : res.text());
321
+ }
322
+ /**
323
+ * Check whether the current session is valid.
324
+ * Calls /userInfo which requires authentication; returns true if the user is
325
+ * logged in, false otherwise (no exception thrown).
326
+ */
327
+ async function checkAuth() {
328
+ try {
329
+ const res = await fetch(apiUrl("/userInfo"), {
330
+ method: "GET",
331
+ credentials: "include",
332
+ });
333
+ return res.ok;
334
+ }
335
+ catch {
336
+ return false;
337
+ }
338
+ }
339
+ /**
340
+ * Upload a single file using multipart/form-data with session-cookie auth.
341
+ * @param file The File object to upload.
342
+ * @param relPath Optional relative path within the user's directory.
343
+ */
344
+ async function cookieUpload(file, relPath = "") {
345
+ const form = new FormData();
346
+ form.append("file", file);
347
+ if (relPath)
348
+ form.append("rel_path", relPath);
349
+ const res = await fetch(apiUrl("/upload"), {
350
+ method: "POST",
351
+ credentials: "include",
352
+ body: form,
353
+ });
354
+ if (res.status === 401)
355
+ throw new Error("UNAUTHENTICATED");
356
+ if (!res.ok)
357
+ throw new Error(`Upload failed: HTTP ${res.status}`);
358
+ return res.json();
359
+ }
360
+
361
+ /** File-upload helper (used by non-React fallback pages). */
362
+ async function uploadFile(file, subdir) {
363
+ const fileInput = file || document.getElementById('file-browser');
364
+ const subdirInput = subdir || document.getElementById('subdir');
365
+ const banner = document.getElementById('banner');
366
+ if (!fileInput.files?.length) {
367
+ alert('No file selected.');
368
+ return;
369
+ }
370
+ const f = fileInput.files[0];
371
+ const dir = (subdirInput?.value ?? '').trim();
372
+ const relPath = dir ? `${dir}/${f.name}` : f.name;
373
+ try {
374
+ const json = await cookieUpload(f, relPath);
375
+ if (banner)
376
+ banner.textContent = `Uploaded: ${json.filepath}`;
377
+ fileInput.value = '';
378
+ if (subdirInput)
379
+ subdirInput.value = '';
380
+ window.location.reload();
381
+ }
382
+ catch (err) {
383
+ console.error('Upload failed', err);
384
+ }
385
+ }
386
+
387
+ async function uploadFiles(file, subdir = '') {
388
+ if (!(file instanceof File))
389
+ throw new Error('Invalid file: must be a File object.');
390
+ if (typeof subdir !== 'string')
391
+ throw new Error('Invalid subdir: must be a string.');
392
+ const relPath = subdir.trim() ? `${subdir.trim()}/${file.name}` : file.name;
393
+ const json = await cookieUpload(file, relPath);
394
+ if (!json?.filepath)
395
+ throw new Error('Server response missing filepath.');
396
+ return { filepath: json.filepath };
397
+ }
398
+
399
+ async function removeFile(item) {
400
+ const resp = await cookieFetch("remove", { id: item.id }, "POST");
401
+ if (!resp || resp === false || resp?.result === false) {
402
+ throw new Error(`Could not delete "${item.filename ?? item.id}"`);
403
+ }
404
+ }
405
+
406
+ /** Bulk download: pass a NodeList or array of checkboxes. */
407
+ async function removeSelected(checkboxes) {
408
+ const list = Array.from(checkboxes);
409
+ if (!list.length) {
410
+ alert("No files selected.");
411
+ return;
412
+ }
413
+ for (const cb of list) {
414
+ alert(cb);
415
+ await removeFile(cb);
416
+ }
417
+ }
418
+
419
+ /** Given a checkbox inside a table-row → download that row’s file. */
420
+ async function removeRow(cb) {
421
+ const row = cb.closest("tr");
422
+ abstractUtilities.alertit(row);
423
+ if (row && row?.id)
424
+ await removeFile(row);
425
+ }
426
+
427
+ async function listFiles(setFiles = null) {
428
+ const result = await cookieFetch("list", undefined, "GET");
429
+ const apiRows = Array.isArray(result)
430
+ ? result
431
+ : (result?.result ?? []);
432
+ const rows = apiRows.map((r) => ({
433
+ id: r.id,
434
+ filename: r.filename,
435
+ uploader_id: r.uploader_id,
436
+ filepath: r.filepath,
437
+ created_at: r.created_at,
438
+ download_count: r.download_count,
439
+ download_limit: r.download_limit,
440
+ shareable: Boolean(r.shareable),
441
+ needsPassword: !!r.share_password,
442
+ share_password: r.share_password,
443
+ password_str: r.password_str,
444
+ }));
445
+ if (setFiles)
446
+ setFiles(rows);
447
+ return rows;
448
+ }
449
+
450
+ /**
451
+ * Toggle sharing on a file.
452
+ * Sends the updated settings to /api/secure-files/files/share.
453
+ * Does NOT update local state – the caller is responsible for that.
454
+ */
455
+ async function toggleShare(file) {
456
+ const updated = { ...file, shareable: !file.shareable };
457
+ if (!updated.shareable)
458
+ updated.needsPassword = false;
459
+ await cookieFetch("files/share", {
460
+ id: updated.id,
461
+ shareable: updated.shareable,
462
+ share_password: updated.share_password ?? null,
463
+ password_str: updated.password_str ?? null,
464
+ download_limit: updated.download_limit ?? null,
465
+ }, "POST");
466
+ }
467
+
468
+ const useLoadFiles = (setLoading, setError, setFiles) => {
469
+ return React.useCallback(async () => {
470
+ setLoading(true);
471
+ setError(null);
472
+ try {
473
+ const result = await cookieFetch("list", undefined, "GET");
474
+ const apiRows = Array.isArray(result)
475
+ ? result
476
+ : (result?.result ?? []);
477
+ setFiles(apiRows.map((r) => ({
478
+ id: r.id,
479
+ filename: r.filename,
480
+ uploader_id: r.uploader_id,
481
+ filepath: r.filepath,
482
+ created_at: r.created_at,
483
+ download_count: r.download_count,
484
+ download_limit: r.download_limit,
485
+ shareable: Boolean(r.shareable),
486
+ needsPassword: !!r.share_password,
487
+ share_password: r.share_password,
488
+ password_str: r.password_str,
489
+ })));
490
+ }
491
+ catch (err) {
492
+ setError(err instanceof Error ? err : new Error('Failed to load files'));
493
+ }
494
+ finally {
495
+ setLoading(false);
496
+ }
497
+ }, [setLoading, setError, setFiles]);
498
+ };
499
+
500
+ const useToggleShare = (setFiles) => {
501
+ return React.useCallback(async (file) => {
502
+ const updated = { ...file, shareable: !file.shareable };
503
+ if (!updated.shareable)
504
+ updated.needsPassword = false;
505
+ try {
506
+ await cookieFetch("files/share", {
507
+ id: updated.id,
508
+ shareable: updated.shareable,
509
+ share_password: updated.share_password ?? null,
510
+ password_str: updated.password_str ?? null,
511
+ download_limit: updated.download_limit ?? null,
512
+ }, "POST");
513
+ setFiles(curr => curr.map(f => f.id === file.id ? updated : f));
514
+ }
515
+ catch (err) {
516
+ console.error('Failed to toggle share:', err);
517
+ }
518
+ }, [setFiles]);
519
+ };
520
+
521
+ const usePatchShareSettings = (setFiles) => {
522
+ return React.useCallback(async (file) => {
523
+ const body = {
524
+ id: file.id,
525
+ shareable: file.shareable,
526
+ share_password: file.share_password ?? null,
527
+ password_str: file.password_str ?? null,
528
+ download_limit: file.download_limit ?? null,
529
+ };
530
+ try {
531
+ await cookieFetch("files/share", body, "POST");
532
+ setFiles(curr => curr.map(f => f.id === body.id ? { ...f, ...body } : f));
533
+ }
534
+ catch (err) {
535
+ console.error('Failed to update share settings:', err);
536
+ }
537
+ }, [setFiles]);
538
+ };
539
+
540
+ /**
541
+ * Hook alias used by useFiles.ts. Patches share settings via the session-cookie
542
+ * authenticated /files/share endpoint.
543
+ */
544
+ const useSaveShareSettings = (setFiles) => {
545
+ return React.useCallback(async (file) => {
546
+ const body = {
547
+ id: file.id,
548
+ shareable: file.shareable,
549
+ share_password: file.share_password ?? null,
550
+ password_str: file.password_str ?? null,
551
+ download_limit: file.download_limit ?? null,
552
+ };
553
+ try {
554
+ await cookieFetch("files/share", body, "POST");
555
+ setFiles(curr => curr.map(f => f.id === body.id ? { ...f, ...body } : f));
556
+ }
557
+ catch (err) {
558
+ console.error('Failed to save share settings:', err);
559
+ }
560
+ }, [setFiles]);
561
+ };
562
+
563
+ const useDeleteFile = (setFiles) => {
564
+ return React.useCallback(async (file) => {
565
+ const targets = Array.isArray(file) ? file : [file];
566
+ for (const f of targets) {
567
+ try {
568
+ await cookieFetch("remove", { id: f.id }, "POST");
569
+ setFiles(curr => curr.filter(item => item.id !== f.id));
570
+ }
571
+ catch (err) {
572
+ console.error(`Failed to delete "${f.filename}":`, err);
573
+ }
574
+ }
575
+ }, [setFiles]);
576
+ };
577
+
578
+ function useFiles() {
579
+ const [files, setFiles] = React.useState([]);
580
+ const [loading, setLoading] = React.useState(false);
581
+ const [error, setError] = React.useState(null);
582
+ const loadCallback = useLoadFiles(setLoading, setError, setFiles);
583
+ const toggleShareCallback = useToggleShare(setFiles);
584
+ const patchShareSettingsCallback = useSaveShareSettings(setFiles);
585
+ const deleteFileCallback = useDeleteFile(setFiles);
586
+ React.useEffect(() => {
587
+ loadCallback();
588
+ }, [loadCallback]);
589
+ return {
590
+ files,
591
+ loading,
592
+ error,
593
+ load: loadCallback,
594
+ toggleShare: toggleShareCallback,
595
+ patchShareSettings: patchShareSettingsCallback,
596
+ deleteFile: deleteFileCallback,
597
+ setFiles,
598
+ setLoading,
599
+ setError,
600
+ };
601
+ }
602
+
603
+ function FileRow({ file, onDownload, onSettings, onToggleShare, onDelete, }) {
604
+ const isOwner = file.uploader_id?.toLowerCase() === (abstractUtilities.currentUsername() ?? '').toLowerCase();
605
+ const downloadAllowed = isOwner || file.shareable;
606
+ const [isDownloading, setIsDownloading] = React.useState(false);
607
+ const [isTogglingShare, setIsTogglingShare] = React.useState(false);
608
+ const [isDeleting, setIsDeleting] = React.useState(false);
609
+ // Mimic WindowManagerApp's unsaved heuristic
610
+ const looksUnsaved = file.filename.toLowerCase().startsWith('untitled') ||
611
+ file.filename.includes('*');
612
+ const handleDownload = async () => {
613
+ setIsDownloading(true);
614
+ try {
615
+ await onDownload();
616
+ }
617
+ finally {
618
+ setIsDownloading(false);
619
+ }
620
+ };
621
+ const handleToggleShare = async () => {
622
+ setIsTogglingShare(true);
623
+ try {
624
+ await onToggleShare();
625
+ }
626
+ finally {
627
+ setIsTogglingShare(false);
628
+ }
629
+ };
630
+ const handleDeleteFile = async () => {
631
+ setIsDeleting(true);
632
+ try {
633
+ await onDelete();
634
+ }
635
+ finally {
636
+ setIsDeleting(false);
637
+ }
638
+ };
639
+ 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: () => {
640
+ console.log('⚙︎ clicked', file.id);
641
+ onSettings();
642
+ }, "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
643
+ ? 'Toggling...'
644
+ : file.shareable
645
+ ? 'Sharing'
646
+ : '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 ?? "∞"] })] }));
647
+ }
648
+
649
+ function ShareModal({ file, onClose, onSave }) {
650
+ const [fileId, setFileId] = React.useState("");
651
+ const [enabled, setEnabled] = React.useState(false);
652
+ const [password, setPassword] = React.useState(undefined);
653
+ const [strPassword, setStrPassword] = React.useState("");
654
+ const [download_limit, setDownloadLimit] = React.useState(0);
655
+ const [error, setError] = React.useState("");
656
+ const [busy, setBusy] = React.useState(false);
657
+ const modalRef = React.useRef(null);
658
+ const shareLink = file && enabled
659
+ ? `/api/secure-files/secure-download/${file.id}`
660
+ : "";
661
+ React.useEffect(() => {
662
+ if (!file)
663
+ return;
664
+ setFileId(file.id);
665
+ setEnabled(file.shareable);
666
+ setPassword(file.share_password);
667
+ setStrPassword(file.password_str ?? "");
668
+ setDownloadLimit(file.download_limit ?? 0);
669
+ setError("");
670
+ modalRef.current?.focus();
671
+ }, [file]);
672
+ React.useEffect(() => {
673
+ const h = (e) => e.key === "Escape" && onClose();
674
+ window.addEventListener("keydown", h);
675
+ return () => window.removeEventListener("keydown", h);
676
+ }, [onClose]);
677
+ if (!file)
678
+ return null;
679
+ async function handleSave() {
680
+ if (strPassword && strPassword.length < 4) {
681
+ setError("Password must be at least 4 characters");
682
+ return;
683
+ }
684
+ setBusy(true);
685
+ const save_dict = {
686
+ id: fileId,
687
+ shareable: enabled,
688
+ needsPassword: !!strPassword,
689
+ share_password: strPassword || null,
690
+ password_str: strPassword || null,
691
+ download_limit: download_limit === "" ? null : Number(download_limit),
692
+ };
693
+ try {
694
+ const resp = await onSave(save_dict);
695
+ onClose();
696
+ }
697
+ catch (err) {
698
+ setError(`${err} Failed to save settings. Please try again.`);
699
+ }
700
+ finally {
701
+ setBusy(false);
702
+ }
703
+ }
704
+ 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);
705
+ }
706
+
707
+ // Build a directory tree from a flat file list
708
+ function buildTree(files) {
709
+ const root = { name: '', path: '', children: new Map(), files: [] };
710
+ files.forEach(file => {
711
+ const parts = file.filepath.split('/');
712
+ let node = root;
713
+ parts.slice(0, -1).forEach((seg, idx) => {
714
+ const subPath = parts.slice(0, idx + 1).join('/');
715
+ if (!node.children.has(seg)) {
716
+ node.children.set(seg, { name: seg, path: subPath, children: new Map(), files: [] });
717
+ }
718
+ node = node.children.get(seg);
719
+ });
720
+ node.files.push(file);
721
+ });
722
+ return root;
723
+ }
724
+ // Collect all file IDs under a node (recursive)
725
+ function collectNodeIds(node) {
726
+ let ids = [];
727
+ node.files.forEach(f => ids.push(f.id));
728
+ node.children.forEach(child => {
729
+ ids = ids.concat(collectNodeIds(child));
730
+ });
731
+ return ids;
732
+ }
733
+ function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelete }) {
734
+ const { load } = useFiles();
735
+ const [search, setSearch] = React.useState('');
736
+ const [modalFile, setModalFile] = React.useState(null);
737
+ const [selected, setSelected] = React.useState(new Set());
738
+ const [expanded, setExpanded] = React.useState(new Set());
739
+ // Upload controls
740
+ const [uploadDir, setUploadDir] = React.useState('');
741
+ const [uploadFile, setUploadFile] = React.useState(null);
742
+ // Filter files by search term
743
+ const filtered = React.useMemo(() => files.filter(f => f.filename.toLowerCase().includes(search.trim().toLowerCase())), [files, search]);
744
+ // Build tree and directory paths list
745
+ const tree = React.useMemo(() => buildTree(filtered), [filtered]);
746
+ const allDirPaths = React.useMemo(() => {
747
+ const paths = [''];
748
+ function collect(node) {
749
+ node.children.forEach(child => {
750
+ paths.push(child.path);
751
+ collect(child);
752
+ });
753
+ }
754
+ collect(tree);
755
+ return paths;
756
+ }, [tree]);
757
+ // Selection & expand/collapse logic
758
+ const allIds = filtered.map(f => f.id);
759
+ const toggleAllSelection = () => selected.size < allIds.length ? setSelected(new Set(allIds)) : setSelected(new Set());
760
+ const toggleDirOpen = (path) => setExpanded(prev => {
761
+ const next = new Set(prev);
762
+ next.has(path) ? next.delete(path) : next.add(path);
763
+ return next;
764
+ });
765
+ const expandAll = () => setExpanded(new Set(allDirPaths));
766
+ const collapseAll = () => setExpanded(new Set());
767
+ // Recursive renderer: directory row with select-all checkbox, then its files, then subdirectories
768
+ const renderNode = (node, depth = 0) => {
769
+ const rows = [];
770
+ if (node.path) {
771
+ const isOpen = expanded.has(node.path);
772
+ const nodeIds = collectNodeIds(node);
773
+ const allSelected = nodeIds.every(id => selected.has(id));
774
+ const someSelected = nodeIds.some(id => selected.has(id));
775
+ 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)
776
+ el.indeterminate = !allSelected && someSelected; }, onChange: () => {
777
+ setSelected(prev => {
778
+ const next = new Set(prev);
779
+ if (allSelected) {
780
+ nodeIds.forEach(id => next.delete(id));
781
+ }
782
+ else {
783
+ nodeIds.forEach(id => next.add(id));
784
+ }
785
+ return next;
786
+ });
787
+ } }) }), 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));
788
+ if (!isOpen)
789
+ return rows;
790
+ }
791
+ // Render files in this directory
792
+ node.files.sort((a, b) => a.filename.localeCompare(b.filename)).forEach(f => {
793
+ const isOwner = f.uploader_id.toLowerCase() === (abstractUtilities.currentUsername() ?? '').toLowerCase();
794
+ const canDownload = isOwner || f.shareable;
795
+ 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 => {
796
+ const next = new Set(prev);
797
+ next.has(f.id) ? next.delete(f.id) : next.add(f.id);
798
+ return next;
799
+ }) }) }), 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));
800
+ });
801
+ // Render subdirectories
802
+ Array.from(node.children.values())
803
+ .sort((a, b) => a.name.localeCompare(b.name))
804
+ .forEach(child => rows.push(...renderNode(child, depth + 1)));
805
+ return rows;
806
+ };
807
+ // Batch actions
808
+ const handleDownloadAll = async () => {
809
+ const inputs = filtered.filter(f => selected.has(f.id)).map(f => document.createElement('input'));
810
+ inputs.forEach((cb, idx) => cb.value = filtered.filter(f => selected.has(f.id))[idx].filepath);
811
+ await downloadSelected(inputs);
812
+ };
813
+ const handleDeleteAll = async () => { await onDelete(filtered.filter(f => selected.has(f.id))); setSelected(new Set()); await load(); };
814
+ const handleShareAll = async () => { for (const f of filtered.filter(f => selected.has(f.id)))
815
+ await onToggleShare(f); };
816
+ if (loading)
817
+ return jsxRuntime.jsx(Spinner, {});
818
+ if (error)
819
+ return jsxRuntime.jsx("p", { className: "text-red-600", children: error.message });
820
+ 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) => {
821
+ if (!modalFile)
822
+ return;
823
+ const updated = { ...modalFile, ...patch };
824
+ await onPatchShare(updated);
825
+ setModalFile(null);
826
+ } })] }));
827
+ }
828
+
829
+ function Upload({ onUploaded }) {
830
+ const [queue, setQueue] = React.useState([]);
831
+ const [busy, setBusy] = React.useState(false);
832
+ const [message, setMessage] = React.useState(null);
833
+ const [drag, setDrag] = React.useState(false);
834
+ const inputRef = React.useRef(null);
835
+ const [dirs, setDirs] = React.useState([]);
836
+ // — existing files → directory list —
837
+ const { files, load } = useFiles();
838
+ React.useEffect(() => {
839
+ const dirSet = new Set();
840
+ files.forEach(f => {
841
+ const parts = f.filepath.split('/');
842
+ if (parts.length > 1) {
843
+ parts.pop();
844
+ dirSet.add(parts.join('/') + '/');
845
+ }
846
+ });
847
+ setDirs(Array.from(dirSet).sort());
848
+ }, [files]);
849
+ // — directory selection state —
850
+ const [uploadDir, setUploadDir] = React.useState('');
851
+ const [creatingNew, setCreatingNew] = React.useState(false);
852
+ const [newDirName, setNewDirName] = React.useState('');
853
+ // — queue logic —
854
+ const enqueue = React.useCallback((files) => {
855
+ setQueue(prev => {
856
+ const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
857
+ files.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
858
+ return Array.from(map.values());
859
+ });
860
+ }, []);
861
+ const extractFromDataTransfer = React.useCallback(async (dt) => {
862
+ const out = [];
863
+ const walk = async (entry, path = "") => {
864
+ if (!entry)
865
+ return;
866
+ if (entry.isFile) {
867
+ const file = await new Promise(res => entry.file(res));
868
+ out.push({ file, relPath: path, progress: 0 });
869
+ }
870
+ else if (entry.isDirectory) {
871
+ const reader = entry.createReader();
872
+ const entries = await new Promise(r => reader.readEntries(r));
873
+ await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
874
+ }
875
+ };
876
+ await Promise.all(Array.from(dt.items).map(i => walk(i.webkitGetAsEntry())));
877
+ return out;
878
+ }, []);
879
+ const handleSelect = (e) => {
880
+ const files = Array.from(e.target.files || []);
881
+ const queued = files.map(f => ({
882
+ file: f,
883
+ relPath: f.webkitRelativePath.replace(/[^/]*$/, ""),
884
+ progress: 0,
885
+ }));
886
+ enqueue(queued);
887
+ e.target.value = "";
888
+ };
889
+ const handleDrop = async (e) => {
890
+ e.preventDefault();
891
+ const files = await extractFromDataTransfer(e.dataTransfer);
892
+ enqueue(files.length ? files : []);
893
+ setDrag(false);
894
+ };
895
+ const handleUploadAll = async () => {
896
+ if (!queue.length) {
897
+ setMessage("No files queued.");
898
+ return;
899
+ }
900
+ setBusy(true);
901
+ setMessage(null);
902
+ const targetDir = creatingNew
903
+ ? (newDirName.trim().replace(/^\/+|\/+$/g, "") + "/")
904
+ : uploadDir;
905
+ try {
906
+ for (const { file, relPath } of queue) {
907
+ await uploadFiles(file, targetDir + relPath);
908
+ setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
909
+ }
910
+ setQueue([]);
911
+ await onUploaded();
912
+ setMessage("Upload complete");
913
+ load();
914
+ }
915
+ catch {
916
+ setMessage("Upload failed");
917
+ }
918
+ finally {
919
+ setBusy(false);
920
+ }
921
+ };
922
+ // — directory selector handlers —
923
+ const handleDirChange = (e) => {
924
+ const v = e.target.value;
925
+ if (v === "__new__") {
926
+ setCreatingNew(true);
927
+ setNewDirName("");
928
+ }
929
+ else {
930
+ setCreatingNew(false);
931
+ setUploadDir(v);
932
+ }
933
+ };
934
+ const acceptNewDir = () => {
935
+ // normalize like before
936
+ const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
937
+ const normalized = `${uploadDir}${cleanName}/`;
938
+ // ① add it to our dirs list if it isn't there already
939
+ setDirs(prev => {
940
+ if (prev.includes(normalized))
941
+ return prev;
942
+ return [...prev, normalized].sort();
943
+ });
944
+ // ② select it & flip back to the <select>
945
+ setUploadDir(normalized);
946
+ setCreatingNew(false);
947
+ setNewDirName('');
948
+ };
949
+ return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [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
950
+ ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: () => inputRef.current?.click(), onDragEnter: e => { e.preventDefault(); setDrag(true); }, onDragOver: e => e.preventDefault(), onDragLeave: () => 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" }), 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: inputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: true, 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}`))) })] }))] }));
951
+ }
952
+
12
953
  const SecureFiles = () => {
13
- const [loggedIn, setLoggedIn] = react.useState(checkTokenValid());
14
- const filesHook = abstractFiles.useFiles(); // ← one single hook instance
15
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(abstractFiles.Upload, { onUploaded: filesHook.load }), jsxRuntime.jsx(abstractFiles.FileTable, { files: filesHook.files, loading: filesHook.loading, error: filesHook.error, onDownload: filesHook.toggleShare, onToggleShare: filesHook.toggleShare, onPatchShare: filesHook.patchShareSettings, onDelete: filesHook.deleteFile })] }));
954
+ const [authChecked, setAuthChecked] = React.useState(false);
955
+ const [loggedIn, setLoggedIn] = React.useState(false);
956
+ const filesHook = useFiles();
957
+ React.useEffect(() => {
958
+ checkAuth().then(ok => {
959
+ setLoggedIn(ok);
960
+ setAuthChecked(true);
961
+ });
962
+ }, []);
963
+ if (!authChecked)
964
+ return null;
965
+ if (!loggedIn) {
966
+ return (jsxRuntime.jsx("p", { style: { textAlign: 'center', marginTop: '2rem', color: '#6b7280' }, children: "Please log in to access your files." }));
967
+ }
968
+ 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 })] }));
16
969
  };
17
970
 
971
+ exports.Button = Button;
972
+ exports.Checkbox = Checkbox;
973
+ exports.FileRow = FileRow;
974
+ exports.FileTable = FileTable;
975
+ exports.Input = Input;
18
976
  exports.SecureFiles = SecureFiles;
977
+ exports.ShareModal = ShareModal;
978
+ exports.Spinner = Spinner;
979
+ exports.Upload = Upload;
980
+ exports.directDownload = directDownload;
981
+ exports.downloadFile = downloadFile;
982
+ exports.downloadRow = downloadRow;
983
+ exports.downloadSelect = downloadSelect;
984
+ exports.downloadSelected = downloadSelected;
985
+ exports.initSelectAll = initSelectAll;
986
+ exports.listFiles = listFiles;
987
+ exports.patchFileSetting = patchFileSetting;
988
+ exports.removeFile = removeFile;
989
+ exports.removeRow = removeRow;
990
+ exports.removeSelected = removeSelected;
991
+ exports.syncHeaderCheckbox = syncHeaderCheckbox;
992
+ exports.toggleCheckboxes = toggleCheckboxes;
993
+ exports.toggleShare = toggleShare;
994
+ exports.triggerDownload = triggerDownload;
995
+ exports.uploadFile = uploadFile;
996
+ exports.uploadFiles = uploadFiles;
997
+ exports.useDeleteFile = useDeleteFile;
998
+ exports.useFiles = useFiles;
999
+ exports.useLoadFiles = useLoadFiles;
1000
+ exports.usePatchShareSettings = usePatchShareSettings;
1001
+ exports.useSaveShareSettings = useSaveShareSettings;
1002
+ exports.useToggleShare = useToggleShare;
19
1003
  //# sourceMappingURL=index.cjs.map