@putkoff/abstract-securefiles 0.0.50 → 0.0.52
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.
- package/dist/MyComputers.d.ts +7 -0
- package/dist/api.d.ts +12 -1
- package/dist/index.cjs +107 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +107 -30
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +16 -2
- package/package.json +1 -1
- package/src/MyComputers.tsx +141 -0
- package/src/SecureFiles.tsx +25 -6
- package/src/ShareDialog.tsx +26 -5
- package/src/api.ts +8 -2
- package/src/index.ts +1 -1
- package/src/styles.css +38 -0
- package/src/types.ts +17 -2
package/dist/index.js
CHANGED
|
@@ -35,6 +35,9 @@ function createApi(apiBase = DEFAULT_API_BASE) {
|
|
|
35
35
|
}
|
|
36
36
|
return {
|
|
37
37
|
list: () => json("/userInfo"),
|
|
38
|
+
devices: () => json("/devices"),
|
|
39
|
+
pairDevice: () => json("/devices/pair", { method: "POST", body: "{}" }),
|
|
40
|
+
removeDevice: (id) => json(`/devices/${id}`, { method: "DELETE" }),
|
|
38
41
|
roots: () => json("/admin/roots"),
|
|
39
42
|
requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
|
|
40
43
|
confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
|
|
@@ -243,6 +246,25 @@ function CopyButton({ text, label = "Copy link" }) {
|
|
|
243
246
|
return (jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
250
|
+
const [armed, setArmed] = useState(false);
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!armed)
|
|
253
|
+
return;
|
|
254
|
+
const t = setTimeout(() => setArmed(false), 3000);
|
|
255
|
+
return () => clearTimeout(t);
|
|
256
|
+
}, [armed]);
|
|
257
|
+
return (jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
258
|
+
if (armed) {
|
|
259
|
+
setArmed(false);
|
|
260
|
+
onConfirm();
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
setArmed(true);
|
|
264
|
+
}
|
|
265
|
+
}, children: armed ? "Confirm" : label }));
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
function ShareDialog({ file, onClose, onSave, }) {
|
|
247
269
|
const [shareable, setShareable] = useState(file.shareable);
|
|
248
270
|
const [limit, setLimit] = useState(file.download_limit ? String(file.download_limit) : "");
|
|
@@ -258,6 +280,19 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
258
280
|
}, [onClose]);
|
|
259
281
|
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
260
282
|
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
283
|
+
const newLink = async () => {
|
|
284
|
+
setSaving(true);
|
|
285
|
+
setError(null);
|
|
286
|
+
try {
|
|
287
|
+
await onSave({ id: file.id, shareable: true, regenerate_link: true });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
setSaving(false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
261
296
|
const save = async () => {
|
|
262
297
|
setSaving(true);
|
|
263
298
|
setError(null);
|
|
@@ -282,26 +317,7 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
282
317
|
setSaving(false);
|
|
283
318
|
}
|
|
284
319
|
};
|
|
285
|
-
return (jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxs("header", { className: "sf-dialog__head", children: [jsx("h3", { children: "Share settings" }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsx("p", { className: "sf-muted", children: file.filepath }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Download limit (blank = unlimited)" }), jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Password" }), jsxs("div", { className: "sf-row", children: [file.has_password && (jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Share link" }), jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsx(CopyButton, { text: file.share_url })] }), !file.shareable && jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })] })] }), error && jsx("p", { className: "sf-error", children: error }), jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
289
|
-
const [armed, setArmed] = useState(false);
|
|
290
|
-
useEffect(() => {
|
|
291
|
-
if (!armed)
|
|
292
|
-
return;
|
|
293
|
-
const t = setTimeout(() => setArmed(false), 3000);
|
|
294
|
-
return () => clearTimeout(t);
|
|
295
|
-
}, [armed]);
|
|
296
|
-
return (jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
297
|
-
if (armed) {
|
|
298
|
-
setArmed(false);
|
|
299
|
-
onConfirm();
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
setArmed(true);
|
|
303
|
-
}
|
|
304
|
-
}, children: armed ? "Confirm" : label }));
|
|
320
|
+
return (jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxs("header", { className: "sf-dialog__head", children: [jsx("h3", { children: "Share settings" }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsx("p", { className: "sf-muted", children: file.filepath }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Download limit (blank = unlimited)" }), jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Password" }), jsxs("div", { className: "sf-row", children: [file.has_password && (jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Share link" }), file.share_url ? (jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsx(CopyButton, { text: file.share_url }), jsx(ConfirmButton, { label: "New link", disabled: saving, onConfirm: newLink })] })) : (jsx("span", { className: "sf-muted", children: "A private link is created when you save with sharing on." })), file.share_url && !file.shareable && (jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })), jsx("span", { className: "sf-muted", children: "\"New link\" invalidates the current one immediately." })] })] }), error && jsx("p", { className: "sf-error", children: error }), jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
305
321
|
}
|
|
306
322
|
|
|
307
323
|
function formatSize(bytes) {
|
|
@@ -314,7 +330,7 @@ function formatSize(bytes) {
|
|
|
314
330
|
}
|
|
315
331
|
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
316
332
|
}
|
|
317
|
-
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
333
|
+
const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
|
|
318
334
|
function AddRoot({ api, onAdded }) {
|
|
319
335
|
const [path, setPath] = useState("");
|
|
320
336
|
const [label, setLabel] = useState("");
|
|
@@ -330,7 +346,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
330
346
|
setSignature("");
|
|
331
347
|
}
|
|
332
348
|
catch (err) {
|
|
333
|
-
setError(errText(err));
|
|
349
|
+
setError(errText$1(err));
|
|
334
350
|
}
|
|
335
351
|
finally {
|
|
336
352
|
setBusy(false);
|
|
@@ -349,7 +365,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
349
365
|
setSignature("");
|
|
350
366
|
}
|
|
351
367
|
catch (err) {
|
|
352
|
-
setError(errText(err));
|
|
368
|
+
setError(errText$1(err));
|
|
353
369
|
}
|
|
354
370
|
finally {
|
|
355
371
|
setBusy(false);
|
|
@@ -372,7 +388,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
372
388
|
setPicked(new Set());
|
|
373
389
|
}
|
|
374
390
|
catch (err) {
|
|
375
|
-
setError(errText(err));
|
|
391
|
+
setError(errText$1(err));
|
|
376
392
|
}
|
|
377
393
|
}, [api]);
|
|
378
394
|
useEffect(() => {
|
|
@@ -400,7 +416,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
400
416
|
await open(listing.path);
|
|
401
417
|
}
|
|
402
418
|
catch (err) {
|
|
403
|
-
setResult(errText(err));
|
|
419
|
+
setResult(errText$1(err));
|
|
404
420
|
}
|
|
405
421
|
finally {
|
|
406
422
|
setBusy(false);
|
|
@@ -424,7 +440,7 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
424
440
|
setError(null);
|
|
425
441
|
}
|
|
426
442
|
catch (err) {
|
|
427
|
-
setError(errText(err));
|
|
443
|
+
setError(errText$1(err));
|
|
428
444
|
}
|
|
429
445
|
}, [api]);
|
|
430
446
|
return (jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsx("summary", { children: "Server files (admin)" }), jsxs("div", { className: "sf-stack", children: [jsx("p", { className: "sf-muted", children: "Link files or whole folders that already live on the server instead of uploading copies. A linked folder downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only items inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original. Available from the local network only." }), error && jsx("p", { className: "sf-error", children: error }), roots && (jsxs(Fragment, { children: [jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsx("p", { className: "sf-muted", children: "None yet." })) : (jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxs("li", { children: [jsxs("span", { children: [jsx("code", { children: r.path }), " ", r.label && jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsx("span", { className: "sf-error", children: " (missing)" })] }), jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
@@ -433,11 +449,70 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
433
449
|
onLinked();
|
|
434
450
|
}
|
|
435
451
|
catch (err) {
|
|
436
|
-
setError(errText(err));
|
|
452
|
+
setError(errText$1(err));
|
|
437
453
|
}
|
|
438
454
|
} })] }, r.id))) }))] }), jsx(AddRoot, { api: api, onAdded: setRoots }), jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
|
|
439
455
|
}
|
|
440
456
|
|
|
457
|
+
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
458
|
+
function ago(iso) {
|
|
459
|
+
if (!iso)
|
|
460
|
+
return "never";
|
|
461
|
+
const secs = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
|
|
462
|
+
if (secs < 90)
|
|
463
|
+
return "just now";
|
|
464
|
+
if (secs < 3600)
|
|
465
|
+
return `${Math.round(secs / 60)} min ago`;
|
|
466
|
+
if (secs < 86400)
|
|
467
|
+
return `${Math.round(secs / 3600)} h ago`;
|
|
468
|
+
return `${Math.round(secs / 86400)} d ago`;
|
|
469
|
+
}
|
|
470
|
+
function AddComputer({ api, onPaired }) {
|
|
471
|
+
const [code, setCode] = useState(null);
|
|
472
|
+
const [expiresAt, setExpiresAt] = useState(0);
|
|
473
|
+
const [, setTick] = useState(0);
|
|
474
|
+
const [error, setError] = useState(null);
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
if (!code)
|
|
477
|
+
return;
|
|
478
|
+
const timer = setInterval(() => {
|
|
479
|
+
if (Date.now() > expiresAt)
|
|
480
|
+
setCode(null);
|
|
481
|
+
setTick((t) => t + 1);
|
|
482
|
+
onPaired();
|
|
483
|
+
}, 5000);
|
|
484
|
+
return () => clearInterval(timer);
|
|
485
|
+
}, [code, expiresAt, onPaired]);
|
|
486
|
+
const create = async () => {
|
|
487
|
+
setError(null);
|
|
488
|
+
try {
|
|
489
|
+
const res = await api.pairDevice();
|
|
490
|
+
setCode(res.code);
|
|
491
|
+
setExpiresAt(Date.now() + res.expires_in * 1000);
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
setError(errText(err));
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const minutesLeft = Math.max(1, Math.round((expiresAt - Date.now()) / 60000));
|
|
498
|
+
if (!code) {
|
|
499
|
+
return (jsxs("div", { className: "sf-row", children: [jsx("button", { type: "button", className: "sf-btn sf-btn--primary", onClick: create, children: "Add a computer" }), error && jsx("span", { className: "sf-error", children: error })] }));
|
|
500
|
+
}
|
|
501
|
+
return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Connect a computer" }), jsxs("ol", { className: "sf-steps", children: [jsxs("li", { children: ["On that computer, install the agent (Linux, macOS or Windows with Python 3.9+):", jsxs("div", { className: "sf-row", children: [jsx("code", { className: "sf-codeline", children: "pipx install abstract-securefiles && securefiles setup" }), jsx(CopyButton, { text: "pipx install abstract-securefiles && securefiles setup", label: "Copy" })] }), jsxs("span", { className: "sf-muted", children: ["No pipx? Use ", jsx("code", { children: "python3 -m pip install --user abstract-securefiles" }), " then ", jsx("code", { children: "securefiles setup" }), "."] })] }), jsxs("li", { children: ["In the panel that opens (", jsx("code", { children: "http://127.0.0.1:47821" }), "), enter this code:", jsxs("div", { className: "sf-row", children: [jsx("span", { className: "sf-paircode", children: code }), jsx(CopyButton, { text: code, label: "Copy" })] }), jsxs("span", { className: "sf-muted", children: ["Valid for about ", minutesLeft, " more minutes, once."] })] }), jsxs("li", { children: ["Pick the files and folders to share there. They appear here under ", jsx("code", { children: "devices/<computer>/" }), "."] })] }), jsx("div", { className: "sf-row", children: jsx("button", { type: "button", className: "sf-btn", onClick: () => setCode(null), children: "Done" }) })] }));
|
|
502
|
+
}
|
|
503
|
+
function MyComputers({ api, devices, onChange, }) {
|
|
504
|
+
const [error, setError] = useState(null);
|
|
505
|
+
return (jsxs("details", { className: "sf-admin", children: [jsxs("summary", { children: ["My computers", " ", jsxs("span", { className: "sf-muted", children: ["(", devices.filter((d) => d.online).length, " online / ", devices.length, ")"] })] }), jsxs("div", { className: "sf-stack", children: [jsx("p", { className: "sf-muted", children: "Share files and folders straight from your own computers \u2014 nothing is uploaded. Downloads stream from the computer through this site with the same links, passwords, limits and resume, while it's online." }), devices.length > 0 && (jsx("ul", { className: "sf-roots", children: devices.map((d) => (jsxs("li", { children: [jsxs("span", { children: [jsx("span", { className: `sf-dot${d.online ? " sf-dot--on" : ""}` }), " ", jsx("b", { children: d.name }), " ", jsxs("span", { className: "sf-muted", children: [d.online ? "online" : `offline · last seen ${ago(d.last_seen)}`, d.platform ? ` · ${d.platform}` : ""] })] }), jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
506
|
+
try {
|
|
507
|
+
await api.removeDevice(d.id);
|
|
508
|
+
onChange();
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
setError(errText(err));
|
|
512
|
+
}
|
|
513
|
+
} })] }, d.id))) })), error && jsx("p", { className: "sf-error", children: error }), jsx(AddComputer, { api: api, onPaired: onChange })] })] }));
|
|
514
|
+
}
|
|
515
|
+
|
|
441
516
|
function styleInject(css, ref) {
|
|
442
517
|
if ( ref === void 0 ) ref = {};
|
|
443
518
|
var insertAt = ref.insertAt;
|
|
@@ -465,7 +540,7 @@ function styleInject(css, ref) {
|
|
|
465
540
|
}
|
|
466
541
|
}
|
|
467
542
|
|
|
468
|
-
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}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}";
|
|
543
|
+
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}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}.sf-dot{background:var(--sf-danger);border-radius:50%;display:inline-block;height:9px;vertical-align:middle;width:9px}.sf-dot--on{background:#16a34a}.sf-steps{display:flex;flex-direction:column;gap:10px;margin:0;padding-left:20px}.sf-steps li>.sf-row{margin:4px 0}.sf-codeline{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;font-size:12px;padding:4px 8px}.sf-codeline,.sf-paircode{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sf-paircode{font-size:22px;font-weight:700;letter-spacing:.12em}.sf-small{font-size:12px}";
|
|
469
544
|
styleInject(css_248z);
|
|
470
545
|
|
|
471
546
|
function buildTree(files) {
|
|
@@ -491,6 +566,7 @@ function SecureFiles({ apiBase }) {
|
|
|
491
566
|
const [files, setFiles] = useState([]);
|
|
492
567
|
const [user, setUser] = useState(null);
|
|
493
568
|
const [isAdmin, setIsAdmin] = useState(false);
|
|
569
|
+
const [devices, setDevices] = useState([]);
|
|
494
570
|
const [loading, setLoading] = useState(true);
|
|
495
571
|
const [error, setError] = useState(null);
|
|
496
572
|
const [notice, setNotice] = useState(null);
|
|
@@ -505,6 +581,7 @@ function SecureFiles({ apiBase }) {
|
|
|
505
581
|
const data = await api.list();
|
|
506
582
|
setUser(data.user_name);
|
|
507
583
|
setIsAdmin(data.is_admin);
|
|
584
|
+
setDevices(data.devices || []);
|
|
508
585
|
setFiles(data.files);
|
|
509
586
|
setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
|
|
510
587
|
}
|
|
@@ -587,7 +664,7 @@ function SecureFiles({ apiBase }) {
|
|
|
587
664
|
const searching = search.trim().length > 0;
|
|
588
665
|
const renderFile = (f, depth) => {
|
|
589
666
|
const busy = busyIds.has(f.id);
|
|
590
|
-
return (jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsx("td", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (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" }))] }), jsxs("td", { children: [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 && jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsx("td", { className: "sf-col-actions", children: jsxs("div", { className: "sf-row sf-row--end", children: [jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && jsx(CopyButton, { text: f.share_url }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
667
|
+
return (jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsx("td", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsx("span", { className: f.available || f.device ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), f.device && (jsxs("span", { className: `sf-tag${f.device.online ? "" : " sf-tag--warn"}`, title: "Served from your computer through the relay", children: ["on ", f.device.name, " \u00B7 ", f.device.online ? "online" : "offline"] })), !f.available && !f.device && (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" }))] }), jsxs("td", { children: [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 && jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsx("td", { className: "sf-col-actions", children: jsxs("div", { className: "sf-row sf-row--end", children: [jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsx(CopyButton, { text: f.share_url }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), f.device ? (jsxs("span", { className: "sf-muted sf-small", title: "Stop sharing it from that computer's Secure Files panel", children: ["managed on ", f.device.name] })) : (jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) }))] }) })] }, f.id));
|
|
591
668
|
};
|
|
592
669
|
const renderFolder = (node, depth) => {
|
|
593
670
|
const rows = [];
|
|
@@ -604,7 +681,7 @@ function SecureFiles({ apiBase }) {
|
|
|
604
681
|
rows.push(...ownFiles.map((f) => renderFile(f, depth)));
|
|
605
682
|
return rows;
|
|
606
683
|
};
|
|
607
|
-
return (jsxs("div", { className: "sf-root", children: [jsxs("header", { className: "sf-head", children: [jsx("h2", { children: "Secure Files" }), user && jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxs("section", { className: "sf-toolbar", children: [jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-row", children: [jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsx("th", { children: "Name" }), jsx("th", { children: "Sharing" }), jsx("th", { children: "Downloads" }), jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
684
|
+
return (jsxs("div", { className: "sf-root", children: [jsxs("header", { className: "sf-head", children: [jsx("h2", { children: "Secure Files" }), user && jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsx(MyComputers, { api: api, devices: devices, onChange: load }), isAdmin && jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxs("section", { className: "sf-toolbar", children: [jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-row", children: [jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsx("th", { children: "Name" }), jsx("th", { children: "Sharing" }), jsx("th", { children: "Downloads" }), jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
608
685
|
}
|
|
609
686
|
|
|
610
687
|
export { ApiError, DEFAULT_API_BASE, SecureFiles, createApi };
|