From a7a346ef6018ec675c2256380f3f7758a24fc5ed Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 5 May 2026 11:14:40 +0300 Subject: Add frontend podcast manager UI and episode renderer --- web/js/app.js | 2 + web/js/podcasts.js | 182 +++++++++++++++++++++++++++++------------------------ 2 files changed, 103 insertions(+), 81 deletions(-) diff --git a/web/js/app.js b/web/js/app.js index 4029825..fb6e787 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -7,6 +7,7 @@ import { initShuffle, toggle as toggleShuffle, isOn as isShuffle } from './shuff import { initThemes } from './themes.js'; import { initNotes, open as openNotes } from './notes.js'; import { initAdmin } from './admin.js'; +import { initPodcasts } from './podcasts.js'; import { state, setMedia } from './state.js'; import { initPWA } from './pwa.js'; import { initLightbox, open as openLightbox, close as closeLightbox, isOpen as isLightboxOpen, next as lightboxNext, prev as lightboxPrev, zoomIn as lightboxZoomIn, zoomOut as lightboxZoomOut, toggleSlideshow as lightboxToggleSlideshow } from './lightbox.js'; @@ -174,6 +175,7 @@ async function initApp() { }); initNotes(() => toast('Note saved')); initAdmin(); + initPodcasts(); initPWA(); initUpload(); initHelp(); diff --git a/web/js/podcasts.js b/web/js/podcasts.js index c82d2eb..c683159 100644 --- a/web/js/podcasts.js +++ b/web/js/podcasts.js @@ -1,117 +1,137 @@ // podcastUI.js — Podcast feed manager + episode rendering. +import { API } from './api.js'; -export function createPodcastManager(container, api, renderEpisodeList) { - const el = document.createElement('div'); - el.id = 'podcast-manager'; - el.className = 'modal'; - el.innerHTML = ` - - `; - container.appendChild(el); +export function initPodcasts() { + const modal = document.getElementById('podcast-modal'); + const closeBtn = document.getElementById('podcast-close'); + const form = document.getElementById('podcast-form'); + const urlInput = document.getElementById('podcast-url'); + const nameInput = document.getElementById('podcast-name'); + const listEl = document.getElementById('podcast-list'); + const adminBtn = document.getElementById('admin-podcasts'); - const urlInput = el.querySelector('#pm-url'); - const nameInput = el.querySelector('#pm-name'); - const addBtn = el.querySelector('#pm-add'); - const listEl = el.querySelector('#pm-list'); - const closeBtn = el.querySelector('.btn-close'); + if (!modal) return; - async function refresh() { - try { - const podcasts = await api.podcasts(); - listEl.innerHTML = podcasts.length === 0 - ? '

No podcasts subscribed yet.

' - : podcasts.map(p => ` -
- ${p.name} - ${p.is_podcast ? 'Podcast' : 'Set'} -
- `).join(''); - } catch (err) { - listEl.innerHTML = `

Error: ${err.message}

`; - } - } + adminBtn?.addEventListener('click', () => { + modal.classList.add('open'); + refreshPodcasts(); + }); + + closeBtn?.addEventListener('click', () => modal.classList.remove('open')); + modal?.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('open'); }); - addBtn.addEventListener('click', async () => { + form?.addEventListener('submit', async (e) => { + e.preventDefault(); const url = urlInput.value.trim(); + const name = nameInput.value.trim(); if (!url) return; - addBtn.disabled = true; try { - await api.subscribePodcast(url, nameInput.value.trim()); + await API.subscribePodcast(url, name); + toast('Subscribed to podcast'); urlInput.value = ''; nameInput.value = ''; - await refresh(); + refreshPodcasts(); } catch (err) { - alert('Failed to subscribe: ' + err.message); + toast(err.message || 'Subscribe failed', 'error'); } - addBtn.disabled = false; }); - closeBtn.addEventListener('click', () => el.classList.remove('open')); - - return { - open() { - el.classList.add('open'); - refresh(); - }, - close() { - el.classList.remove('open'); + async function refreshPodcasts() { + if (!listEl) return; + try { + const podcasts = await API.podcasts(); + if (!podcasts || !podcasts.length) { + listEl.innerHTML = '

No podcasts subscribed.

'; + return; + } + listEl.innerHTML = podcasts.map(p => + `
+ ${escapeHtml(p.name)} + ${escapeHtml(p.root_path)} +
` + ).join(''); + } catch (err) { + listEl.innerHTML = `

${escapeHtml(err.message)}

`; } - }; + } } -export function renderEpisodeCard(ep, onDownload, onToggleComplete) { - const isDownloaded = ep.is_downloaded; +export function insertPodcastEpisodes(state) { + if (!state.selectedSetId || state.selectedSetIds.length !== 1) return; + const set = state.sets.find(s => s.id === state.selectedSetId); + if (!set || !set.is_podcast) return; + + const grid = document.getElementById('media-grid'); + if (!grid) return; + + API.podcastEpisodes(state.selectedSetId).then(episodes => { + if (!episodes || !episodes.length) return; + const divider = document.createElement('div'); + divider.className = 'grid-divider'; + divider.textContent = 'Podcast Episodes'; + grid.appendChild(divider); + + episodes.forEach(ep => { + const card = document.createElement('div'); + card.className = 'media-card episode-card'; + card.innerHTML = renderEpisodeHtml(ep); + grid.appendChild(card); + + const downloadBtn = card.querySelector('.btn-download-episode'); + const completeBtn = card.querySelector('.btn-complete'); + downloadBtn?.addEventListener('click', async () => { + try { + await API.downloadEpisode(ep.id); + toast('Download started'); + } catch (err) { toast(err.message || 'Download failed', 'error'); } + }); + completeBtn?.addEventListener('click', async () => { + try { + await API.toggleEpisodeComplete(ep.id); + completeBtn.classList.toggle('active'); + toast(ep.is_completed ? 'Marked unlistened' : 'Marked listened'); + } catch (err) { toast(err.message || 'Toggle failed', 'error'); } + }); + }); + }).catch(() => {}); // silently ignore errors +} + +function renderEpisodeHtml(ep) { const completed = ep.is_completed; const dateStr = ep.published_at ? new Date(ep.published_at).toLocaleDateString() : ''; - const duration = ep.duration_seconds ? formatDuration(ep.duration_seconds) : ''; - const size = ep.file_size ? formatBytes(ep.file_size) : ''; - + const duration = ep.duration_seconds ? fmtDuration(ep.duration_seconds) : ''; return ` -
-
-
${escapeHtml(ep.title)}
-
- ${dateStr ? `${dateStr}` : ''} - ${duration ? `${duration}` : ''} - ${size ? `${size}` : ''} -
-
+
+ 🎙️ + ${dateStr}${duration ? ' • ' + duration : ''}
- ${!isDownloaded - ? `` - : `` - } - + +
+
+
${escapeHtml(ep.title || 'Untitled')}
+
${escapeHtml(ep.description || 'Podcast episode')}
+
`; } -function formatDuration(s) { +function fmtDuration(s) { const m = Math.floor(s / 60); const h = Math.floor(m / 60); if (h > 0) return `${h}h ${m % 60}m`; return `${m}m`; } -function formatBytes(b) { - if (b < 1024) return `${b} B`; - if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; - return `${(b / (1024 * 1024)).toFixed(1)} MB`; -} - function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); } + +function toast(msg, type = 'info') { + const t = document.getElementById('toast'); + if (!t) return; + t.textContent = msg; + t.className = 'toast show ' + type; + setTimeout(() => t.classList.remove('show'), 2800); +} -- cgit v1.2.3