mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
playlists(#37): close the picker instantly + auto-refresh the channel section
Fixes two rough edges from first use: - The "Add to playlist" dialog blocked on the relay publish before closing, so it felt stuck / "didn't close, just reset". Now it closes IMMEDIATELY on Save and publishes in the background; the result comes back as a toast (optimistic, like the feed insert). No more waiting on the relay round-trip with the modal open. - After adding/creating a playlist you had to hard-refresh to see it. The picker now dispatches `bitvid:playlists-changed`, and channelPlaylists.js listens for it (matching the current channel) and re-fetches — immediately and again after ~2s to cover relay propagation lag — so the section updates on its own. Syntax + lint + build clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
f4fe4982bd
commit
a2e26b00b9
2 files changed
+124
-76
No files matched your search
+79
-44
@@ -4,14 +4,21 @@
|
||||
// renders a row of cards linking to the playlist view. Kept out of
|
||||
// channelProfile.js (that file is at its size cap) and wired in via a one-line
|
||||
// hook, mirroring channelZapTotal.js. Gated by FEATURE_PLAYLISTS — with the flag
|
||||
// off (default) it renders nothing and does no fetch, so it's invisible until
|
||||
// the whole playlist UI ships.
|
||||
// off it renders nothing and does no fetch.
|
||||
//
|
||||
// Also listens for `bitvid:playlists-changed` (dispatched by the "Add to
|
||||
// playlist" picker) so the section refreshes right after you add/create a
|
||||
// playlist, instead of needing a manual page reload.
|
||||
|
||||
import { fetchCreatorPlaylists } from "./playlists/playlistFacade.js";
|
||||
import { FEATURE_PLAYLISTS } from "./constants.js";
|
||||
import { devLogger } from "./utils/logger.js";
|
||||
|
||||
let currentToken = 0;
|
||||
let currentHex = "";
|
||||
let currentClient;
|
||||
let currentDoc = null;
|
||||
let listenerAttached = false;
|
||||
|
||||
function clearChildren(el) {
|
||||
while (el.firstChild) {
|
||||
@@ -63,8 +70,71 @@ function renderPlaylistCard(doc, playlist) {
|
||||
return card;
|
||||
}
|
||||
|
||||
// Fetch + render the currently-wired channel's playlists. Safe to call any time
|
||||
// (event refresh, initial wire); a nav token drops a stale fetch.
|
||||
async function refresh() {
|
||||
const doc = currentDoc || globalThis.document;
|
||||
const section =
|
||||
doc && typeof doc.getElementById === "function"
|
||||
? doc.getElementById("channelPlaylistsSection")
|
||||
: null;
|
||||
const grid = doc?.getElementById?.("channelPlaylistsGrid") || null;
|
||||
if (!section || !grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FEATURE_PLAYLISTS || !currentHex) {
|
||||
section.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = currentToken;
|
||||
let playlists = [];
|
||||
try {
|
||||
playlists = await fetchCreatorPlaylists(
|
||||
currentHex,
|
||||
currentClient ? { client: currentClient } : {},
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[channelPlaylists] Failed to load playlists:", error);
|
||||
playlists = [];
|
||||
}
|
||||
|
||||
// A newer navigation superseded this fetch.
|
||||
if (token !== currentToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playlists.length) {
|
||||
section.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
clearChildren(grid);
|
||||
const frag = doc.createDocumentFragment();
|
||||
for (const playlist of playlists) {
|
||||
frag.appendChild(renderPlaylistCard(doc, playlist));
|
||||
}
|
||||
grid.appendChild(frag);
|
||||
section.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function onPlaylistsChanged(event) {
|
||||
const changed = (event?.detail?.pubkey || "").trim().toLowerCase();
|
||||
if (!currentHex || changed !== currentHex) {
|
||||
return;
|
||||
}
|
||||
// Refresh now, then again shortly after — relays need a beat to serve the
|
||||
// event we just published back, so the first fetch can miss it.
|
||||
refresh();
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
export function teardownChannelPlaylists() {
|
||||
currentToken += 1;
|
||||
currentHex = "";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,51 +148,16 @@ export async function wireChannelPlaylists(
|
||||
{ document: doc = globalThis.document, client } = {},
|
||||
) {
|
||||
teardownChannelPlaylists();
|
||||
const token = currentToken;
|
||||
currentHex = typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
||||
currentClient = client;
|
||||
currentDoc = doc || globalThis.document;
|
||||
|
||||
const section =
|
||||
doc && typeof doc.getElementById === "function"
|
||||
? doc.getElementById("channelPlaylistsSection")
|
||||
: null;
|
||||
const grid = doc?.getElementById?.("channelPlaylistsGrid") || null;
|
||||
if (!section || !grid) {
|
||||
return;
|
||||
if (!listenerAttached && currentDoc?.addEventListener) {
|
||||
currentDoc.addEventListener("bitvid:playlists-changed", onPlaylistsChanged);
|
||||
listenerAttached = true;
|
||||
}
|
||||
|
||||
const hide = () => section.classList.add("hidden");
|
||||
const show = () => section.classList.remove("hidden");
|
||||
|
||||
const hex = typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
||||
if (!FEATURE_PLAYLISTS || !hex) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let playlists = [];
|
||||
try {
|
||||
playlists = await fetchCreatorPlaylists(hex, client ? { client } : {});
|
||||
} catch (error) {
|
||||
devLogger.warn("[channelPlaylists] Failed to load playlists:", error);
|
||||
playlists = [];
|
||||
}
|
||||
|
||||
// A newer navigation (channel switch / teardown) superseded this fetch.
|
||||
if (token !== currentToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playlists.length) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
clearChildren(grid);
|
||||
const frag = doc.createDocumentFragment();
|
||||
for (const playlist of playlists) {
|
||||
frag.appendChild(renderPlaylistCard(doc, playlist));
|
||||
}
|
||||
grid.appendChild(frag);
|
||||
show();
|
||||
await refresh();
|
||||
}
|
||||
|
||||
export default wireChannelPlaylists;
|
||||
+45
-32
@@ -169,13 +169,13 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
saveBtn.disabled = true;
|
||||
cancelBtn.disabled = true;
|
||||
saveBtn.textContent = "Saving…";
|
||||
|
||||
function onSave() {
|
||||
// Compute the diff synchronously, then close the dialog IMMEDIATELY and
|
||||
// publish in the background — relay publishes take a beat, and blocking the
|
||||
// dialog on them made it feel stuck / "not close". Success/failure comes
|
||||
// back as a toast; a `bitvid:playlists-changed` event lets the channel
|
||||
// section refresh without a manual page reload.
|
||||
const jobs = [];
|
||||
// Diff existing playlists: only publish those whose membership changed.
|
||||
for (const { playlist, input, wasIn } of checkboxes) {
|
||||
if (input.checked === wasIn) {
|
||||
continue;
|
||||
@@ -185,7 +185,6 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) {
|
||||
: removeVideoFromPlaylist(playlist.items, coordinate);
|
||||
jobs.push({ ...playlist, items });
|
||||
}
|
||||
// New playlist, if named.
|
||||
const newName = newInput.value.trim();
|
||||
if (newName) {
|
||||
jobs.push({
|
||||
@@ -196,41 +195,55 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
cleanup();
|
||||
if (!jobs.length) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
await publishPlaylist({ pubkey, ...job });
|
||||
ok += 1;
|
||||
} catch (error) {
|
||||
userLogger.warn("[playlistPicker] Failed to publish playlist:", error);
|
||||
failed += 1;
|
||||
(async () => {
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
await publishPlaylist({ pubkey, ...job });
|
||||
ok += 1;
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"[playlistPicker] Failed to publish playlist:",
|
||||
error,
|
||||
);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup();
|
||||
if (ok && !failed) {
|
||||
say(
|
||||
"success",
|
||||
ok === 1 ? "Playlist updated." : `Updated ${ok} playlists.`,
|
||||
);
|
||||
} else if (ok && failed) {
|
||||
say("error", `Updated ${ok}; ${failed} could not be saved.`);
|
||||
} else {
|
||||
say("error", "Couldn't save your playlist changes.");
|
||||
}
|
||||
if (ok && !failed) {
|
||||
say(
|
||||
"success",
|
||||
ok === 1 ? "Playlist updated." : `Updated ${ok} playlists.`,
|
||||
);
|
||||
} else if (ok && failed) {
|
||||
say("error", `Updated ${ok}; ${failed} could not be saved.`);
|
||||
} else {
|
||||
say("error", "Couldn't save your playlist changes.");
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
try {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("bitvid:playlists-changed", {
|
||||
detail: { pubkey },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
cancelBtn.addEventListener("click", cleanup);
|
||||
backdrop.addEventListener("click", cleanup);
|
||||
saveBtn.addEventListener("click", () => {
|
||||
onSave().catch(() => cleanup());
|
||||
});
|
||||
saveBtn.addEventListener("click", onSave);
|
||||
document.addEventListener("keydown", onKeydown, true);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
Reference in new issue
Block a user