/** * rTube demo — client-side video library controller. * * Handles video selection from the sidebar, search filtering, * playback (or warning for unplayable formats), and copy-link. * No WebSocket — all local state with seed data baked into the HTML. */ /* ─── Helpers ──────────────────────────────────────────────── */ function isPlayable(filename: string): boolean { const ext = filename.split(".").pop()?.toLowerCase() || ""; return ["mp4", "webm", "mov", "ogg", "m4v"].includes(ext); } function getExt(filename: string): string { return filename.split(".").pop()?.toLowerCase() || ""; } /* ─── DOM refs ─────────────────────────────────────────────── */ const sidebar = document.getElementById("rd-video-sidebar") as HTMLElement; const videoList = document.getElementById("rd-video-list") as HTMLElement; const searchInput = document.getElementById("rd-video-search") as HTMLInputElement; const emptyMsg = document.getElementById("rd-video-empty") as HTMLElement; const playerContainer = document.getElementById("rd-player-container") as HTMLElement; const placeholder = document.getElementById("rd-player-placeholder") as HTMLElement; const infoBar = document.getElementById("rd-video-info") as HTMLElement; const videoNameEl = document.getElementById("rd-video-name") as HTMLElement; const downloadBtn = document.getElementById("rd-download-btn") as HTMLAnchorElement; const copyLinkBtn = document.getElementById("rd-copy-link-btn") as HTMLButtonElement; /* ─── State ─────────────────────────────────────────────────── */ let currentVideo: string | null = null; /* ─── Video selection ──────────────────────────────────────── */ function selectVideo(filename: string): void { currentVideo = filename; // Update active state in sidebar const items = videoList.querySelectorAll(".rd-tube-item"); for (const item of items) { if (item.dataset.video === filename) { item.classList.add("rd-tube-item--active"); } else { item.classList.remove("rd-tube-item--active"); } } const ext = getExt(filename); const playable = isPlayable(filename); // Clear player container (keep only the placeholder, hidden) playerContainer.innerHTML = ""; if (playable) { // Create video element const video = document.createElement("video"); video.controls = true; video.autoplay = true; video.preload = "auto"; video.style.width = "100%"; video.style.height = "100%"; const source = document.createElement("source"); // Demo: point to a placeholder URL (no real video files) source.src = `#demo-video/${encodeURIComponent(filename)}`; source.type = ext === "webm" ? "video/webm" : "video/mp4"; video.appendChild(source); // Suppress error UI for missing demo files video.addEventListener("error", () => { playerContainer.innerHTML = `
\u{1F3AC}

${filename}

Demo mode — no actual video files loaded.
In a real space, this would stream from Cloudflare R2.

`; }); playerContainer.appendChild(video); } else { // Show unplayable warning playerContainer.innerHTML = `
\u26A0\uFE0F

${ext.toUpperCase()} files cannot play in browsers

Download to play locally, or re-record in MP4 format

`; } // Show info bar infoBar.style.display = ""; videoNameEl.textContent = filename; downloadBtn.href = "#"; } /* ─── Event delegation: sidebar clicks ─────────────────────── */ sidebar.addEventListener("click", (e) => { const item = (e.target as HTMLElement).closest("[data-video]"); if (!item) return; const filename = item.dataset.video; if (filename) selectVideo(filename); }); // Keyboard support (Enter/Space on focused items) sidebar.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const item = (e.target as HTMLElement).closest("[data-video]"); if (!item) return; e.preventDefault(); const filename = item.dataset.video; if (filename) selectVideo(filename); }); /* ─── Search filtering ─────────────────────────────────────── */ searchInput.addEventListener("input", () => { const query = searchInput.value.toLowerCase().trim(); const items = videoList.querySelectorAll(".rd-tube-item"); let visibleCount = 0; for (const item of items) { const name = (item.dataset.video || "").toLowerCase(); const matches = !query || name.includes(query); item.style.display = matches ? "" : "none"; if (matches) visibleCount++; } emptyMsg.style.display = visibleCount === 0 ? "" : "none"; }); /* ─── Copy link ────────────────────────────────────────────── */ copyLinkBtn.addEventListener("click", () => { if (!currentVideo) return; const url = `${window.location.origin}/api/v/${encodeURIComponent(currentVideo)}`; navigator.clipboard.writeText(url).then( () => { const original = copyLinkBtn.textContent; copyLinkBtn.textContent = "Copied!"; setTimeout(() => { copyLinkBtn.innerHTML = ` Copy Link`; }, 1500); }, (err) => { console.error("[Tube] Copy failed:", err); }, ); });