157 lines
6.1 KiB
TypeScript
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);
|
|
},
|
|
);
|
|
});
|