Podcasts im Browser: Stöbern, Lieblings-Abos, Suche → Folgenansicht (diora-android 1c1ec4e); podcasts-web.js speichert folders, refresh_interval, favorite; sw v140
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8df8ae2be3
commit
6e5092ab3f
4 changed files with 354 additions and 13 deletions
|
|
@ -76,7 +76,9 @@
|
|||
const SORT_RULES = ['', 'new', 'old', 'short', 'long', 'feed'];
|
||||
const FEED_SETTINGS = ['playback_speed', 'skip_intro', 'skip_outro', 'volume',
|
||||
'new_episodes', 'include_words', 'exclude_words', 'min_duration', 'auto_download', 'notify',
|
||||
'episode_sort', 'episode_filter', 'skip_silence', 'kind', 'folder'];
|
||||
'episode_sort', 'episode_filter', 'skip_silence', 'kind', 'folder',
|
||||
// Folders (any number), the fetch interval and a favourite subscription (docs/library.md, docs/podcasts.md).
|
||||
'folders', 'refresh_interval', 'favorite'];
|
||||
// The account's playback settings (diora-web PodcastSettings) and what each may be.
|
||||
const SKIPS = [5, 10, 15, 20, 30, 45, 60];
|
||||
const MIN_SPEED = 0.5;
|
||||
|
|
@ -455,7 +457,10 @@
|
|||
if (FEED_SETTINGS.indexOf(key) < 0) continue;
|
||||
f[key] = body[key] = changes[key];
|
||||
}
|
||||
if ('new_episodes' in body) f.auto_queue = body.new_episodes === 'queue';
|
||||
// Stamm (into the Merkliste, or straight into the Wiedergabeliste) queues by itself.
|
||||
if ('new_episodes' in body) f.auto_queue = body.new_episodes === 'queue' || body.new_episodes === 'play';
|
||||
// As the server does: the first folder is also `folder`.
|
||||
if (Array.isArray(body.folders)) f.folder = body.folders[0] || '';
|
||||
if (Object.keys(body).length) queueOp('/podcasts/feeds/' + feedId + '/settings/', body);
|
||||
});
|
||||
if (cur && cur.episode__feed__id === feedId) { applyKind(); report(); }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v139';
|
||||
const CACHE = 'diora-v140';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android ef914bd 2026-10-19: web-native/static/native-podcast.js
|
||||
// Copied from diora-android 1c1ec4e 2026-10-19: web-native/static/native-podcast.js
|
||||
// by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android
|
||||
// and copy again. The browser host is static/js/podcasts-web.js.
|
||||
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
{id: 'stream', label: 'Strom'},
|
||||
{id: 'feeds', label: 'Abos'},
|
||||
];
|
||||
const VIEW_IDS = VIEWS.map(v => v.id).concat('later', 'stream', 'search');
|
||||
const VIEW_IDS = VIEWS.map(v => v.id).concat('later', 'stream', 'browse', 'search');
|
||||
// The app's start page (this device; native.js opens it, the browser host too).
|
||||
const IN_APP = !window.DioraPodcastHost;
|
||||
// A function: the app loads this file in <head>, before #tab-home exists.
|
||||
|
|
@ -116,6 +116,7 @@
|
|||
const views = () => (v2() ? VIEWS_V2 : VIEWS);
|
||||
// A view of the other model (a start page, a notice, an older choice): its counterpart.
|
||||
function fixView() {
|
||||
if (!v2() && view === 'browse') view = 'inbox';
|
||||
if (v2() && (view === 'inbox' || view === 'episodes')) view = 'stream';
|
||||
if (!v2() && view === 'stream') view = 'inbox';
|
||||
if (!v2() && view === 'later') view = 'queue';
|
||||
|
|
@ -123,6 +124,23 @@
|
|||
// The play list's name.
|
||||
const QNAME = () => (v2() ? 'Wiedergabeliste' : 'Warteschlange');
|
||||
const inLater = e => later().some(x => x.episode__id === e.id) || !!(e.raw && e.raw.in_later);
|
||||
// Favourite subscriptions (feed.favorite, docs/podcasts.md): ★ in Abos, the „★ Faves“ chips.
|
||||
const isFave = feedId => { const f = feeds().find(x => x.id === feedId); return !!(f && f.favorite); };
|
||||
let podFaves = false;
|
||||
try { podFaves = localStorage.getItem('nui_pod_faves') === '1'; } catch (e) {}
|
||||
function setFaves(on) {
|
||||
podFaves = !!on;
|
||||
try { localStorage.setItem('nui_pod_faves', podFaves ? '1' : '0'); } catch (e) {}
|
||||
render();
|
||||
scrollTop(0);
|
||||
}
|
||||
function toggleFeedFave(f) {
|
||||
const on = !f.favorite;
|
||||
f.favorite = on;
|
||||
N.podcastFeedSettings(f.id, JSON.stringify({favorite: on}));
|
||||
toast(on ? '★ Lieblings-Abo' : 'Kein Lieblings-Abo mehr');
|
||||
render();
|
||||
}
|
||||
// All new episodes: the server sends the first ones (inbox_limit) and how many there are
|
||||
// (inbox_total); what left „Neu“ here since then counts off.
|
||||
function inboxTotal() {
|
||||
|
|
@ -417,6 +435,8 @@
|
|||
function play(e) {
|
||||
const r = remote();
|
||||
if (r ? r.kind === 'podcast' && r.episodeId === e.id : currentId() === e.id) { openPlayer(); return; }
|
||||
// An audiobook's chapter: heard in the library (its place, the rest of the book), never queued.
|
||||
if (isBook(feeds().find(f => f.id === e.feedId)) && openAudiobook(e.feedId)) return;
|
||||
// Two lists: ▶ puts it first in the Wiedergabeliste (in the Merkliste it stays until heard).
|
||||
if (v2()) {
|
||||
if (inQueue(e.id)) N.podcastMove(e.id, 0);
|
||||
|
|
@ -431,6 +451,15 @@
|
|||
toast('In der ' + QNAME() + (label ? ': ' + label.label.toLowerCase() : ''));
|
||||
}
|
||||
|
||||
function openAudiobook(feedId) {
|
||||
if (!feedId || typeof window.__dioraAudiobookOpen !== 'function') return false;
|
||||
closeNotes();
|
||||
closePlayer();
|
||||
if (window.DioraNav) DioraNav.go('audiobooks');
|
||||
window.__dioraAudiobookOpen(feedId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// +: onto the Merkliste, or off it again.
|
||||
function toggleLater(e) {
|
||||
if (!N.podcastLaterAdd) return;
|
||||
|
|
@ -1102,6 +1131,7 @@
|
|||
{id: 'queued', get label() { return v2() ? 'Wiedergabeliste' : 'Warteschlange'; }, test: o => inQueue(o.id),
|
||||
get none() { return 'Keine davon steht in der ' + QNAME() + '.'; }},
|
||||
{id: 'later', label: 'Merkliste', v2: true, test: o => inLater({id: o.id, raw: o}), none: 'Keine davon steht auf der Merkliste.'},
|
||||
{id: 'fave_feeds', label: '★ Faves', v2: true, test: o => isFave(o.feed__id), none: 'Keine Folgen deiner Lieblings-Abos.'},
|
||||
{id: 'downloaded', label: 'Geladen', app: true, test: o => dlDone(o.id), none: 'Keine Folgen auf dem Gerät.'},
|
||||
{id: 'history', label: 'Verlauf', all: true, test: o => !!o.played || (o.position_seconds || 0) > 0,
|
||||
none: 'Noch nichts gehört.'},
|
||||
|
|
@ -1333,12 +1363,13 @@
|
|||
if (view === 'queue') folderChips(queue().map(ep), e => e.feedId);
|
||||
else if (view === 'inbox') folderChips(inbox().map(ep), e => e.feedId);
|
||||
else if (view === 'later') folderChips(later().map(ep), e => e.feedId);
|
||||
else if (view === 'feeds') folderChips(podcastFeeds(), f => f.id);
|
||||
else if (view === 'feeds') folderChips(podcastFeeds(), f => f.id, v2());
|
||||
else if ((view === 'episodes' || view === 'stream') && !selected) folderChips(podcastFeeds(), f => f.id);
|
||||
if (view === 'queue') renderQueue();
|
||||
else if (view === 'inbox') renderInbox();
|
||||
else if (view === 'later') renderLater();
|
||||
else if (view === 'episodes' || view === 'stream') renderEpisodes();
|
||||
else if (view === 'browse') renderBrowse();
|
||||
else if (view === 'feeds') renderFeeds();
|
||||
else renderSearch();
|
||||
}
|
||||
|
|
@ -2035,6 +2066,243 @@
|
|||
root.append(list);
|
||||
}
|
||||
|
||||
// ---- Stöbern (diora-web docs/podcasts.md): within the subscriptions, and a step out ---------
|
||||
// Search in titles and shownotes, the week's topics, people, forgotten episodes,
|
||||
// recommendations; a person's episodes elsewhere go onto the Merkliste (their podcast
|
||||
// becomes „Auf Abruf“). Everything here needs a connection.
|
||||
|
||||
let browse = {mode: 'home'};
|
||||
const browseCache = new Map(); // path → {at, data}
|
||||
|
||||
async function browseGet(path, maxAge) {
|
||||
const hit = browseCache.get(path);
|
||||
if (hit && Date.now() - hit.at < (maxAge || 10 * 60000)) return hit.data;
|
||||
const data = await getJson(path);
|
||||
browseCache.set(path, {at: Date.now(), data});
|
||||
return data;
|
||||
}
|
||||
|
||||
function openBrowse() {
|
||||
selected = null;
|
||||
browse = {mode: 'home'};
|
||||
view = 'browse';
|
||||
render();
|
||||
scrollTop(0);
|
||||
}
|
||||
|
||||
function browseBack() {
|
||||
if (browse.mode !== 'home') { browse = {mode: 'home', q: browse.q && browse.mode === 'search' ? '' : ''}; render(); scrollTop(0); return true; }
|
||||
view = 'stream';
|
||||
render();
|
||||
scrollTop(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A section that loads by itself: its heading, „Wird geladen …“, then fill(box, data).
|
||||
function browseSection(title, path, fill, maxAge) {
|
||||
const sec = el('div', 'nui-pod-browsesec');
|
||||
if (title) sec.append(el('div', 'nui-pod-when', title));
|
||||
const box = el('div', 'nui-pod-browsebox');
|
||||
sec.append(box);
|
||||
const hit = browseCache.get(path);
|
||||
if (hit && Date.now() - hit.at < (maxAge || 10 * 60000)) {
|
||||
fill(box, hit.data);
|
||||
} else {
|
||||
box.append(el('div', 'nui-pod-meta', 'Wird geladen …'));
|
||||
browseGet(path, maxAge).then(d => { box.textContent = ''; fill(box, d); })
|
||||
.catch(() => { box.textContent = ''; box.append(el('div', 'nui-pod-meta', navigator.onLine ? 'Ließ sich gerade nicht laden.' : 'Stöbern braucht eine Verbindung.')); });
|
||||
}
|
||||
return sec;
|
||||
}
|
||||
|
||||
function browseRows(box, rows, limit) {
|
||||
const list = el('div', 'nui-pod-list');
|
||||
const shownRows = limit ? rows.slice(0, limit) : rows;
|
||||
for (const o of shownRows) list.append(episodeRow(ep(o), {showFeed: true, showDate: true, list: 'stream', noSelect: true}));
|
||||
box.append(list);
|
||||
if (!rows.length) box.append(el('div', 'nui-pod-meta', 'Nichts gefunden.'));
|
||||
return rows.length > shownRows.length;
|
||||
}
|
||||
|
||||
function moreLink(box, label, onClick) {
|
||||
const b = el('button', 'nui-pod-bundle', label);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', onClick);
|
||||
box.append(b);
|
||||
}
|
||||
|
||||
function chipRow(items) {
|
||||
const row = el('div', 'nui-pod-filters nui-pod-browsechips');
|
||||
for (const it of items) {
|
||||
const b = el('button', 'nui-pod-chip', it.label);
|
||||
b.type = 'button';
|
||||
if (it.count) b.append(el('span', 'nui-pod-chipcount', ' ' + it.count));
|
||||
b.addEventListener('click', it.onClick);
|
||||
row.append(b);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderBrowse() {
|
||||
const head = el('div', 'nui-pod-head nui-pod-browsehead');
|
||||
const titles = {home: 'Stöbern', search: 'Suche in deinen Podcasts', topic: browse.label, person: browse.name,
|
||||
forgotten: 'Vergessen', recommend: 'Empfehlungen'};
|
||||
head.append(button('‹', browse.mode === 'home' ? 'Zurück zum Strom' : 'Zurück zum Stöbern', browseBack, 'nui-pod-back'),
|
||||
el('div', 'nui-pod-browsetitle', titles[browse.mode] || 'Stöbern'));
|
||||
root.append(head);
|
||||
if (browse.mode === 'home' || browse.mode === 'search') {
|
||||
const form = el('form', 'nui-pod-search');
|
||||
const input = el('input', 'search-input');
|
||||
input.type = 'search';
|
||||
input.id = 'nui-pod-q';
|
||||
input.placeholder = 'In Titeln und Shownotes deiner Podcasts …';
|
||||
input.setAttribute('enterkeyhint', 'search');
|
||||
input.value = browse.q || '';
|
||||
const go = el('button', 'btn', 'Suchen');
|
||||
go.type = 'submit';
|
||||
form.append(input, go);
|
||||
form.addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
const q = input.value.trim();
|
||||
browse = q ? {mode: 'search', q} : {mode: 'home'};
|
||||
render();
|
||||
});
|
||||
root.append(form);
|
||||
}
|
||||
const body = el('div', 'nui-pod-browse');
|
||||
root.append(body);
|
||||
if (browse.mode === 'search') {
|
||||
body.append(browseSection('', `/podcasts/discover/search/?q=${encodeURIComponent(browse.q)}&stream=1`,
|
||||
(box, d) => browseRows(box, d.episodes || [])));
|
||||
return;
|
||||
}
|
||||
if (browse.mode === 'topic') {
|
||||
body.append(browseSection('', `/podcasts/episodes/?ids=${browse.ids.join(',')}&limit=100`,
|
||||
(box, d) => browseRows(box, (d.episodes || []).sort((a, b) => (Date.parse(b.pub_date) || 0) - (Date.parse(a.pub_date) || 0)))));
|
||||
return;
|
||||
}
|
||||
if (browse.mode === 'person') {
|
||||
body.append(browseSection('In deinen Podcasts', `/podcasts/discover/person/?name=${encodeURIComponent(browse.name)}`,
|
||||
(box, d) => browseRows(box, d.episodes || [])));
|
||||
body.append(browseSection('Anderswo', `/podcasts/discover/elsewhere/?name=${encodeURIComponent(browse.name)}`,
|
||||
(box, d) => elsewhereRows(box, d.episodes || []), 60 * 60000));
|
||||
return;
|
||||
}
|
||||
if (browse.mode === 'forgotten') {
|
||||
body.append(browseSection('Von den Podcasts, die du am meisten hörst', '/podcasts/discover/forgotten/',
|
||||
(box, d) => browseRows(box, d.loved || [])));
|
||||
body.append(browseSection('Von den seltenen', '/podcasts/discover/forgotten/', (box, d) => browseRows(box, d.rare || [])));
|
||||
return;
|
||||
}
|
||||
if (browse.mode === 'recommend') {
|
||||
body.append(browseSection('', '/podcasts/discover/recommend/', (box, d) => recommendCards(box, d.podcasts || []), 60 * 60000));
|
||||
return;
|
||||
}
|
||||
// home
|
||||
body.append(browseSection('Themen der Woche', '/podcasts/discover/topics/?days=7', (box, d) => {
|
||||
const ts = d.topics || [];
|
||||
if (!ts.length) { box.append(el('div', 'nui-pod-meta', 'Diese Woche keine gemeinsamen Themen.')); return; }
|
||||
box.append(chipRow(ts.map(t => ({label: t.label, count: t.ids.length,
|
||||
onClick: () => { browse = {mode: 'topic', label: t.label, ids: t.ids}; render(); scrollTop(0); }}))));
|
||||
}));
|
||||
body.append(browseSection('Personen', '/podcasts/discover/people/', (box, d) => {
|
||||
const ps = (d.people || []).slice(0, 24);
|
||||
if (!ps.length) { box.append(el('div', 'nui-pod-meta', 'Noch keine Namen gefunden.')); return; }
|
||||
box.append(chipRow(ps.map(x => ({label: x.name, count: x.episodes,
|
||||
onClick: () => { browse = {mode: 'person', name: x.name}; render(); scrollTop(0); }}))));
|
||||
}, 60 * 60000));
|
||||
body.append(browseSection('Vergessen', '/podcasts/discover/forgotten/', (box, d) => {
|
||||
const rows = (d.loved || []).slice(0, 3).concat((d.rare || []).slice(0, 2));
|
||||
browseRows(box, rows);
|
||||
moreLink(box, 'Alles Vergessene ›', () => { browse = {mode: 'forgotten'}; render(); scrollTop(0); });
|
||||
}));
|
||||
body.append(browseSection('Empfohlen', '/podcasts/discover/recommend/', (box, d) => {
|
||||
const ps = d.podcasts || [];
|
||||
recommendCards(box, ps.slice(0, 3));
|
||||
if (ps.length > 3) moreLink(box, `Alle ${ps.length} Empfehlungen ›`, () => { browse = {mode: 'recommend'}; render(); scrollTop(0); });
|
||||
}, 60 * 60000));
|
||||
}
|
||||
|
||||
// Episodes naming a person in podcasts not subscribed: + puts one on the Merkliste and
|
||||
// subscribes its podcast „Auf Abruf“ (POST /podcasts/discover/take/).
|
||||
function elsewhereRows(box, items) {
|
||||
if (!items.length) { box.append(el('div', 'nui-pod-meta', 'Anderswo nichts gefunden.')); return; }
|
||||
const list = el('div', 'nui-pod-list');
|
||||
for (const it of items) {
|
||||
const row = el('div', 'nui-pod-row');
|
||||
row.append(art(it.artwork_url, null, null, it.podcast));
|
||||
const main = el('div', 'nui-pod-main');
|
||||
main.append(el('div', 'nui-pod-title', plain(it.title)));
|
||||
const meta = [];
|
||||
if (it.duration_seconds) meta.push(minutes(it.duration_seconds));
|
||||
if (it.pub_date) meta.push(day(it.pub_date));
|
||||
if (it.podcast) meta.push(plain(it.podcast));
|
||||
main.append(el('div', 'nui-pod-meta', meta.join(' · ')));
|
||||
row.append(main);
|
||||
const take = button('+', 'Auf die Merkliste (der Podcast kommt „Auf Abruf“ in die Abos)', async () => {
|
||||
take.disabled = true;
|
||||
try {
|
||||
const r = await fetch('/podcasts/discover/take/', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(it)});
|
||||
const d = await r.json();
|
||||
if (!r.ok || !d.ok) throw new Error(d.error || r.status);
|
||||
take.textContent = '✓';
|
||||
take.classList.add('on');
|
||||
toast(d.subscribed ? `Auf der Merkliste · „${plain(it.podcast)}“ auf Abruf abonniert` : 'Auf der Merkliste');
|
||||
refresh(false);
|
||||
} catch (e) {
|
||||
take.disabled = false;
|
||||
toast(navigator.onLine ? 'Ließ sich nicht merken' : 'Braucht eine Verbindung');
|
||||
}
|
||||
}, 'nui-pod-mini');
|
||||
row.append(take);
|
||||
list.append(row);
|
||||
}
|
||||
box.append(list);
|
||||
}
|
||||
|
||||
// Podcasts like the ones heard most (fyyd): subscribe „Auf Abruf“ (or open the preview).
|
||||
function recommendCards(box, items) {
|
||||
if (!items.length) { box.append(el('div', 'nui-pod-meta', 'Gerade keine Empfehlungen.')); return; }
|
||||
const have = new Set(feeds().map(f => f.rss_url));
|
||||
for (const p of items) {
|
||||
const card = el('div', 'nui-pod-row nui-pod-reco');
|
||||
card.append(art(p.artwork_url, null, null, p.title));
|
||||
const main = el('div', 'nui-pod-main');
|
||||
main.append(el('div', 'nui-pod-title', plain(p.title)));
|
||||
if (p.because && p.because.length) main.append(el('div', 'nui-pod-meta', 'weil du ' + p.because.slice(0, 2).map(plain).join(' und ') + ' hörst'));
|
||||
if (p.description) main.append(el('div', 'nui-pod-meta nui-pod-reco-desc', plain(p.description).slice(0, 160)));
|
||||
card.append(main);
|
||||
if (have.has(p.feed_url)) {
|
||||
card.append(el('span', 'nui-pod-badge', 'Abo'));
|
||||
} else {
|
||||
const sub = button('+', 'Abonnieren – „Auf Abruf“: nicht im Strom, nur auf seiner Seite', async () => {
|
||||
sub.disabled = true;
|
||||
if (await subscribeOnDemand(p.feed_url, p.title)) { sub.textContent = '✓'; sub.classList.add('on'); } else sub.disabled = false;
|
||||
}, 'nui-pod-mini');
|
||||
card.append(sub);
|
||||
}
|
||||
if (N.podcastPreview) main.addEventListener('click', () => openPreview(p.feed_url));
|
||||
box.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribeOnDemand(url, title) {
|
||||
try {
|
||||
const r = await fetch('/podcasts/feeds/add/', {method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({rss_url: url, title})});
|
||||
const d = await r.json();
|
||||
if (!r.ok || !d.ok) throw new Error(d.error || r.status);
|
||||
await fetch(`/podcasts/feeds/${d.feed_id}/settings/`, {method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({new_episodes: 'ignore'})});
|
||||
toast(`„${plain(d.title || title)}“ auf Abruf abonniert`);
|
||||
refresh(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast(navigator.onLine ? 'Ließ sich nicht abonnieren' : 'Abonnieren braucht eine Verbindung');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderInbox() {
|
||||
const all = inbox().map(ep);
|
||||
if (!all.length) {
|
||||
|
|
@ -2125,7 +2393,7 @@
|
|||
}
|
||||
|
||||
// The chips (folders.js) above a list: each folder with how many of the list are in it.
|
||||
function folderChips(items, feedIdOf) {
|
||||
function folderChips(items, feedIdOf, faves) {
|
||||
if (!hasFolders()) return;
|
||||
const names = namesOf(podcastFeeds());
|
||||
if (!names.length || (podFolder && !names.includes(podFolder))) podFolder = null;
|
||||
|
|
@ -2139,11 +2407,24 @@
|
|||
for (const n of fs) counts.set(n, (counts.get(n) || 0) + 1);
|
||||
}
|
||||
const box = el('div', 'nui-pod-folders');
|
||||
const favesOn = faves && podFaves;
|
||||
const nFaves = faves ? items.filter(x => isFave(feedIdOf(x))).length : 0;
|
||||
DioraFolders.chips(box, {
|
||||
folders: names.map(name => ({name, count: counts.get(name) || 0})),
|
||||
active: podFolder,
|
||||
none: none || (podFolder === '' ? '0' : 0),
|
||||
onPick: name => setFolder(typeof name === 'string' ? name : null),
|
||||
lead: faves && nFaves ? [{id: 'all', label: 'Alle', on: podFolder == null && !favesOn},
|
||||
{id: 'faves', label: '★ Faves', count: nFaves, on: favesOn}] : null,
|
||||
onPick: name => {
|
||||
if (name && typeof name === 'object') {
|
||||
if (name.lead === 'faves') { setFaves(!podFaves); return; }
|
||||
podFaves = false;
|
||||
try { localStorage.setItem('nui_pod_faves', '0'); } catch (e) {}
|
||||
setFolder(null);
|
||||
return;
|
||||
}
|
||||
setFolder(typeof name === 'string' ? name : null);
|
||||
},
|
||||
onManage: manageFolders,
|
||||
});
|
||||
root.append(box);
|
||||
|
|
@ -2321,6 +2602,12 @@
|
|||
row.addEventListener('click', () => toggleSelected(f.id));
|
||||
return row;
|
||||
}
|
||||
if (v2() && 'favorite' in f && N.podcastFeedSettings) {
|
||||
const star = button(f.favorite ? '★' : '☆', f.favorite ? 'Lieblings-Abo – antippen nimmt es heraus' : 'Als Lieblings-Abo markieren',
|
||||
() => toggleFeedFave(f), 'nui-pod-mini nui-pod-starbtn' + (f.favorite ? ' on' : ''));
|
||||
star.addEventListener('click', ev => ev.stopPropagation());
|
||||
row.append(star);
|
||||
}
|
||||
if (v2()) {
|
||||
if (f.new_episodes === 'queue' || f.new_episodes === 'play') row.append(el('span', 'nui-pod-badge', 'Stamm'));
|
||||
else if (f.new_episodes === 'ignore') row.append(el('span', 'nui-pod-badge', 'Abruf'));
|
||||
|
|
@ -2338,7 +2625,8 @@
|
|||
|
||||
function renderFeeds() {
|
||||
const sort = FEED_SORTS.find(s => s.id === feedSort) || FEED_SORTS[0];
|
||||
const items = podcastFeeds().filter(f => inFolder(f.id)).sort((a, b) => sort.cmp(a, b) || byTitle(a, b));
|
||||
const items = podcastFeeds().filter(f => inFolder(f.id) && (!podFaves || !v2() || f.favorite))
|
||||
.sort((a, b) => sort.cmp(a, b) || byTitle(a, b));
|
||||
const books = feeds().length - items.length;
|
||||
const booksHint = () => el('div', 'nui-pod-hint',
|
||||
`${books === 1 ? 'Ein Hörbuch' : books + ' Hörbücher'}: unter Hören → Hörbücher`);
|
||||
|
|
@ -2375,6 +2663,15 @@
|
|||
feedSettingsSheet(f);
|
||||
return true;
|
||||
};
|
||||
// The search (diora-web docs/search.md): a row as the episode lists give it; true when opened.
|
||||
window.__dioraPodcastOpenEpisode = function (row) {
|
||||
if (!row || row.id == null) return false;
|
||||
const e = ep(row);
|
||||
if (isBook(feeds().find(f => f.id === e.feedId)) && openAudiobook(e.feedId)) return true;
|
||||
openEpisode(e, {list: 'search'});
|
||||
return true;
|
||||
};
|
||||
|
||||
window.__dioraPodcastOpenFeed = function (feedId) {
|
||||
const f = feeds().find(x => x.id === Number(feedId));
|
||||
if (!f) return false;
|
||||
|
|
@ -2933,9 +3230,10 @@
|
|||
const head = el('div', 'nui-pod-head');
|
||||
head.append(el('div', 'nui-pod-summary', `${nEpisodes(total)} · ${sortLabel(epSort).toLowerCase()}`),
|
||||
listTools([
|
||||
view === 'stream' && button('✦', 'Stöbern: suchen, Themen, Personen, Vergessenes, Empfehlungen', openBrowse, 'nui-pod-browsebtn'),
|
||||
button('⇅', 'Sortieren', () => sortMenuFor(epSort, true, id => setEpisodeList(epFilters, id))),
|
||||
button('☐', 'Mehrere auswählen', () => startSelecting()),
|
||||
]));
|
||||
].filter(Boolean)));
|
||||
root.append(head);
|
||||
}
|
||||
const list = el('div', 'nui-pod-list');
|
||||
|
|
@ -4077,6 +4375,19 @@
|
|||
const actions = el('div', 'nui-pod-epactions');
|
||||
const box = el('div', 'nui-pod-notesbox');
|
||||
panel.append(top, head, playBtn, actions, box);
|
||||
// Similar episodes from the other podcasts (docs/podcasts.md, Stöbern).
|
||||
if (v2() && e.id > 0) {
|
||||
const sim = el('div', 'nui-pod-similar');
|
||||
panel.append(sim);
|
||||
browseGet(`/podcasts/discover/similar/${e.id}/`, 60 * 60000).then(d => {
|
||||
const rows = (d.episodes || []).slice(0, 6);
|
||||
if (!rows.length || !sim.isConnected) return;
|
||||
sim.append(el('div', 'nui-pod-when', 'Ähnliche Folgen'));
|
||||
const list = el('div', 'nui-pod-list');
|
||||
for (const o of rows) list.append(episodeRow(ep(o), {showFeed: true, showDate: true, list: 'stream', noSelect: true}));
|
||||
sim.append(list);
|
||||
}).catch(() => {});
|
||||
}
|
||||
document.body.append(notesSheet);
|
||||
epScreen = {e, ctx, actions, playBtn, facts: info.querySelector('.nui-pod-epfacts')};
|
||||
episodeScreenParts();
|
||||
|
|
@ -4251,7 +4562,20 @@
|
|||
panel.append(top, cover);
|
||||
const chapBox = el('div', 'nui-pod-chapbox');
|
||||
chapBox.hidden = true;
|
||||
panel.append(el('div', 'nui-pod-player-title'), el('div', 'nui-pod-player-feed'), chapBox);
|
||||
const feedLine = el('div', 'nui-pod-player-feed nui-pod-player-link');
|
||||
feedLine.setAttribute('role', 'button');
|
||||
feedLine.title = 'Zur Seite des Podcasts';
|
||||
// Tapping the podcast's (or the audiobook's) name: its page.
|
||||
feedLine.addEventListener('click', () => {
|
||||
const id = player.feedId;
|
||||
if (!id) return;
|
||||
if (isBook(feeds().find(f => f.id === id)) || player.book) {
|
||||
if (openAudiobook(id)) return;
|
||||
}
|
||||
closePlayer();
|
||||
window.__dioraPodcastOpenFeed(id);
|
||||
});
|
||||
panel.append(el('div', 'nui-pod-player-title'), feedLine, chapBox);
|
||||
const range = el('input', 'nui-pod-seek');
|
||||
range.type = 'range';
|
||||
range.min = 0;
|
||||
|
|
@ -5323,10 +5647,11 @@
|
|||
// From the search back to the list it was opened from.
|
||||
if (view === 'search') {
|
||||
try { view = localStorage.getItem('nui_pod_view') || 'queue'; } catch (e) { view = 'queue'; }
|
||||
if (!VIEWS.some(v => v.id === view)) view = 'queue';
|
||||
if (!views().some(v => v.id === view)) view = 'queue';
|
||||
render();
|
||||
return true;
|
||||
}
|
||||
if (view === 'browse') return browseBack();
|
||||
}
|
||||
if (back && back()) return true;
|
||||
if (onPodcasts) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copied from diora-android ef914bd 2026-10-19: the podcast parts of web-native/static/native.css
|
||||
/* Copied from diora-android 1c1ec4e 2026-10-19: the podcast parts of web-native/static/native.css
|
||||
(bottom sheet, podcasts) by tools/sync_podcast_ui.py. Do not edit here.
|
||||
The web's own additions are in podcasts-web.css. */
|
||||
|
||||
|
|
@ -232,6 +232,8 @@ button.nui-pod-link { display: block; width: 100%; border: none; background: non
|
|||
/* Headings by time in the episode lists (Heute, Gestern, Diese Woche, …). */
|
||||
.nui-pod-when { padding: 14px 2px 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted); font-family: var(--nui-sans); }
|
||||
.nui-pod-when:first-child { padding-top: 4px; }
|
||||
.nui-pod-player-link { cursor: pointer; }
|
||||
.nui-pod-player-link:active { opacity: 0.6; }
|
||||
/* The two lists (docs/podcasts.md): four tabs that fit a phone, the Merkliste by podcast, the Strom bundled. */
|
||||
.nui-pod-tabs-v2 .nui-pod-tab { font-size: 13px; padding-left: 4px; padding-right: 4px; }
|
||||
.nui-pod-group { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 2px; background: none; border: none; border-bottom: 1px solid var(--nui-line, rgba(127,127,127,.2)); color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
||||
|
|
@ -248,6 +250,15 @@ button.nui-pod-link { display: block; width: 100%; border: none; background: non
|
|||
.nui-pod-bundle.nui-pod-fresh::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent, #e63946); margin-right: 6px; vertical-align: middle; }
|
||||
.nui-pod-bundle { align-self: flex-start; margin: -2px 0 6px 56px; padding: 4px 0; background: none; border: none; color: var(--nui-muted); font: inherit; font-size: 12px; cursor: pointer; }
|
||||
.nui-pod-bundled { padding-left: 14px; }
|
||||
/* Stöbern (docs/podcasts.md). */
|
||||
.nui-pod-browsehead { align-items: center; gap: 6px; justify-content: flex-start; }
|
||||
.nui-pod-browsetitle { font-weight: 600; font-size: 15px; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nui-pod-browsechips { flex-wrap: wrap; overflow: visible; }
|
||||
.nui-pod-chipcount { opacity: 0.6; font-size: 11px; }
|
||||
.nui-pod-browsesec { margin-bottom: 6px; }
|
||||
.nui-pod-reco .nui-pod-main { cursor: pointer; }
|
||||
.nui-pod-reco-desc { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.nui-pod-similar { margin-top: 12px; }
|
||||
.nui-pod-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue