feat: inline video player with Vimeo/MP4/YouTube support
All checks were successful
Stuffle/personal-learn/pipeline/head This commit looks good
All checks were successful
Stuffle/personal-learn/pipeline/head This commit looks good
▶ video button expands a player panel directly below the card: - Vimeo (player.vimeo.com): iframe embed - YouTube: iframe embed - Direct MP4 / local file: <video> element Click ▶ video to open, ✕ close (or click again) to dismiss. Playback stops on close (iframe src reset, video.pause()). No external navigation — user stays on the catalogue page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
83
server.py
83
server.py
@@ -146,12 +146,28 @@ main { padding: 1.2rem 1.4rem; }
|
||||
text-decoration: none; border: 1px solid;
|
||||
}
|
||||
.jump.video { color: #34d399; border-color: #34d399;
|
||||
background: rgba(52,211,153,.08); }
|
||||
background: rgba(52,211,153,.08); cursor: pointer; }
|
||||
.jump.pdf { color: #f87171; border-color: #f87171;
|
||||
background: rgba(248,113,113,.08); }
|
||||
.jump.view { color: var(--teal); border-color: var(--teal);
|
||||
background: rgba(20,184,166,.08); }
|
||||
|
||||
/* ── inline video player ── */
|
||||
.video-panel {
|
||||
margin-top: .7rem; border-radius: 8px; overflow: hidden;
|
||||
background: #000; display: none;
|
||||
}
|
||||
.video-panel.open { display: block; }
|
||||
.video-panel iframe,
|
||||
.video-panel video {
|
||||
width: 100%; aspect-ratio: 16/9; border: none; display: block;
|
||||
}
|
||||
.video-close {
|
||||
display: flex; justify-content: flex-end; padding: .3rem .5rem;
|
||||
background: #111; cursor: pointer; color: var(--dim); font-size: .8rem;
|
||||
}
|
||||
.video-close:hover { color: var(--ink); }
|
||||
|
||||
.empty { color: var(--dim); padding: 2rem 0; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -272,15 +288,40 @@ function render() {
|
||||
: '<div class="empty">No artifacts match the current filters.</div>';
|
||||
}
|
||||
|
||||
// Unique ID counter for video panels
|
||||
let _uid = 0;
|
||||
|
||||
function embedHTML(videoUrl) {
|
||||
if (!videoUrl) return '';
|
||||
// Vimeo player.vimeo.com or vimeo.com/video/* → iframe embed
|
||||
if (videoUrl.includes('vimeo.com')) {
|
||||
// Ensure we use the player embed URL
|
||||
const src = videoUrl.includes('player.vimeo.com')
|
||||
? videoUrl
|
||||
: videoUrl.replace('vimeo.com/', 'player.vimeo.com/video/');
|
||||
return `<iframe src="${src}" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`;
|
||||
}
|
||||
// YouTube
|
||||
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
||||
const id = videoUrl.match(/(?:v=|youtu\.be\/)([^&?]+)/)?.[1];
|
||||
if (id) return `<iframe src="https://www.youtube.com/embed/${id}" allow="autoplay; fullscreen" allowfullscreen></iframe>`;
|
||||
}
|
||||
// Direct MP4 / local file
|
||||
return `<video src="${videoUrl}" controls preload="none"></video>`;
|
||||
}
|
||||
|
||||
function cardHTML(a) {
|
||||
const icon = TYPE_ICONS[a.type] || '•';
|
||||
const badge = `<span class="badge ${a.type}">${a.type}</span>`;
|
||||
const date = a.date ? `<span>${a.date}</span>` : '';
|
||||
const course = a.course ? `<span style="color:var(--dim)">${a.course}</span>` : '';
|
||||
|
||||
const vid = a.video_url;
|
||||
const panelId = vid ? `vp-${_uid++}` : '';
|
||||
|
||||
const links = [];
|
||||
if (a.type === 'transcript' && a.video_url) {
|
||||
links.push(`<a class="jump video" href="${a.video_url}" target="_blank">▶ video</a>`);
|
||||
if (vid) {
|
||||
links.push(`<button class="jump video" onclick="toggleVideo('${panelId}',this)">▶ video</button>`);
|
||||
}
|
||||
if (a.source_url && a.type === 'pdf') {
|
||||
links.push(`<a class="jump pdf" href="${a.source_url}" target="_blank">📄 PDF</a>`);
|
||||
@@ -289,16 +330,52 @@ function cardHTML(a) {
|
||||
links.push(`<a class="jump view" href="${a.served_url}" target="_blank">view text</a>`);
|
||||
}
|
||||
|
||||
const videoPanel = vid ? `
|
||||
<div class="video-panel" id="${panelId}">
|
||||
<div class="video-close" onclick="closeVideo('${panelId}')">✕ close</div>
|
||||
${embedHTML(vid)}
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="card">
|
||||
<div class="type-icon">${icon}</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">${esc(a.title || a.id)}</div>
|
||||
<div class="card-meta">${badge}${date}${course}</div>
|
||||
${links.length ? '<div class="jump-links">' + links.join('') + '</div>' : ''}
|
||||
${videoPanel}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function toggleVideo(panelId, btn) {
|
||||
const panel = document.getElementById(panelId);
|
||||
if (!panel) return;
|
||||
const isOpen = panel.classList.contains('open');
|
||||
if (isOpen) {
|
||||
closeVideo(panelId);
|
||||
btn.textContent = '▶ video';
|
||||
} else {
|
||||
panel.classList.add('open');
|
||||
btn.textContent = '▼ video';
|
||||
// Scroll panel into view smoothly
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
function closeVideo(panelId) {
|
||||
const panel = document.getElementById(panelId);
|
||||
if (!panel) return;
|
||||
panel.classList.remove('open');
|
||||
// Stop playback by clearing src on iframe/video
|
||||
const iframe = panel.querySelector('iframe');
|
||||
const video = panel.querySelector('video');
|
||||
if (iframe) { const s = iframe.src; iframe.src = ''; iframe.src = s; }
|
||||
if (video) { video.pause(); video.currentTime = 0; }
|
||||
// Reset button label
|
||||
const btn = panel.closest('.card-body')?.querySelector('.jump.video');
|
||||
if (btn) btn.textContent = '▶ video';
|
||||
}
|
||||
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
|
||||
loadCatalog();
|
||||
|
||||
Reference in New Issue
Block a user