mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
A central management surface, a sibling tab to "My Videos" (gated by
FEATURE_PLAYLISTS — hidden when off).
- components/profile-modal.html: "Playlists" nav item + #profilePanePlaylists.
- js/ui/profileModal/ProfilePlaylistsController.js: lists the user's playlists
(incl. empty) with a collapsible row each — Rename, Delete, and an expand to a
video sublist with ↑/↓ reorder and Remove. Fetches the referenced creators'
videos so the sublist shows real titles/thumbnails. Edits are optimistic and
re-sync on failure; publishes via the facade; dispatches
bitvid:playlists-changed so the channel section + this pane stay in sync.
- profileModalController.js: instantiate + bind nav/pane + cache DOM + register
listeners + selectPane("playlists") refresh; hide the tab when the flag is off.
- promptDialog.js: showTextPrompt (plain text prompt) for rename.
- playlistFacade.js: deletePlaylist — NIP-09 kind-5 (a + e refs) plus an empty
republish so kind-5-ignoring readers also drop it.
- tailwind.source.css: .playlist-row* styles (token-only).
Lint + build clean; profile-modal-controller tests 26/26.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
262 lines
8.9 KiB
JavaScript
262 lines
8.9 KiB
JavaScript
// Promise-based password/PIN prompt rendered with the site's modal styling (sibling of
|
|
// confirmDialog). Used to collect the passphrase that unlocks a stored nsec key — e.g.
|
|
// when switching to a saved nsec account. Resolves the entered string, or null if the
|
|
// user cancels. Builds its own `bv-modal modal-always-on-top` overlay so it stacks above
|
|
// other modals (and is typable thanks to the stacked-modal focus fix).
|
|
|
|
// When `collectRemember` is set, an opt-in "keep unlocked" checkbox is shown and
|
|
// the prompt resolves `{ passphrase, remember }` instead of a bare string (cancel
|
|
// still resolves null). Callers that don't pass `collectRemember` keep the legacy
|
|
// string resolution.
|
|
export function showPasswordPrompt(message, options = {}) {
|
|
const {
|
|
title = "",
|
|
confirmLabel = "Unlock",
|
|
cancelLabel = "Cancel",
|
|
placeholder = "PIN / passphrase",
|
|
collectRemember = false,
|
|
rememberLabel = "Keep me unlocked on this device",
|
|
rememberHint =
|
|
"Stored unencrypted on this device until you clear site data. Anyone with access to this browser can use it. Leave off to re-enter after closing the tab.",
|
|
rememberDefault = false,
|
|
} = options || {};
|
|
|
|
if (typeof document === "undefined" || !document.body) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement("div");
|
|
overlay.className =
|
|
"bv-modal modal-always-on-top items-start justify-center md:items-center";
|
|
overlay.setAttribute("role", "dialog");
|
|
overlay.setAttribute("aria-modal", "true");
|
|
|
|
const backdrop = document.createElement("div");
|
|
backdrop.className = "bv-modal-backdrop";
|
|
overlay.appendChild(backdrop);
|
|
|
|
const sheet = document.createElement("div");
|
|
sheet.className = "modal-sheet w-full max-w-md flex flex-col";
|
|
sheet.tabIndex = -1;
|
|
overlay.appendChild(sheet);
|
|
|
|
if (title) {
|
|
const header = document.createElement("div");
|
|
header.className = "modal-header";
|
|
const heading = document.createElement("h2");
|
|
heading.className = "text-lg font-bold text-text";
|
|
heading.textContent = title;
|
|
header.appendChild(heading);
|
|
sheet.appendChild(header);
|
|
}
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "p-5 space-y-3";
|
|
if (message) {
|
|
const text = document.createElement("p");
|
|
text.className = "text-sm text-muted-strong whitespace-pre-line";
|
|
text.textContent = String(message);
|
|
body.appendChild(text);
|
|
}
|
|
const input = document.createElement("input");
|
|
input.type = "password";
|
|
input.className = "input w-full";
|
|
input.placeholder = placeholder;
|
|
input.autocomplete = "off";
|
|
input.setAttribute("aria-label", placeholder);
|
|
body.appendChild(input);
|
|
|
|
let rememberCheckbox = null;
|
|
if (collectRemember) {
|
|
const rememberWrap = document.createElement("label");
|
|
rememberWrap.className = "flex items-start gap-2 cursor-pointer";
|
|
rememberCheckbox = document.createElement("input");
|
|
rememberCheckbox.type = "checkbox";
|
|
rememberCheckbox.className = "mt-1";
|
|
rememberCheckbox.checked = Boolean(rememberDefault);
|
|
const rememberText = document.createElement("span");
|
|
rememberText.className = "text-sm text-muted-strong";
|
|
const rememberTitle = document.createElement("span");
|
|
rememberTitle.className = "block text-text";
|
|
rememberTitle.textContent = rememberLabel;
|
|
rememberText.appendChild(rememberTitle);
|
|
if (rememberHint) {
|
|
const hint = document.createElement("span");
|
|
hint.className = "block text-xs text-status-danger";
|
|
hint.textContent = rememberHint;
|
|
rememberText.appendChild(hint);
|
|
}
|
|
rememberWrap.appendChild(rememberCheckbox);
|
|
rememberWrap.appendChild(rememberText);
|
|
body.appendChild(rememberWrap);
|
|
}
|
|
|
|
sheet.appendChild(body);
|
|
|
|
const footer = document.createElement("div");
|
|
footer.className =
|
|
"flex items-center justify-end gap-3 border-t border-border/60 p-4";
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.type = "button";
|
|
cancelBtn.className = "btn-ghost focus-ring";
|
|
cancelBtn.textContent = cancelLabel;
|
|
const confirmBtn = document.createElement("button");
|
|
confirmBtn.type = "button";
|
|
confirmBtn.className = "btn focus-ring";
|
|
confirmBtn.textContent = confirmLabel;
|
|
footer.appendChild(cancelBtn);
|
|
footer.appendChild(confirmBtn);
|
|
sheet.appendChild(footer);
|
|
|
|
let settled = false;
|
|
const cleanup = (result) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
document.removeEventListener("keydown", onKeydown, true);
|
|
overlay.remove();
|
|
resolve(result);
|
|
};
|
|
const submit = () =>
|
|
cleanup(
|
|
collectRemember
|
|
? {
|
|
passphrase: input.value,
|
|
remember: Boolean(rememberCheckbox && rememberCheckbox.checked),
|
|
}
|
|
: input.value,
|
|
);
|
|
function onKeydown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
cleanup(null);
|
|
}
|
|
}
|
|
|
|
cancelBtn.addEventListener("click", () => cleanup(null));
|
|
backdrop.addEventListener("click", () => cleanup(null));
|
|
confirmBtn.addEventListener("click", submit);
|
|
input.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
submit();
|
|
}
|
|
});
|
|
document.addEventListener("keydown", onKeydown, true);
|
|
|
|
document.body.appendChild(overlay);
|
|
input.focus();
|
|
});
|
|
}
|
|
|
|
// A plain single-line text prompt (sibling of showPasswordPrompt) — used for
|
|
// things like renaming a playlist. Resolves the trimmed string, or null on
|
|
// cancel / empty. Prefill with `value`.
|
|
export function showTextPrompt(message, options = {}) {
|
|
const {
|
|
title = "",
|
|
confirmLabel = "Save",
|
|
cancelLabel = "Cancel",
|
|
placeholder = "",
|
|
value = "",
|
|
maxLength = 200,
|
|
} = options || {};
|
|
|
|
if (typeof document === "undefined" || !document.body) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement("div");
|
|
overlay.className =
|
|
"bv-modal modal-always-on-top items-start justify-center md:items-center";
|
|
overlay.setAttribute("role", "dialog");
|
|
overlay.setAttribute("aria-modal", "true");
|
|
|
|
const backdrop = document.createElement("div");
|
|
backdrop.className = "bv-modal-backdrop";
|
|
overlay.appendChild(backdrop);
|
|
|
|
const sheet = document.createElement("div");
|
|
sheet.className = "modal-sheet w-full max-w-md flex flex-col";
|
|
sheet.tabIndex = -1;
|
|
overlay.appendChild(sheet);
|
|
|
|
if (title) {
|
|
const header = document.createElement("div");
|
|
header.className = "modal-header";
|
|
const heading = document.createElement("h2");
|
|
heading.className = "text-lg font-bold text-text";
|
|
heading.textContent = title;
|
|
header.appendChild(heading);
|
|
sheet.appendChild(header);
|
|
}
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "p-5 flex flex-col gap-3";
|
|
if (message) {
|
|
const text = document.createElement("p");
|
|
text.className = "text-sm text-muted-strong";
|
|
text.textContent = String(message);
|
|
body.appendChild(text);
|
|
}
|
|
const input = document.createElement("input");
|
|
input.type = "text";
|
|
input.className = "form-input";
|
|
input.placeholder = placeholder;
|
|
input.value = value;
|
|
input.maxLength = maxLength;
|
|
body.appendChild(input);
|
|
sheet.appendChild(body);
|
|
|
|
const footer = document.createElement("div");
|
|
footer.className =
|
|
"flex items-center justify-end gap-3 border-t border-border/60 p-4";
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.type = "button";
|
|
cancelBtn.className = "btn-ghost focus-ring";
|
|
cancelBtn.textContent = cancelLabel;
|
|
const okBtn = document.createElement("button");
|
|
okBtn.type = "button";
|
|
okBtn.className = "btn focus-ring";
|
|
okBtn.textContent = confirmLabel;
|
|
footer.appendChild(cancelBtn);
|
|
footer.appendChild(okBtn);
|
|
sheet.appendChild(footer);
|
|
|
|
let settled = false;
|
|
const cleanup = (result) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
document.removeEventListener("keydown", onKeydown, true);
|
|
overlay.remove();
|
|
resolve(result);
|
|
};
|
|
const submit = () => cleanup(input.value.trim() || null);
|
|
function onKeydown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
cleanup(null);
|
|
} else if (event.key === "Enter" && document.activeElement === input) {
|
|
event.preventDefault();
|
|
submit();
|
|
}
|
|
}
|
|
|
|
cancelBtn.addEventListener("click", () => cleanup(null));
|
|
backdrop.addEventListener("click", () => cleanup(null));
|
|
okBtn.addEventListener("click", submit);
|
|
document.addEventListener("keydown", onKeydown, true);
|
|
|
|
document.body.appendChild(overlay);
|
|
input.focus();
|
|
input.select();
|
|
});
|
|
}
|
|
|
|
export default showPasswordPrompt;
|