summaryrefslogtreecommitdiff
path: root/web/js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-07 15:56:35 +0300
committerPaul Buetow <paul@buetow.org>2026-05-07 15:56:35 +0300
commit7f3ca21cc42979b8f1898f5dfd81f51f04aed5e0 (patch)
tree47987d9c73f2ce760cefed045ec85eee53747ff7 /web/js
parent3f70adb2c24d558be645aacb11a97d5f95f69b76 (diff)
Fix podcast browser workflows for k1
Diffstat (limited to 'web/js')
-rw-r--r--web/js/podcasts.js74
-rw-r--r--web/js/views/media-grid.js13
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(() => {});
}
}