@putkoff/abstract-securefiles 0.0.49 → 0.0.51
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/api.d.ts +2 -1
- package/dist/index.cjs +51 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +51 -46
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/package.json +1 -1
- package/src/SecureFiles.tsx +7 -5
- package/src/ServerFiles.tsx +32 -17
- package/src/ShareDialog.tsx +26 -5
- package/src/api.ts +4 -15
- package/src/styles.css +10 -0
- package/src/types.ts +7 -2
package/dist/api.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export declare function createApi(apiBase?: string): {
|
|
|
40
40
|
error: string;
|
|
41
41
|
}[];
|
|
42
42
|
}>;
|
|
43
|
-
download(
|
|
43
|
+
/** Hand the download to the browser so it streams to disk (native progress + resume, no memory limit). */
|
|
44
|
+
download(ids: number[]): void;
|
|
44
45
|
upload(file: File, relPath: string, onProgress?: (fraction: number) => void): Promise<void>;
|
|
45
46
|
};
|
|
46
47
|
export type SecureFilesApi = ReturnType<typeof createApi>;
|
package/dist/index.cjs
CHANGED
|
@@ -54,26 +54,14 @@ function createApi(apiBase = DEFAULT_API_BASE) {
|
|
|
54
54
|
method: "POST",
|
|
55
55
|
body: JSON.stringify({ ids }),
|
|
56
56
|
}),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
method: "POST",
|
|
60
|
-
credentials: "include",
|
|
61
|
-
headers: { "Content-Type": "application/json" },
|
|
62
|
-
body: JSON.stringify({ ids }),
|
|
63
|
-
});
|
|
64
|
-
if (!res.ok)
|
|
65
|
-
throw await errorFrom(res);
|
|
66
|
-
const disposition = res.headers.get("Content-Disposition") || "";
|
|
67
|
-
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
|
|
68
|
-
const name = match ? decodeURIComponent(match[1]) : fallbackName;
|
|
69
|
-
const url = URL.createObjectURL(await res.blob());
|
|
57
|
+
/** Hand the download to the browser so it streams to disk (native progress + resume, no memory limit). */
|
|
58
|
+
download(ids) {
|
|
70
59
|
const a = document.createElement("a");
|
|
71
|
-
a.href =
|
|
72
|
-
a.
|
|
60
|
+
a.href = `${root}/download?ids=${ids.join(",")}`;
|
|
61
|
+
a.rel = "noopener";
|
|
73
62
|
document.body.appendChild(a);
|
|
74
63
|
a.click();
|
|
75
64
|
a.remove();
|
|
76
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
77
65
|
},
|
|
78
66
|
upload(file, relPath, onProgress) {
|
|
79
67
|
return new Promise((resolve, reject) => {
|
|
@@ -257,6 +245,25 @@ function CopyButton({ text, label = "Copy link" }) {
|
|
|
257
245
|
return (jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
|
|
258
246
|
}
|
|
259
247
|
|
|
248
|
+
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
249
|
+
const [armed, setArmed] = react.useState(false);
|
|
250
|
+
react.useEffect(() => {
|
|
251
|
+
if (!armed)
|
|
252
|
+
return;
|
|
253
|
+
const t = setTimeout(() => setArmed(false), 3000);
|
|
254
|
+
return () => clearTimeout(t);
|
|
255
|
+
}, [armed]);
|
|
256
|
+
return (jsxRuntime.jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
257
|
+
if (armed) {
|
|
258
|
+
setArmed(false);
|
|
259
|
+
onConfirm();
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
setArmed(true);
|
|
263
|
+
}
|
|
264
|
+
}, children: armed ? "Confirm" : label }));
|
|
265
|
+
}
|
|
266
|
+
|
|
260
267
|
function ShareDialog({ file, onClose, onSave, }) {
|
|
261
268
|
const [shareable, setShareable] = react.useState(file.shareable);
|
|
262
269
|
const [limit, setLimit] = react.useState(file.download_limit ? String(file.download_limit) : "");
|
|
@@ -272,6 +279,19 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
272
279
|
}, [onClose]);
|
|
273
280
|
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
274
281
|
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
282
|
+
const newLink = async () => {
|
|
283
|
+
setSaving(true);
|
|
284
|
+
setError(null);
|
|
285
|
+
try {
|
|
286
|
+
await onSave({ id: file.id, shareable: true, regenerate_link: true });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
setSaving(false);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
275
295
|
const save = async () => {
|
|
276
296
|
setSaving(true);
|
|
277
297
|
setError(null);
|
|
@@ -296,26 +316,7 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
296
316
|
setSaving(false);
|
|
297
317
|
}
|
|
298
318
|
};
|
|
299
|
-
return (jsxRuntime.jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxRuntime.jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxRuntime.jsxs("header", { className: "sf-dialog__head", children: [jsxRuntime.jsx("h3", { children: "Share settings" }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsxRuntime.jsx("p", { className: "sf-muted", children: file.filepath }), jsxRuntime.jsxs("label", { className: "sf-check", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxRuntime.jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxRuntime.jsxs("label", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Download limit (blank = unlimited)" }), jsxRuntime.jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsxRuntime.jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxRuntime.jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Password" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [file.has_password && (jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxRuntime.jsxs("label", { className: "sf-check sf-check--inline", children: [jsxRuntime.jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsxRuntime.jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxRuntime.jsxs("div", { className: "sf-field", children: [jsxRuntime.jsx("span", { children: "Share link" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsxRuntime.jsx(CopyButton, { text: file.share_url })] }), !file.shareable && jsxRuntime.jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })] })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
303
|
-
const [armed, setArmed] = react.useState(false);
|
|
304
|
-
react.useEffect(() => {
|
|
305
|
-
if (!armed)
|
|
306
|
-
return;
|
|
307
|
-
const t = setTimeout(() => setArmed(false), 3000);
|
|
308
|
-
return () => clearTimeout(t);
|
|
309
|
-
}, [armed]);
|
|
310
|
-
return (jsxRuntime.jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
311
|
-
if (armed) {
|
|
312
|
-
setArmed(false);
|
|
313
|
-
onConfirm();
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
setArmed(true);
|
|
317
|
-
}
|
|
318
|
-
}, children: armed ? "Confirm" : label }));
|
|
319
|
+
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" })] })] }) }));
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
function formatSize(bytes) {
|
|
@@ -395,6 +396,11 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
395
396
|
if (listing && !roots.some((r) => listing.path.startsWith(r.path)))
|
|
396
397
|
setListing(null);
|
|
397
398
|
}, [roots, listing, open]);
|
|
399
|
+
const togglePick = (path, on) => setPicked((p) => {
|
|
400
|
+
const next = new Set(p);
|
|
401
|
+
on ? next.add(path) : next.delete(path);
|
|
402
|
+
return next;
|
|
403
|
+
});
|
|
398
404
|
const link = async () => {
|
|
399
405
|
setBusy(true);
|
|
400
406
|
setResult(null);
|
|
@@ -417,13 +423,12 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
417
423
|
};
|
|
418
424
|
if (!roots.length)
|
|
419
425
|
return jsxRuntime.jsx("p", { className: "sf-muted", children: "Allow a directory above to browse and link its files." });
|
|
420
|
-
const linkable =
|
|
426
|
+
const linkable = [
|
|
427
|
+
...(listing?.dirs.filter((d) => !d.linked_id) ?? []),
|
|
428
|
+
...(listing?.files.filter((f) => !f.linked_id) ?? []),
|
|
429
|
+
];
|
|
421
430
|
const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
|
|
422
|
-
return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Link files" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsxRuntime.jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsxRuntime.jsx("code", { className: "sf-muted", children: relative })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), listing && (jsxRuntime.jsxs("div", { className: "sf-browser", children: [listing.parent && (jsxRuntime.jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxRuntime.jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(
|
|
423
|
-
const next = new Set(p);
|
|
424
|
-
e.target.checked ? next.add(f.path) : next.delete(f.path);
|
|
425
|
-
return next;
|
|
426
|
-
}) }), jsxRuntime.jsx("span", { className: "sf-browser__name", children: f.name }), jsxRuntime.jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsxRuntime.jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsxRuntime.jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxRuntime.jsxs("div", { className: "sf-row sf-row--bottom", children: [jsxRuntime.jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsxRuntime.jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
|
|
431
|
+
return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Link files or folders" }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsxRuntime.jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsxRuntime.jsx("code", { className: "sf-muted", children: relative })] }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), listing && (jsxRuntime.jsxs("div", { className: "sf-browser", children: [listing.parent && (jsxRuntime.jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxRuntime.jsxs("div", { className: `sf-browser__row${d.linked_id ? " sf-muted" : ""}`, children: [jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select folder ${d.name}`, disabled: !!d.linked_id, checked: picked.has(d.path), onChange: (e) => togglePick(d.path, e.target.checked) }), jsxRuntime.jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(d.path), title: "Open folder", children: ["\uD83D\uDCC1 ", d.name] }), jsxRuntime.jsx("span", { className: "sf-muted", children: d.linked_id ? "already linked" : "folder → .tar" })] }, d.path))), linkable.length > 0 && (jsxRuntime.jsxs("label", { className: "sf-check sf-browser__all", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: linkable.length > 0 && linkable.every((f) => picked.has(f.path)), disabled: !linkable.length, onChange: (e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set()) }), "Select everything here"] })), listing.files.map((f) => (jsxRuntime.jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsxRuntime.jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => togglePick(f.path, e.target.checked) }), jsxRuntime.jsx("span", { className: "sf-browser__name", children: f.name }), jsxRuntime.jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsxRuntime.jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsxRuntime.jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxRuntime.jsxs("div", { className: "sf-row sf-row--bottom", children: [jsxRuntime.jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsxRuntime.jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
|
|
427
432
|
}
|
|
428
433
|
function ServerFiles({ api, folders, onLinked, }) {
|
|
429
434
|
const [roots, setRoots] = react.useState(null);
|
|
@@ -437,7 +442,7 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
437
442
|
setError(errText(err));
|
|
438
443
|
}
|
|
439
444
|
}, [api]);
|
|
440
|
-
return (jsxRuntime.jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsxRuntime.jsx("summary", { children: "Server files (admin)" }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Link files that already live on the server instead of uploading copies. Only
|
|
445
|
+
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 () => {
|
|
441
446
|
try {
|
|
442
447
|
setRoots((await api.removeRoot(r.id)).roots);
|
|
443
448
|
onLinked();
|
|
@@ -475,7 +480,7 @@ function styleInject(css, ref) {
|
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
482
|
|
|
478
|
-
var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}";
|
|
483
|
+
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}";
|
|
479
484
|
styleInject(css_248z);
|
|
480
485
|
|
|
481
486
|
function buildTree(files) {
|
|
@@ -557,7 +562,7 @@ function SecureFiles({ apiBase }) {
|
|
|
557
562
|
const ready = items.filter((f) => f.available);
|
|
558
563
|
if (!ready.length)
|
|
559
564
|
return;
|
|
560
|
-
|
|
565
|
+
api.download(ready.map((f) => f.id));
|
|
561
566
|
};
|
|
562
567
|
const setShared = (items, shareable) => run(items.map((f) => f.id), async () => {
|
|
563
568
|
for (const f of items)
|
|
@@ -597,7 +602,7 @@ function SecureFiles({ apiBase }) {
|
|
|
597
602
|
const searching = search.trim().length > 0;
|
|
598
603
|
const renderFile = (f, depth) => {
|
|
599
604
|
const busy = busyIds.has(f.id);
|
|
600
|
-
return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
605
|
+
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 && 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" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
601
606
|
};
|
|
602
607
|
const renderFolder = (node, depth) => {
|
|
603
608
|
const rows = [];
|