@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/index.js
CHANGED
|
@@ -52,26 +52,14 @@ function createApi(apiBase = DEFAULT_API_BASE) {
|
|
|
52
52
|
method: "POST",
|
|
53
53
|
body: JSON.stringify({ ids }),
|
|
54
54
|
}),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
method: "POST",
|
|
58
|
-
credentials: "include",
|
|
59
|
-
headers: { "Content-Type": "application/json" },
|
|
60
|
-
body: JSON.stringify({ ids }),
|
|
61
|
-
});
|
|
62
|
-
if (!res.ok)
|
|
63
|
-
throw await errorFrom(res);
|
|
64
|
-
const disposition = res.headers.get("Content-Disposition") || "";
|
|
65
|
-
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
|
|
66
|
-
const name = match ? decodeURIComponent(match[1]) : fallbackName;
|
|
67
|
-
const url = URL.createObjectURL(await res.blob());
|
|
55
|
+
/** Hand the download to the browser so it streams to disk (native progress + resume, no memory limit). */
|
|
56
|
+
download(ids) {
|
|
68
57
|
const a = document.createElement("a");
|
|
69
|
-
a.href =
|
|
70
|
-
a.
|
|
58
|
+
a.href = `${root}/download?ids=${ids.join(",")}`;
|
|
59
|
+
a.rel = "noopener";
|
|
71
60
|
document.body.appendChild(a);
|
|
72
61
|
a.click();
|
|
73
62
|
a.remove();
|
|
74
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
75
63
|
},
|
|
76
64
|
upload(file, relPath, onProgress) {
|
|
77
65
|
return new Promise((resolve, reject) => {
|
|
@@ -255,6 +243,25 @@ function CopyButton({ text, label = "Copy link" }) {
|
|
|
255
243
|
return (jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
|
|
256
244
|
}
|
|
257
245
|
|
|
246
|
+
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
247
|
+
const [armed, setArmed] = useState(false);
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!armed)
|
|
250
|
+
return;
|
|
251
|
+
const t = setTimeout(() => setArmed(false), 3000);
|
|
252
|
+
return () => clearTimeout(t);
|
|
253
|
+
}, [armed]);
|
|
254
|
+
return (jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
255
|
+
if (armed) {
|
|
256
|
+
setArmed(false);
|
|
257
|
+
onConfirm();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
setArmed(true);
|
|
261
|
+
}
|
|
262
|
+
}, children: armed ? "Confirm" : label }));
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
function ShareDialog({ file, onClose, onSave, }) {
|
|
259
266
|
const [shareable, setShareable] = useState(file.shareable);
|
|
260
267
|
const [limit, setLimit] = useState(file.download_limit ? String(file.download_limit) : "");
|
|
@@ -270,6 +277,19 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
270
277
|
}, [onClose]);
|
|
271
278
|
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
272
279
|
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
280
|
+
const newLink = async () => {
|
|
281
|
+
setSaving(true);
|
|
282
|
+
setError(null);
|
|
283
|
+
try {
|
|
284
|
+
await onSave({ id: file.id, shareable: true, regenerate_link: true });
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
setSaving(false);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
273
293
|
const save = async () => {
|
|
274
294
|
setSaving(true);
|
|
275
295
|
setError(null);
|
|
@@ -294,26 +314,7 @@ function ShareDialog({ file, onClose, onSave, }) {
|
|
|
294
314
|
setSaving(false);
|
|
295
315
|
}
|
|
296
316
|
};
|
|
297
|
-
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" })] })] }) }));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
301
|
-
const [armed, setArmed] = useState(false);
|
|
302
|
-
useEffect(() => {
|
|
303
|
-
if (!armed)
|
|
304
|
-
return;
|
|
305
|
-
const t = setTimeout(() => setArmed(false), 3000);
|
|
306
|
-
return () => clearTimeout(t);
|
|
307
|
-
}, [armed]);
|
|
308
|
-
return (jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
309
|
-
if (armed) {
|
|
310
|
-
setArmed(false);
|
|
311
|
-
onConfirm();
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
setArmed(true);
|
|
315
|
-
}
|
|
316
|
-
}, children: armed ? "Confirm" : label }));
|
|
317
|
+
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" })] })] }) }));
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
function formatSize(bytes) {
|
|
@@ -393,6 +394,11 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
393
394
|
if (listing && !roots.some((r) => listing.path.startsWith(r.path)))
|
|
394
395
|
setListing(null);
|
|
395
396
|
}, [roots, listing, open]);
|
|
397
|
+
const togglePick = (path, on) => setPicked((p) => {
|
|
398
|
+
const next = new Set(p);
|
|
399
|
+
on ? next.add(path) : next.delete(path);
|
|
400
|
+
return next;
|
|
401
|
+
});
|
|
396
402
|
const link = async () => {
|
|
397
403
|
setBusy(true);
|
|
398
404
|
setResult(null);
|
|
@@ -415,13 +421,12 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
415
421
|
};
|
|
416
422
|
if (!roots.length)
|
|
417
423
|
return jsx("p", { className: "sf-muted", children: "Allow a directory above to browse and link its files." });
|
|
418
|
-
const linkable =
|
|
424
|
+
const linkable = [
|
|
425
|
+
...(listing?.dirs.filter((d) => !d.linked_id) ?? []),
|
|
426
|
+
...(listing?.files.filter((f) => !f.linked_id) ?? []),
|
|
427
|
+
];
|
|
419
428
|
const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
|
|
420
|
-
return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Link files" }), jsxs("div", { className: "sf-row", children: [jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsx("code", { className: "sf-muted", children: relative })] }), error && jsx("p", { className: "sf-error", children: error }), listing && (jsxs("div", { className: "sf-browser", children: [listing.parent && (jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(
|
|
421
|
-
const next = new Set(p);
|
|
422
|
-
e.target.checked ? next.add(f.path) : next.delete(f.path);
|
|
423
|
-
return next;
|
|
424
|
-
}) }), jsx("span", { className: "sf-browser__name", children: f.name }), jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxs("div", { className: "sf-row sf-row--bottom", children: [jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
|
|
429
|
+
return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Link files or folders" }), jsxs("div", { className: "sf-row", children: [jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsx("code", { className: "sf-muted", children: relative })] }), error && jsx("p", { className: "sf-error", children: error }), listing && (jsxs("div", { className: "sf-browser", children: [listing.parent && (jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxs("div", { className: `sf-browser__row${d.linked_id ? " sf-muted" : ""}`, children: [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) }), jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(d.path), title: "Open folder", children: ["\uD83D\uDCC1 ", d.name] }), jsx("span", { className: "sf-muted", children: d.linked_id ? "already linked" : "folder → .tar" })] }, d.path))), linkable.length > 0 && (jsxs("label", { className: "sf-check sf-browser__all", children: [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) => (jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => togglePick(f.path, e.target.checked) }), jsx("span", { className: "sf-browser__name", children: f.name }), jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxs("div", { className: "sf-row sf-row--bottom", children: [jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
|
|
425
430
|
}
|
|
426
431
|
function ServerFiles({ api, folders, onLinked, }) {
|
|
427
432
|
const [roots, setRoots] = useState(null);
|
|
@@ -435,7 +440,7 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
435
440
|
setError(errText(err));
|
|
436
441
|
}
|
|
437
442
|
}, [api]);
|
|
438
|
-
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 that already live on the server instead of uploading copies. Only
|
|
443
|
+
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 () => {
|
|
439
444
|
try {
|
|
440
445
|
setRoots((await api.removeRoot(r.id)).roots);
|
|
441
446
|
onLinked();
|
|
@@ -473,7 +478,7 @@ function styleInject(css, ref) {
|
|
|
473
478
|
}
|
|
474
479
|
}
|
|
475
480
|
|
|
476
|
-
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}";
|
|
481
|
+
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}";
|
|
477
482
|
styleInject(css_248z);
|
|
478
483
|
|
|
479
484
|
function buildTree(files) {
|
|
@@ -555,7 +560,7 @@ function SecureFiles({ apiBase }) {
|
|
|
555
560
|
const ready = items.filter((f) => f.available);
|
|
556
561
|
if (!ready.length)
|
|
557
562
|
return;
|
|
558
|
-
|
|
563
|
+
api.download(ready.map((f) => f.id));
|
|
559
564
|
};
|
|
560
565
|
const setShared = (items, shareable) => run(items.map((f) => f.id), async () => {
|
|
561
566
|
for (const f of items)
|
|
@@ -595,7 +600,7 @@ function SecureFiles({ apiBase }) {
|
|
|
595
600
|
const searching = search.trim().length > 0;
|
|
596
601
|
const renderFile = (f, depth) => {
|
|
597
602
|
const busy = busyIds.has(f.id);
|
|
598
|
-
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.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));
|
|
603
|
+
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 && 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" }), jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
599
604
|
};
|
|
600
605
|
const renderFolder = (node, depth) => {
|
|
601
606
|
const rows = [];
|