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 ? '
' + links.join('') + '
' : ''}
+ ${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();