diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-07 15:56:35 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-07 15:56:35 +0300 |
| commit | 7f3ca21cc42979b8f1898f5dfd81f51f04aed5e0 (patch) | |
| tree | 47987d9c73f2ce760cefed045ec85eee53747ff7 /web/js | |
| parent | 3f70adb2c24d558be645aacb11a97d5f95f69b76 (diff) | |
Fix podcast browser workflows for k1
Diffstat (limited to 'web/js')
| -rw-r--r-- | web/js/podcasts.js | 74 | ||||
| -rw-r--r-- | web/js/views/media-grid.js | 13 |
2 files changed, 69 insertions, 18 deletions
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 => - `<div class="flex gap-2 align-center py-1 border-b"> + `<div class="podcast-feed-row py-1 border-b"> + <img class="podcast-feed-cover" src="/api/sets/${p.id}/cover" alt="" loading="lazy"> <span class="flex-1">${escapeHtml(p.name)}</span> <span class="text-xs text-muted">${escapeHtml(p.root_path)}</span> </div>` @@ -55,6 +59,15 @@ export function initPodcasts() { listEl.innerHTML = `<p class="error-message">${escapeHtml(err.message)}</p>`; } } + + 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 + ? '<span class="text-xs text-muted">Downloaded</span>' + : '<button class="btn btn-primary btn-sm btn-download-episode" data-action="download-episode">Download</button>'; return ` <div class="thumb-wrap"> <span class="placeholder">🎙️</span> @@ -102,6 +147,7 @@ function renderEpisodeHtml(ep) { <div class="meta"> <div class="title">${escapeHtml(ep.title || 'Untitled')}</div> <div class="subtitle">${escapeHtml(ep.description || 'Podcast episode')}</div> + ${downloadButton} </div> `; } 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 = '<p class="text-muted text-sm grid-full">Folder is empty.</p>'; 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(() => {}); } } |
