@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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SecureFilesApi } from "./api";
|
|
2
|
+
import type { Device } from "./types";
|
|
3
|
+
export declare function MyComputers({ api, devices, onChange, }: {
|
|
4
|
+
api: SecureFilesApi;
|
|
5
|
+
devices: Device[];
|
|
6
|
+
onChange: () => void;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowseResult, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
|
|
1
|
+
import type { BrowseResult, Device, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
|
|
2
2
|
export declare const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
|
|
3
3
|
export declare class ApiError extends Error {
|
|
4
4
|
status: number;
|
|
@@ -9,6 +9,17 @@ export declare function createApi(apiBase?: string): {
|
|
|
9
9
|
user_name: string;
|
|
10
10
|
is_admin: boolean;
|
|
11
11
|
files: SecureFile[];
|
|
12
|
+
devices: Device[];
|
|
13
|
+
}>;
|
|
14
|
+
devices: () => Promise<{
|
|
15
|
+
devices: Device[];
|
|
16
|
+
}>;
|
|
17
|
+
pairDevice: () => Promise<{
|
|
18
|
+
code: string;
|
|
19
|
+
expires_in: number;
|
|
20
|
+
}>;
|
|
21
|
+
removeDevice: (id: number) => Promise<{
|
|
22
|
+
devices: Device[];
|
|
12
23
|
}>;
|
|
13
24
|
roots: () => Promise<{
|
|
14
25
|
roots: LinkRoot[];
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,9 @@ function createApi(apiBase = DEFAULT_API_BASE) {
|
|
|
37
37
|
}
|
|
38
38
|
return {
|
|
39
39
|
list: () => json("/userInfo"),
|
|
40
|
+
devices: () => json("/devices"),
|
|
41
|
+
pairDevice: () => json("/devices/pair", { method: "POST", body: "{}" }),
|
|
42
|
+
removeDevice: (id) => json(`/devices/${id}`, { method: "DELETE" }),
|
|
40
43
|
roots: () => json("/admin/roots"),
|
|
41
44
|
requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
|
|
42
45
|
confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
|
|
@@ -245,6 +248,25 @@ function CopyButton({ text, label = "Copy link" }) {
|
|
|
245
248
|
return (jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
252
|
+
const [armed, setArmed] = react.useState(false);
|
|
253
|
+
react.useEffect(() => {
|
|
254
|
+
if (!armed)
|
|
255
|
+
return;
|
|
256
|
+
const t = setTimeout(() => setArmed(false), 3000);
|
|
257
|
+
return () => clearTimeout(t);
|
|
258
|
+
}, [armed]);
|
|
259
|
+
return (jsxRuntime.jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
260
|
+
if (armed) {
|
|
261
|
+
setArmed(false);
|
|
262
|
+
onConfirm();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
setArmed(true);
|
|
266
|
+
}
|
|
267
|
+
}, children: armed ? "Confirm" : label }));
|
|
268
|
+
}
|
|
269
|
+
|
|
248
270
|
function ShareDialog({ file, onClose, onSave, }) {
|
|
249
271
|
const [shareable, setShareable] = react.useState(file.shareable);
|
|
250
272
|
const [limit, setLimit] = react.useState(file.download_limit ? String(file.download_limit) : "");
|
|
@@ -260,6 +282,19 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
260
282
|
}, [onClose]);
|
|
261
283
|
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
262
284
|
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
285
|
+
const newLink = async () => {
|
|
286
|
+
setSaving(true);
|
|
287
|
+
setError(null);
|
|
288
|
+
try {
|
|
289
|
+
await onSave({ id: file.id, shareable: true, regenerate_link: true });
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
setSaving(false);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
263
298
|
const save = async () => {
|
|
264
299
|
setSaving(true);
|
|
265
300
|
setError(null);
|
|
@@ -284,26 +319,7 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
284
319
|
setSaving(false);
|
|
285
320
|
}
|
|
286
321
|
};
|
|
287
|
-
return (jsxRuntime.jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxRuntime.jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxRuntime.jsxs("header", { className: "sf-dialog__head", children: [jsxRuntime.jsx("h3", { children: "Share settings" }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsxRuntime.jsx("p", { className: "sf-muted", children: file.filepath }), jsxRuntime.jsxs("label", { className: "sf-check", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxRuntime.jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Download limit (blank = unlimited)" }), jsxRuntime.jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsxRuntime.jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxRuntime.jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Password" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [file.has_password && (jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsxRuntime.jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Share link" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsxRuntime.jsx(CopyButton, { text: file.share_url })] }), !file.shareable && jsxRuntime.jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })] })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
291
|
-
const [armed, setArmed] = react.useState(false);
|
|
292
|
-
react.useEffect(() => {
|
|
293
|
-
if (!armed)
|
|
294
|
-
return;
|
|
295
|
-
const t = setTimeout(() => setArmed(false), 3000);
|
|
296
|
-
return () => clearTimeout(t);
|
|
297
|
-
}, [armed]);
|
|
298
|
-
return (jsxRuntime.jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
299
|
-
if (armed) {
|
|
300
|
-
setArmed(false);
|
|
301
|
-
onConfirm();
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
setArmed(true);
|
|
305
|
-
}
|
|
306
|
-
}, children: armed ? "Confirm" : label }));
|
|
322
|
+
return (jsxRuntime.jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxRuntime.jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxRuntime.jsxs("header", { className: "sf-dialog__head", children: [jsxRuntime.jsx("h3", { children: "Share settings" }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsxRuntime.jsx("p", { className: "sf-muted", children: file.filepath }), jsxRuntime.jsxs("label", { className: "sf-check", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxRuntime.jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Download limit (blank = unlimited)" }), jsxRuntime.jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsxRuntime.jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxRuntime.jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Password" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [file.has_password && (jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsxRuntime.jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Share link" }), file.share_url ? (jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsxRuntime.jsx(CopyButton, { text: file.share_url }), jsxRuntime.jsx(ConfirmButton, { label: "New link", disabled: saving, onConfirm: newLink })] })) : (jsxRuntime.jsx("span", { className: "sf-muted", children: "A private link is created when you save with sharing on." })), file.share_url && !file.shareable && (jsxRuntime.jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })), jsxRuntime.jsx("span", { className: "sf-muted", children: "\"New link\" invalidates the current one immediately." })] })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
function formatSize(bytes) {
|
|
@@ -316,7 +332,7 @@ function formatSize(bytes) {
|
|
|
316
332
|
}
|
|
317
333
|
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
318
334
|
}
|
|
319
|
-
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
335
|
+
const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
|
|
320
336
|
function AddRoot({ api, onAdded }) {
|
|
321
337
|
const [path, setPath] = react.useState("");
|
|
322
338
|
const [label, setLabel] = react.useState("");
|
|
@@ -332,7 +348,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
332
348
|
setSignature("");
|
|
333
349
|
}
|
|
334
350
|
catch (err) {
|
|
335
|
-
setError(errText(err));
|
|
351
|
+
setError(errText$1(err));
|
|
336
352
|
}
|
|
337
353
|
finally {
|
|
338
354
|
setBusy(false);
|
|
@@ -351,7 +367,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
351
367
|
setSignature("");
|
|
352
368
|
}
|
|
353
369
|
catch (err) {
|
|
354
|
-
setError(errText(err));
|
|
370
|
+
setError(errText$1(err));
|
|
355
371
|
}
|
|
356
372
|
finally {
|
|
357
373
|
setBusy(false);
|
|
@@ -374,7 +390,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
374
390
|
setPicked(new Set());
|
|
375
391
|
}
|
|
376
392
|
catch (err) {
|
|
377
|
-
setError(errText(err));
|
|
393
|
+
setError(errText$1(err));
|
|
378
394
|
}
|
|
379
395
|
}, [api]);
|
|
380
396
|
react.useEffect(() => {
|
|
@@ -402,7 +418,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
402
418
|
await open(listing.path);
|
|
403
419
|
}
|
|
404
420
|
catch (err) {
|
|
405
|
-
setResult(errText(err));
|
|
421
|
+
setResult(errText$1(err));
|
|
406
422
|
}
|
|
407
423
|
finally {
|
|
408
424
|
setBusy(false);
|
|
@@ -426,7 +442,7 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
426
442
|
setError(null);
|
|
427
443
|
}
|
|
428
444
|
catch (err) {
|
|
429
|
-
setError(errText(err));
|
|
445
|
+
setError(errText$1(err));
|
|
430
446
|
}
|
|
431
447
|
}, [api]);
|
|
432
448
|
return (jsxRuntime.jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsxRuntime.jsx("summary", { children: "Server files (admin)" }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Link files 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 && jsxRuntime.jsx("p", { className: "sf-error", children: error }), roots && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "None yet." })) : (jsxRuntime.jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("code", { children: r.path }), " ", r.label && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsxRuntime.jsx("span", { className: "sf-error", children: " (missing)" })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
@@ -435,11 +451,70 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
435
451
|
onLinked();
|
|
436
452
|
}
|
|
437
453
|
catch (err) {
|
|
438
|
-
setError(errText(err));
|
|
454
|
+
setError(errText$1(err));
|
|
439
455
|
}
|
|
440
456
|
} })] }, r.id))) }))] }), jsxRuntime.jsx(AddRoot, { api: api, onAdded: setRoots }), jsxRuntime.jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
|
|
441
457
|
}
|
|
442
458
|
|
|
459
|
+
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
460
|
+
function ago(iso) {
|
|
461
|
+
if (!iso)
|
|
462
|
+
return "never";
|
|
463
|
+
const secs = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
|
|
464
|
+
if (secs < 90)
|
|
465
|
+
return "just now";
|
|
466
|
+
if (secs < 3600)
|
|
467
|
+
return `${Math.round(secs / 60)} min ago`;
|
|
468
|
+
if (secs < 86400)
|
|
469
|
+
return `${Math.round(secs / 3600)} h ago`;
|
|
470
|
+
return `${Math.round(secs / 86400)} d ago`;
|
|
471
|
+
}
|
|
472
|
+
function AddComputer({ api, onPaired }) {
|
|
473
|
+
const [code, setCode] = react.useState(null);
|
|
474
|
+
const [expiresAt, setExpiresAt] = react.useState(0);
|
|
475
|
+
const [, setTick] = react.useState(0);
|
|
476
|
+
const [error, setError] = react.useState(null);
|
|
477
|
+
react.useEffect(() => {
|
|
478
|
+
if (!code)
|
|
479
|
+
return;
|
|
480
|
+
const timer = setInterval(() => {
|
|
481
|
+
if (Date.now() > expiresAt)
|
|
482
|
+
setCode(null);
|
|
483
|
+
setTick((t) => t + 1);
|
|
484
|
+
onPaired();
|
|
485
|
+
}, 5000);
|
|
486
|
+
return () => clearInterval(timer);
|
|
487
|
+
}, [code, expiresAt, onPaired]);
|
|
488
|
+
const create = async () => {
|
|
489
|
+
setError(null);
|
|
490
|
+
try {
|
|
491
|
+
const res = await api.pairDevice();
|
|
492
|
+
setCode(res.code);
|
|
493
|
+
setExpiresAt(Date.now() + res.expires_in * 1000);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
setError(errText(err));
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const minutesLeft = Math.max(1, Math.round((expiresAt - Date.now()) / 60000));
|
|
500
|
+
if (!code) {
|
|
501
|
+
return (jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", onClick: create, children: "Add a computer" }), error && jsxRuntime.jsx("span", { className: "sf-error", children: error })] }));
|
|
502
|
+
}
|
|
503
|
+
return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Connect a computer" }), jsxRuntime.jsxs("ol", { className: "sf-steps", children: [jsxRuntime.jsxs("li", { children: ["On that computer, install the agent (Linux, macOS or Windows with Python 3.9+):", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("code", { className: "sf-codeline", children: "pipx install abstract-securefiles && securefiles setup" }), jsxRuntime.jsx(CopyButton, { text: "pipx install abstract-securefiles && securefiles setup", label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["No pipx? Use ", jsxRuntime.jsx("code", { children: "python3 -m pip install --user abstract-securefiles" }), " then ", jsxRuntime.jsx("code", { children: "securefiles setup" }), "."] })] }), jsxRuntime.jsxs("li", { children: ["In the panel that opens (", jsxRuntime.jsx("code", { children: "http://127.0.0.1:47821" }), "), enter this code:", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("span", { className: "sf-paircode", children: code }), jsxRuntime.jsx(CopyButton, { text: code, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Valid for about ", minutesLeft, " more minutes, once."] })] }), jsxRuntime.jsxs("li", { children: ["Pick the files and folders to share there. They appear here under ", jsxRuntime.jsx("code", { children: "devices/<computer>/" }), "."] })] }), jsxRuntime.jsx("div", { className: "sf-row", children: jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCode(null), children: "Done" }) })] }));
|
|
504
|
+
}
|
|
505
|
+
function MyComputers({ api, devices, onChange, }) {
|
|
506
|
+
const [error, setError] = react.useState(null);
|
|
507
|
+
return (jsxRuntime.jsxs("details", { className: "sf-admin", children: [jsxRuntime.jsxs("summary", { children: ["My computers", " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: ["(", devices.filter((d) => d.online).length, " online / ", devices.length, ")"] })] }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.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 && (jsxRuntime.jsx("ul", { className: "sf-roots", children: devices.map((d) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("span", { className: `sf-dot${d.online ? " sf-dot--on" : ""}` }), " ", jsxRuntime.jsx("b", { children: d.name }), " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: [d.online ? "online" : `offline · last seen ${ago(d.last_seen)}`, d.platform ? ` · ${d.platform}` : ""] })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
508
|
+
try {
|
|
509
|
+
await api.removeDevice(d.id);
|
|
510
|
+
onChange();
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
setError(errText(err));
|
|
514
|
+
}
|
|
515
|
+
} })] }, d.id))) })), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsx(AddComputer, { api: api, onPaired: onChange })] })] }));
|
|
516
|
+
}
|
|
517
|
+
|
|
443
518
|
function styleInject(css, ref) {
|
|
444
519
|
if ( ref === void 0 ) ref = {};
|
|
445
520
|
var insertAt = ref.insertAt;
|
|
@@ -467,7 +542,7 @@ function styleInject(css, ref) {
|
|
|
467
542
|
}
|
|
468
543
|
}
|
|
469
544
|
|
|
470
|
-
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}";
|
|
545
|
+
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}";
|
|
471
546
|
styleInject(css_248z);
|
|
472
547
|
|
|
473
548
|
function buildTree(files) {
|
|
@@ -493,6 +568,7 @@ function SecureFiles({ apiBase }) {
|
|
|
493
568
|
const [files, setFiles] = react.useState([]);
|
|
494
569
|
const [user, setUser] = react.useState(null);
|
|
495
570
|
const [isAdmin, setIsAdmin] = react.useState(false);
|
|
571
|
+
const [devices, setDevices] = react.useState([]);
|
|
496
572
|
const [loading, setLoading] = react.useState(true);
|
|
497
573
|
const [error, setError] = react.useState(null);
|
|
498
574
|
const [notice, setNotice] = react.useState(null);
|
|
@@ -507,6 +583,7 @@ function SecureFiles({ apiBase }) {
|
|
|
507
583
|
const data = await api.list();
|
|
508
584
|
setUser(data.user_name);
|
|
509
585
|
setIsAdmin(data.is_admin);
|
|
586
|
+
setDevices(data.devices || []);
|
|
510
587
|
setFiles(data.files);
|
|
511
588
|
setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
|
|
512
589
|
}
|
|
@@ -589,7 +666,7 @@ function SecureFiles({ apiBase }) {
|
|
|
589
666
|
const searching = search.trim().length > 0;
|
|
590
667
|
const renderFile = (f, depth) => {
|
|
591
668
|
const busy = busyIds.has(f.id);
|
|
592
|
-
return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
669
|
+
return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available || f.device ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), f.device && (jsxRuntime.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 && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), f.device ? (jsxRuntime.jsxs("span", { className: "sf-muted sf-small", title: "Stop sharing it from that computer's Secure Files panel", children: ["managed on ", f.device.name] })) : (jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) }))] }) })] }, f.id));
|
|
593
670
|
};
|
|
594
671
|
const renderFolder = (node, depth) => {
|
|
595
672
|
const rows = [];
|
|
@@ -606,7 +683,7 @@ function SecureFiles({ apiBase }) {
|
|
|
606
683
|
rows.push(...ownFiles.map((f) => renderFile(f, depth)));
|
|
607
684
|
return rows;
|
|
608
685
|
};
|
|
609
|
-
return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
686
|
+
return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsxRuntime.jsx(MyComputers, { api: api, devices: devices, onChange: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
610
687
|
}
|
|
611
688
|
|
|
612
689
|
exports.ApiError = ApiError;
|