diff --git a/server.py b/server.py index 6c4dd57..5297452 100644 --- a/server.py +++ b/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; } @@ -272,15 +288,40 @@ function render() { : '
No artifacts match the current filters.
'; } +// 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 ``; + } + // YouTube + if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { + const id = videoUrl.match(/(?:v=|youtu\.be\/)([^&?]+)/)?.[1]; + if (id) return ``; + } + // Direct MP4 / local file + return ``; +} + function cardHTML(a) { const icon = TYPE_ICONS[a.type] || '•'; const badge = `${a.type}`; const date = a.date ? `${a.date}` : ''; const course = a.course ? `${a.course}` : ''; + const vid = a.video_url; + const panelId = vid ? `vp-${_uid++}` : ''; + const links = []; - if (a.type === 'transcript' && a.video_url) { - links.push(`▶ video`); + if (vid) { + links.push(``); } if (a.source_url && a.type === 'pdf') { links.push(`📄 PDF`); @@ -289,16 +330,52 @@ function cardHTML(a) { links.push(`view text`); } + const videoPanel = vid ? ` +
+
✕ close
+ ${embedHTML(vid)} +
` : ''; + return `
${icon}
${esc(a.title || a.id)}
${badge}${date}${course}
${links.length ? '' : ''} + ${videoPanel}
`; } +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,'>'); loadCatalog();