From 7f3ca21cc42979b8f1898f5dfd81f51f04aed5e0 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 7 May 2026 15:56:35 +0300 Subject: Fix podcast browser workflows for k1 --- web/css/layout.css | 23 ++++++++++++++ web/js/podcasts.js | 74 +++++++++++++++++++++++++++++++++++++--------- web/js/views/media-grid.js | 13 +++++--- 3 files changed, 92 insertions(+), 18 deletions(-) (limited to 'web') diff --git a/web/css/layout.css b/web/css/layout.css index 909ecfb..d73d07f 100644 --- a/web/css/layout.css +++ b/web/css/layout.css @@ -607,3 +607,26 @@ body { .admin-list li.is-admin { background: var(--accent-soft); } + +.podcast-feed-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0; + border-bottom: 1px solid var(--border); +} + +.podcast-feed-cover { + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-sm); + object-fit: cover; + background: var(--bg-elevated); + border: 1px solid var(--border); + flex: 0 0 auto; +} + +.btn-download-episode { + align-self: flex-start; + margin-top: 0.2rem; +} diff --git a/web/js/podcasts.js b/web/js/podcasts.js index 1883e63..4692f7b 100644 --- a/web/js/podcasts.js +++ b/web/js/podcasts.js @@ -1,6 +1,8 @@ // podcastUI.js — Podcast feed manager + episode rendering. import { API } from './api.js'; +import { state } from './state.js'; import { escapeHtml, fmtDur, toast } from './utils.js'; +import { renderSets } from './views/sets.js'; export function initPodcasts() { const modal = document.getElementById('podcast-modal'); @@ -32,6 +34,7 @@ export function initPodcasts() { urlInput.value = ''; nameInput.value = ''; refreshPodcasts(); + refreshSets(); } catch (err) { toast(err.message || 'Subscribe failed', 'error'); } @@ -46,7 +49,8 @@ export function initPodcasts() { return; } listEl.innerHTML = podcasts.map(p => - `
+ `
+ ${escapeHtml(p.name)} ${escapeHtml(p.root_path)}
` @@ -55,6 +59,15 @@ export function initPodcasts() { listEl.innerHTML = `

${escapeHtml(err.message)}

`; } } + + async function refreshSets() { + try { + state.sets = await API.sets() || []; + renderSets(); + } catch { + // The subscription succeeded; leave the existing sidebar in place if refresh fails. + } + } } export function renderPodcastEpisodes(grid, episodes) { @@ -68,21 +81,50 @@ export function renderPodcastEpisodes(grid, episodes) { const card = document.createElement('div'); card.className = 'media-card episode-card'; card.dataset.id = ep.id; - card.innerHTML = renderEpisodeHtml(ep); + renderEpisodeCard(card, ep); grid.appendChild(card); + }); +} + +function renderEpisodeCard(card, ep) { + card.innerHTML = renderEpisodeHtml(ep); + + const playBtn = card.querySelector('[data-action="play"]'); + const downloadBtn = card.querySelector('[data-action="download-episode"]'); + const completeBtn = card.querySelector('.btn-complete'); + + playBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + toast(ep.is_downloaded ? 'Play from downloads' : 'Download first to play', 'info'); + }); - const playBtn = card.querySelector('[data-action="play"]'); - const completeBtn = card.querySelector('.btn-complete'); - playBtn?.addEventListener('click', () => { - toast(ep.is_downloaded ? 'Play from downloads' : 'Download first to play', 'info'); - }); - completeBtn?.addEventListener('click', async () => { - try { - const res = await API.toggleEpisodeComplete(ep.id); - completeBtn.classList.toggle('active'); - toast(res.is_completed ? 'Marked listened' : 'Marked unlistened'); - } catch (err) { toast(err.message || 'Toggle failed', 'error'); } - }); + downloadBtn?.addEventListener('click', async (e) => { + e.stopPropagation(); + downloadBtn.disabled = true; + try { + const media = await API.downloadEpisode(ep.id); + ep.is_downloaded = true; + ep.media_id = media.id; + ep.file_name = media.file_name; + toast('Episode downloaded'); + card.dispatchEvent(new CustomEvent('podcast:episode-downloaded', { bubbles: true, detail: { media } })); + renderEpisodeCard(card, ep); + } catch (err) { + downloadBtn.disabled = false; + toast(err.message || 'Download failed', 'error'); + } + }); + + completeBtn?.addEventListener('click', async (e) => { + e.stopPropagation(); + try { + await API.toggleEpisodeComplete(ep.id); + ep.is_completed = !ep.is_completed; + completeBtn.classList.toggle('active', ep.is_completed); + toast(ep.is_completed ? 'Marked listened' : 'Marked unlistened'); + } catch (err) { + toast(err.message || 'Toggle failed', 'error'); + } }); } @@ -90,6 +132,9 @@ function renderEpisodeHtml(ep) { const completed = ep.is_completed; const dateStr = ep.published_at ? new Date(ep.published_at).toLocaleDateString() : ''; const duration = ep.duration_seconds ? fmtDur(ep.duration_seconds) : ''; + const downloadButton = ep.is_downloaded + ? 'Downloaded' + : ''; return `
🎙️ @@ -102,6 +147,7 @@ function renderEpisodeHtml(ep) {
${escapeHtml(ep.title || 'Untitled')}
${escapeHtml(ep.description || 'Podcast episode')}
+ ${downloadButton}
`; } diff --git a/web/js/views/media-grid.js b/web/js/views/media-grid.js index 5c16de0..08435b8 100644 --- a/web/js/views/media-grid.js +++ b/web/js/views/media-grid.js @@ -30,6 +30,10 @@ export function initMediaGrid(options = {}) { enterFolder(folder.dataset.name); } }); + + grid?.addEventListener('podcast:episode-downloaded', () => { + loadMedia(); + }); } export async function loadMedia() { @@ -52,7 +56,7 @@ export async function loadMedia() { updateBreadcrumb(data.current_path); setMedia(mediaWithBrowsePath(data.media || [], data.current_path || '')); renderBrowse(data); - const total = (data.media?.length || 0) + (data.folders?.length || 0); + const total = (data.media?.length || 0) + (data.folders?.length || 0) + (data.episodes?.length || 0); resultCount.textContent = `${total} items`; } else { breadcrumb?.classList.add('hidden'); @@ -129,7 +133,8 @@ function renderBrowse(data) { if (!grid) return; const folders = data.folders || []; const media = mediaWithBrowsePath(data.media || [], data.current_path || ''); - if (!folders.length && !media.length) { + const episodes = data.episodes || []; + if (!folders.length && !media.length && !episodes.length) { grid.innerHTML = '

Folder is empty.

'; clearSelection(); return; @@ -164,9 +169,9 @@ function renderBrowse(data) { bindMediaItems(grid); - if (data.episodes && data.episodes.length) { + if (episodes.length) { import('../podcasts.js').then(m => { - m.renderPodcastEpisodes(grid, data.episodes); + m.renderPodcastEpisodes(grid, episodes); }).catch(() => {}); } } -- cgit v1.2.3