rspace-online/modules/rtube/components/tube-demo.ts

157 lines
6.1 KiB
TypeScript

/**
* 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<HTMLElement>(".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 = `
<div class="rd-tube-warning">
<div class="rd-tube-warning__icon">\u{1F3AC}</div>
<p><span class="rd-tube-warning__ext">${filename}</span></p>
<p class="rd-tube-warning__hint">Demo mode — no actual video files loaded.<br>In a real space, this would stream from Cloudflare R2.</p>
</div>`;
});
playerContainer.appendChild(video);
} else {
// Show unplayable warning
playerContainer.innerHTML = `
<div class="rd-tube-warning">
<div class="rd-tube-warning__icon">\u26A0\uFE0F</div>
<p><span class="rd-tube-warning__ext">${ext.toUpperCase()}</span> files cannot play in browsers</p>
<p class="rd-tube-warning__hint">Download to play locally, or re-record in MP4 format</p>
</div>`;
}
// 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<HTMLElement>("[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<HTMLElement>("[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<HTMLElement>(".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 = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
Copy Link`;
}, 1500);
},
(err) => {
console.error("[Tube] Copy failed:", err);
},
);
});