yt-playlist-features/src/content/ui/lifecycle.ts

94 lines
2.7 KiB
TypeScript

import type { PlaylistData, PlaylistVideo } from "../../types/playlist";
import { injectStyles } from "./styles";
import type { DetailUpdate } from "../../types/playlist";
import { renderPlaylistTable, loadColumnPrefs, type PlaylistTableHandle } from "./table-renderer";
const CONTAINER_ID = "ytpf-playlist-table";
let currentHandle: PlaylistTableHandle | null = null;
function findAnchor(): { parent: Element; before: Element | null } | null {
// Primary: top of ytd-item-section-renderer (before any header/content)
const itemSection = document.querySelector(
"ytd-section-list-renderer ytd-item-section-renderer",
);
if (itemSection) {
return { parent: itemSection, before: itemSection.firstElementChild };
}
// Fallback: end of ytd-section-list-renderer > #contents
const sectionContents = document.querySelector(
"ytd-section-list-renderer > #contents",
);
if (sectionContents) {
return { parent: sectionContents, before: null };
}
// Last resort: #primary
const primary = document.querySelector(
"ytd-two-column-browse-results-renderer > #primary",
);
if (primary) {
return { parent: primary, before: null };
}
return null;
}
function insertAt(el: HTMLElement, anchor: { parent: Element; before: Element | null }): void {
anchor.parent.insertBefore(el, anchor.before);
}
let pendingObserver: MutationObserver | null = null;
export async function mountTable(data: PlaylistData): Promise<void> {
unmountTable();
injectStyles();
const prefs = await loadColumnPrefs();
currentHandle = renderPlaylistTable(data, prefs);
const el = currentHandle.element;
el.id = CONTAINER_ID;
const anchor = findAnchor();
if (anchor) {
insertAt(el, anchor);
return;
}
// DOM not ready yet — wait for the anchor element to appear
pendingObserver = new MutationObserver(() => {
const a = findAnchor();
if (a) {
pendingObserver!.disconnect();
pendingObserver = null;
insertAt(el, a);
}
});
pendingObserver.observe(document.body, { childList: true, subtree: true });
}
export function appendToTable(newVideos: PlaylistVideo[]): void {
currentHandle?.appendVideos(newVideos);
}
export function setTableComplete(extractedCount: number): void {
currentHandle?.setComplete(extractedCount);
}
export function updateTableDetails(updates: DetailUpdate[]): void {
currentHandle?.updateDetails(updates);
}
export function unmountTable(): void {
if (pendingObserver) {
pendingObserver.disconnect();
pendingObserver = null;
}
currentHandle = null;
document.getElementById(CONTAINER_ID)?.remove();
}
// Auto-cleanup on SPA navigation
document.addEventListener("yt-navigate-start", unmountTable);