Podcasts im Browser: Wiedergabeliste · Merkliste · Strom · Abos (diora-android ef914bd), sw v137
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3ef2bdcb61
commit
3c9185689a
3 changed files with 395 additions and 41 deletions
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v136';
|
||||
const CACHE = 'diora-v137';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android bd98557 2026-10-19: web-native/static/native-podcast.js
|
||||
// Copied from diora-android ef914bd 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.
|
||||
|
||||
|
|
@ -31,7 +31,16 @@
|
|||
{id: 'episodes', label: 'Folgen'},
|
||||
{id: 'feeds', label: 'Abos'},
|
||||
];
|
||||
const VIEW_IDS = VIEWS.map(v => v.id).concat('search');
|
||||
// With the two lists (diora-web docs/podcasts.md, state.lists_v2): the queue is the
|
||||
// Wiedergabeliste, the Merkliste holds what to hear some day, the Strom replaces „Neu“
|
||||
// and „Folgen“.
|
||||
const VIEWS_V2 = [
|
||||
{id: 'queue', label: 'Wiedergabeliste'},
|
||||
{id: 'later', label: 'Merkliste'},
|
||||
{id: 'stream', label: 'Strom'},
|
||||
{id: 'feeds', label: 'Abos'},
|
||||
];
|
||||
const VIEW_IDS = VIEWS.map(v => v.id).concat('later', 'stream', '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.
|
||||
|
|
@ -72,6 +81,13 @@
|
|||
let state = parse(N.podcastState(), {});
|
||||
let player = {kind: null};
|
||||
let view = 'queue';
|
||||
let streamSeen = false; // podcastStreamSeen() sent for this visit (until the app was away 10 min)
|
||||
let lastView = '';
|
||||
let hiddenAt = 0;
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) { hiddenAt = Date.now(); return; }
|
||||
if (hiddenAt && Date.now() - hiddenAt > 10 * 60000) { streamSeen = false; lastView = ''; if (view === 'stream') render(); }
|
||||
});
|
||||
let feedView = null; // {feed, episodes} while one podcast is open
|
||||
let searchResults = null;
|
||||
let preview = null; // {id, url, data | choices | error} while a feed is looked at before subscribing
|
||||
|
|
@ -95,6 +111,18 @@
|
|||
|
||||
const queue = () => state.queue || [];
|
||||
const inbox = () => state.inbox || [];
|
||||
const later = () => state.later || [];
|
||||
const v2 = () => !!state.lists_v2;
|
||||
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 === 'inbox' || view === 'episodes')) view = 'stream';
|
||||
if (!v2() && view === 'stream') view = 'inbox';
|
||||
if (!v2() && view === 'later') view = 'queue';
|
||||
}
|
||||
// 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);
|
||||
// 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() {
|
||||
|
|
@ -389,13 +417,48 @@
|
|||
function play(e) {
|
||||
const r = remote();
|
||||
if (r ? r.kind === 'podcast' && r.episodeId === e.id : currentId() === e.id) { openPlayer(); 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);
|
||||
else N.podcastEnqueue(JSON.stringify([e.raw]), 'front', currentId());
|
||||
}
|
||||
N.podcastPlayEpisode(JSON.stringify(e.raw), currentId());
|
||||
}
|
||||
|
||||
function enqueue(e, where) {
|
||||
N.podcastEnqueue(JSON.stringify([e.raw]), where || '', currentId());
|
||||
const label = POSITIONS.find(p => p.value === (where || settings().enqueue_position));
|
||||
toast('In der Warteschlange' + (label ? ': ' + label.label.toLowerCase() : ''));
|
||||
toast('In der ' + QNAME() + (label ? ': ' + label.label.toLowerCase() : ''));
|
||||
}
|
||||
|
||||
// +: onto the Merkliste, or off it again.
|
||||
function toggleLater(e) {
|
||||
if (!N.podcastLaterAdd) return;
|
||||
if (inLater(e)) {
|
||||
N.podcastLaterRemove(JSON.stringify([e.id]));
|
||||
if (e.raw) e.raw.in_later = false;
|
||||
toast('Aus der Merkliste genommen');
|
||||
} else {
|
||||
N.podcastLaterAdd(JSON.stringify([e.raw]));
|
||||
if (e.raw) e.raw.in_later = true;
|
||||
toast('Auf der Merkliste');
|
||||
}
|
||||
for (const l of [epList && epList.items, feedView && feedView.episodes]) {
|
||||
for (const o of l || []) if (o.id === e.id) o.in_later = e.raw.in_later;
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function laterButton(e) {
|
||||
const on = inLater(e);
|
||||
return button(on ? '✓' : '+', on ? 'Auf der Merkliste – antippen nimmt es heraus' : 'Auf die Merkliste', () => toggleLater(e),
|
||||
'nui-pod-mini' + (on ? ' on' : ''));
|
||||
}
|
||||
|
||||
function starButton(e) {
|
||||
const on = !!(e.raw && e.raw.favorite);
|
||||
return button(on ? '★' : '☆', on ? 'Favorit' : 'Als Favorit markieren', () => toggleFavorite(e, star => { e.raw.favorite = star; }),
|
||||
'nui-pod-mini nui-pod-starbtn' + (on ? ' on' : ''));
|
||||
}
|
||||
|
||||
// Move within the queue: "next" is right after the episode that plays.
|
||||
|
|
@ -420,13 +483,15 @@
|
|||
|
||||
// „Folge löschen“: out of the queue and „Neu“, its download goes here and on every other
|
||||
// device (the server tells them), but not played: it keeps its place and stays in „Folgen“.
|
||||
const DISCARD_HINT = 'aus Warteschlange und Neu, Datei auf allen Geräten löschen – nicht als gehört';
|
||||
const DISCARD_HINT_OLD = 'aus Warteschlange und Neu, Datei auf allen Geräten löschen – nicht als gehört';
|
||||
const DISCARD_HINT_V2 = 'aus Wiedergabeliste und Merkliste, Datei auf allen Geräten löschen – nicht als gehört';
|
||||
const discardHint = () => (v2() ? DISCARD_HINT_V2 : DISCARD_HINT_OLD);
|
||||
const discardItem = (e, opts) => N.podcastDiscard &&
|
||||
{icon: '⊠', svg: 'trash', label: 'Folge löschen', hint: DISCARD_HINT, action: () => discardOne(e, (opts && opts.list) || '', (opts && opts.filters) || [])};
|
||||
{icon: '⊠', svg: 'trash', label: 'Folge löschen', hint: discardHint(), action: () => discardOne(e, (opts && opts.list) || '', (opts && opts.filters) || [])};
|
||||
|
||||
// One episode: no question, 4 s „Rückgängig“ like a swipe; sent only then. False when its row leaves the list.
|
||||
function discardOne(e, list, filters) {
|
||||
const hide = list === 'queue' || list === 'inbox' || list === 'downloads' ||
|
||||
const hide = list === 'queue' || list === 'inbox' || list === 'later' || list === 'downloads' ||
|
||||
filters.includes('downloaded') || filters.includes('queued');
|
||||
offerUndo(e, 'Folge gelöscht', {hide, svg: 'trash', run: () => {
|
||||
discardNow([e.id]);
|
||||
|
|
@ -445,7 +510,7 @@
|
|||
// Several: asked first.
|
||||
function discardMany(ids) {
|
||||
openSheet(`${nEpisodes(ids.length)} löschen? Nicht als gehört markiert.`, [
|
||||
{icon: '⊠', svg: 'trash', label: 'Löschen', hint: DISCARD_HINT, danger: true, action: () => {
|
||||
{icon: '⊠', svg: 'trash', label: 'Löschen', hint: discardHint(), danger: true, action: () => {
|
||||
discardNow(ids);
|
||||
toast(`${nEpisodes(ids.length)} gelöscht`);
|
||||
stopSelecting();
|
||||
|
|
@ -465,9 +530,10 @@
|
|||
!playing && {icon: '⤴', label: 'Als Nächstes', hint: 'direkt nach der laufenden Folge', action: () => moveTo(e, 'next')},
|
||||
{icon: '⇈', label: 'Ganz nach oben', action: () => moveTo(e, 'top')},
|
||||
{icon: '⇊', label: 'Ganz nach unten', action: () => moveTo(e, 'bottom')},
|
||||
{icon: '✓', label: 'Als gehört markieren', hint: 'verlässt die Warteschlange', action: () => markPlayed(e)},
|
||||
{icon: '✓', label: 'Als gehört markieren', hint: v2() ? 'verlässt beide Listen' : 'verlässt die Warteschlange', action: () => markPlayed(e)},
|
||||
...dlMenuItems(e),
|
||||
{icon: '✕', svg: 'listx', label: 'Aus der Warteschlange nehmen', hint: dlDone(e.id) ? 'Datei bleibt' : null,
|
||||
v2() && N.podcastLaterAdd && !inLater(e) && {icon: '+', label: 'Auf die Merkliste', hint: 'für später', action: () => toggleLater(e)},
|
||||
{icon: '✕', svg: 'listx', label: 'Aus der ' + QNAME() + ' nehmen', hint: dlDone(e.id) && !v2() ? 'Datei bleibt' : null,
|
||||
action: () => N.podcastDequeue(e.id)},
|
||||
discardItem(e, opts),
|
||||
selectItem(e, opts),
|
||||
|
|
@ -475,6 +541,7 @@
|
|||
}
|
||||
|
||||
function episodeMenu(e, opts) {
|
||||
if (v2()) { listsMenu(e, opts); return; }
|
||||
const queued = inQueue(e.id);
|
||||
openSheet(e.title, [
|
||||
{icon: '▶', label: 'Abspielen', hint: queued ? null : 'kommt dafür in die Warteschlange', action: () => play(e)},
|
||||
|
|
@ -495,6 +562,27 @@
|
|||
]);
|
||||
}
|
||||
|
||||
// An episode's menu with the two lists (Merkliste, Strom, a podcast's page).
|
||||
function listsMenu(e, opts) {
|
||||
const queued = inQueue(e.id);
|
||||
const kept = inLater(e);
|
||||
openSheet(e.title, [
|
||||
{icon: '▶', label: 'Abspielen', hint: queued ? null : 'kommt an den Anfang der Wiedergabeliste', action: () => play(e)},
|
||||
{icon: '☰', label: 'Folge ansehen', hint: 'Shownotes, Kapitel, alles zu dieser Folge', action: () => openEpisode(e, opts)},
|
||||
favoriteItem(e),
|
||||
N.podcastLaterAdd && {icon: kept ? '✕' : '+', svg: kept ? 'listx' : null, label: kept ? 'Aus der Merkliste nehmen' : 'Auf die Merkliste',
|
||||
hint: kept ? null : 'für später; wird geladen', action: () => toggleLater(e)},
|
||||
!queued && {icon: '⤴', label: 'Als Nächstes hören', hint: 'in die Wiedergabeliste nach der laufenden Folge', action: () => enqueue(e, 'after_current')},
|
||||
!queued && {icon: '⇊', label: 'Ans Ende der Wiedergabeliste', action: () => enqueue(e, 'back')},
|
||||
queued && {icon: '✕', svg: 'listx', label: 'Aus der Wiedergabeliste nehmen', action: () => N.podcastDequeue(e.id)},
|
||||
!e.played && {icon: '✓', label: 'Als gehört markieren', hint: kept || queued ? 'verlässt die Listen' : null, action: () => markPlayed(e)},
|
||||
e.played && {icon: '↺', label: 'Als ungehört markieren', action: () => N.podcastMarkPlayed(e.id, false)},
|
||||
...dlMenuItems(e),
|
||||
discardItem(e, opts),
|
||||
selectItem(e, opts),
|
||||
]);
|
||||
}
|
||||
|
||||
function settingsMenu() {
|
||||
const s = settings();
|
||||
const choose = (name, title) => openSheet(title, POSITIONS.map(p => ({
|
||||
|
|
@ -510,7 +598,10 @@
|
|||
openSheet('Podcasts', [
|
||||
{icon: '⟳', label: 'Alle Feeds jetzt abrufen', hint: 'der Server holt sie, hier kommt nur das Ergebnis an',
|
||||
action: () => refresh(true)},
|
||||
group('≡', 'Warteschlange', 'wohin neue Folgen kommen, gehörte entfernen', () => [
|
||||
'lists_v2' in state && N.podcastListsSwitch && {icon: '☷', label: 'Wiedergabeliste und Merkliste',
|
||||
hint: v2() ? 'an: kurze Wiedergabeliste, lange Merkliste, der Strom statt „Neu“' : 'aus: eine Warteschlange, „Neu“ und „Folgen“',
|
||||
action: () => listsSwitch(!v2())},
|
||||
group('≡', QNAME(), 'wohin neue Folgen kommen, gehörte entfernen', () => [
|
||||
{icon: '+', label: 'Neue Folgen von Hand einreihen', hint: label(s.enqueue_position),
|
||||
action: () => choose('enqueue_position', 'Von Hand eingereihte Folgen kommen …')},
|
||||
{icon: '↻', label: 'Automatisch eingereihte Folgen', hint: label(s.auto_queue_position),
|
||||
|
|
@ -561,6 +652,26 @@
|
|||
{value: 240, label: 'Alle 4 Stunden'},
|
||||
];
|
||||
|
||||
function listsSwitch(on) {
|
||||
N.podcastListsSwitch(!!on);
|
||||
try { localStorage.setItem('nui_pod_lists', '1'); } catch (e) {}
|
||||
state.lists_v2 = !!on; // until the state comes anew
|
||||
view = on ? 'later' : 'queue';
|
||||
toast(on ? 'Die Warteschlange ist jetzt deine Merkliste; die Wiedergabeliste beginnt leer'
|
||||
: 'Die Merkliste steht jetzt am Ende der Warteschlange');
|
||||
render();
|
||||
}
|
||||
|
||||
// The two lists come on by themselves once per device, when the server has them (the
|
||||
// plan of 2026-10-19, docs/podcasts.md); the settings switch turns them off again.
|
||||
function listsOnOnce() {
|
||||
if (!('lists_v2' in state) || state.lists_v2 || !N.podcastListsSwitch) return;
|
||||
let asked = false;
|
||||
try { asked = localStorage.getItem('nui_pod_lists') === '1'; } catch (e) {}
|
||||
if (asked) return;
|
||||
listsSwitch(true);
|
||||
}
|
||||
|
||||
function backgroundSyncItem() {
|
||||
if (!N.podcastBackgroundSync || !N.podcastSetBackgroundSync) return null;
|
||||
const now = N.podcastBackgroundSync();
|
||||
|
|
@ -988,7 +1099,9 @@
|
|||
none: 'Keine angefangenen Folgen.'},
|
||||
{id: 'favorites', label: 'Favoriten', test: o => !!o.favorite,
|
||||
none: 'Noch keine Favoriten. Im ⋯-Menü einer Folge markierst du sie mit ★.'},
|
||||
{id: 'queued', label: 'Warteschlange', test: o => inQueue(o.id), none: 'Keine davon steht in der Warteschlange.'},
|
||||
{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: '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.'},
|
||||
|
|
@ -1099,7 +1212,7 @@
|
|||
};
|
||||
chip('Alle', !active.length, () => onChange([]));
|
||||
for (const f of FILTERS) {
|
||||
if ((f.app && !DL) || (f.all && !allEpisodes)) continue;
|
||||
if ((f.app && !DL) || (f.all && !allEpisodes) || (f.v2 && !v2())) continue;
|
||||
const on = active.includes(f.id);
|
||||
chip(f.label, on, () => onChange(on ? active.filter(x => x !== f.id)
|
||||
: active.filter(x => !(EXCLUDES[f.id] || []).includes(x)).concat(f.id)));
|
||||
|
|
@ -1190,8 +1303,16 @@
|
|||
} else if (feedView) {
|
||||
renderFeed();
|
||||
} else {
|
||||
const tabs = el('div', 'nui-pod-tabs');
|
||||
for (const v of VIEWS) {
|
||||
fixView();
|
||||
if (view === 'stream' && lastView !== 'stream') epList = null; // fetched anew: what came since shows
|
||||
if (view === 'stream' && !streamSeen) {
|
||||
// The Strom looked at: what came since the last visit keeps its mark for this one.
|
||||
streamSeen = true;
|
||||
if (N.podcastStreamSeen) N.podcastStreamSeen();
|
||||
}
|
||||
lastView = view;
|
||||
const tabs = el('div', 'nui-pod-tabs' + (v2() ? ' nui-pod-tabs-v2' : ''));
|
||||
for (const v of views()) {
|
||||
const b = el('button', 'nui-pod-tab' + (v.id === view ? ' active' : ''));
|
||||
b.type = 'button';
|
||||
b.append(el('span', null, v.label));
|
||||
|
|
@ -1211,11 +1332,13 @@
|
|||
if (offer) root.append(offer);
|
||||
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 === 'episodes' && !selected) folderChips(podcastFeeds(), f => f.id);
|
||||
else if ((view === 'episodes' || view === 'stream') && !selected) folderChips(podcastFeeds(), f => f.id);
|
||||
if (view === 'queue') renderQueue();
|
||||
else if (view === 'inbox') renderInbox();
|
||||
else if (view === 'episodes') renderEpisodes();
|
||||
else if (view === 'later') renderLater();
|
||||
else if (view === 'episodes' || view === 'stream') renderEpisodes();
|
||||
else if (view === 'feeds') renderFeeds();
|
||||
else renderSearch();
|
||||
}
|
||||
|
|
@ -1318,7 +1441,15 @@
|
|||
}
|
||||
const how = {select: !opts.noSelect, list, filters: opts.filters || []};
|
||||
const menu = () => (list === 'queue' ? queueMenu(e, how) : episodeMenu(e, how));
|
||||
row.append(primaryButton(e, list), button('⋯', 'Mehr', menu, 'nui-pod-menu'));
|
||||
if (list === 'stream') {
|
||||
// The Strom: ★, + (Merkliste), ▶ (first in the Wiedergabeliste); the rest by a long press.
|
||||
if (opts.fresh) row.classList.add('nui-pod-fresh');
|
||||
row.append(...[starButton(e), N.podcastLaterAdd && laterButton(e), playButton(e)].filter(Boolean));
|
||||
row.querySelectorAll(':scope > .nui-pod-mini').forEach(b => b.addEventListener('click', ev => ev.stopPropagation()));
|
||||
} else {
|
||||
if (v2() && list === 'feed' && N.podcastLaterAdd) row.append(laterButton(e));
|
||||
row.append(primaryButton(e, list), button('⋯', 'Mehr', menu, 'nui-pod-menu'));
|
||||
}
|
||||
row.addEventListener('click', () => openEpisode(e, how));
|
||||
// Swipes: touch only, not while the queue is locked.
|
||||
const swipes = list && SWIPE_LISTS[list] && !(list === 'queue' && locked) ? list : '';
|
||||
|
|
@ -1329,20 +1460,24 @@
|
|||
// The one action at hand: the queue and the downloads play, „Neu“ queues,
|
||||
// the episode lists download (or queue, where nothing downloads).
|
||||
function dlPrimary(e, list) {
|
||||
// Two lists: downloads follow the lists; a podcast's page plays (and + keeps).
|
||||
if (v2() && list === 'feed') return false;
|
||||
return (list === 'episodes' || list === 'feed') && DL && !!(e.raw.audio_url || e.raw.episode__audio_url);
|
||||
}
|
||||
|
||||
function playButton(e) {
|
||||
const now = currentId() === e.id;
|
||||
const b = button(now && player.playing ? '❚❚' : '▶', now && player.playing ? 'Pause' : 'Abspielen',
|
||||
() => (currentId() === e.id ? N.podcastToggle() : play(e)), 'nui-pod-playbtn');
|
||||
b.dataset.play = e.id;
|
||||
return b;
|
||||
}
|
||||
|
||||
function primaryButton(e, list) {
|
||||
if (list === 'queue' || list === 'downloads') {
|
||||
const now = currentId() === e.id;
|
||||
const b = button(now && player.playing ? '❚❚' : '▶', now && player.playing ? 'Pause' : 'Abspielen',
|
||||
() => (currentId() === e.id ? N.podcastToggle() : play(e)), 'nui-pod-playbtn');
|
||||
b.dataset.play = e.id;
|
||||
return b;
|
||||
}
|
||||
if (list === 'queue' || list === 'downloads' || list === 'later' || (v2() && list === 'feed')) return playButton(e);
|
||||
if (dlPrimary(e, list)) return dlButton(e);
|
||||
const queued = inQueue(e.id) || (list === 'inbox' && e.inQueue);
|
||||
return queued ? button('✓', 'In der Warteschlange', () => toast('Schon in der Warteschlange'), 'nui-pod-queued')
|
||||
return queued ? button('✓', 'In der ' + QNAME(), () => toast('Schon in der ' + QNAME()), 'nui-pod-queued')
|
||||
: button('+', 'Einreihen', () => { enqueue(e); e.raw.in_queue = true; render(); });
|
||||
}
|
||||
|
||||
|
|
@ -1523,6 +1658,9 @@
|
|||
episodes: {label: 'Folgen', left: 'download', right: 'favorite'},
|
||||
feed: {label: 'Podcast-Seite', hint: 'die Folgen eines Abos', left: 'delete', right: 'download'},
|
||||
downloads: {label: 'Downloads', hint: 'Folgen mit dem Filter „Geladen“', left: 'delete', right: 'delete'},
|
||||
// The two lists (docs/podcasts.md).
|
||||
later: {label: 'Merkliste', v2: true, left: 'unlater', right: 'queue'},
|
||||
stream: {label: 'Strom', v2: true, left: 'favorite', right: 'later'},
|
||||
};
|
||||
const SWIPE_ACTIONS = [
|
||||
{id: 'none', label: 'Nichts', icon: ''},
|
||||
|
|
@ -1535,6 +1673,8 @@
|
|||
{id: 'played', label: 'Gehört / ungehört', hint: 'als gehört markieren', icon: '✓'},
|
||||
{id: 'favorite', label: 'Favorit an / aus', icon: '★'},
|
||||
{id: 'dismiss', label: 'Aus „Neu“ entfernen', icon: '⊘', svg: 'listx'},
|
||||
{id: 'later', label: 'Auf die Merkliste', icon: '+', host: 'podcastLaterAdd'},
|
||||
{id: 'unlater', label: 'Aus der Merkliste', icon: '✕', svg: 'listx', host: 'podcastLaterRemove'},
|
||||
{id: 'discard', label: 'Folge löschen', hint: 'aus Warteschlange und Neu, Datei auf allen Geräten weg – nicht gehört',
|
||||
icon: '⊠', svg: 'trash', host: 'podcastDiscard'},
|
||||
];
|
||||
|
|
@ -1576,6 +1716,8 @@
|
|||
case 'delete': return {title: 'Datei löschen', hint: id && deleteDequeues(id) ? 'auch aus Warteschlange' : 'nur auf diesem Gerät'};
|
||||
case 'dequeue': return {title: 'Aus Warteschlange', hint: DL ? 'Datei bleibt' : null};
|
||||
case 'dismiss': return {title: 'Aus Neu entfernen', hint: 'nicht gehört'};
|
||||
case 'later': return {title: 'Merkliste'};
|
||||
case 'unlater': return {title: 'Aus der Merkliste', hint: 'nicht gehört'};
|
||||
case 'queue': return {title: 'Einreihen'};
|
||||
case 'download': return {title: 'Laden'};
|
||||
case 'favorite': return {title: 'Favorit'};
|
||||
|
|
@ -1688,8 +1830,27 @@
|
|||
function swipeDone(e, list, act, filters) {
|
||||
const o = e.raw || {};
|
||||
const has = d => DL && downloads[e.id] && downloads[e.id].state === d;
|
||||
if (act === 'later') {
|
||||
if (inLater(e)) { toast('Schon auf der Merkliste'); return true; }
|
||||
offerUndo(e, 'Auf der Merkliste', {run: () => {
|
||||
N.podcastLaterAdd(JSON.stringify([o]));
|
||||
o.in_later = true;
|
||||
render();
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
if (act === 'unlater') {
|
||||
if (!inLater(e)) { toast('Nicht auf der Merkliste'); return true; }
|
||||
const hide = list === 'later';
|
||||
offerUndo(e, 'Aus der Merkliste', {hide, svg: 'listx', run: () => {
|
||||
N.podcastLaterRemove(JSON.stringify([e.id]));
|
||||
o.in_later = false;
|
||||
if (!hide) render();
|
||||
}});
|
||||
return !hide;
|
||||
}
|
||||
if (act === 'queue') {
|
||||
if (inQueue(e.id)) { toast('Schon in der Warteschlange'); return true; }
|
||||
if (inQueue(e.id)) { toast('Schon in der ' + QNAME()); return true; }
|
||||
// Queued episodes leave „Neu“ for good (AntennaPod's rule): sent only after the 4 s.
|
||||
const hide = list === 'inbox';
|
||||
offerUndo(e, 'Eingereiht', {hide, run: () => {
|
||||
|
|
@ -1729,7 +1890,7 @@
|
|||
if (act === 'played') {
|
||||
const played = !(o.played || e.played);
|
||||
const hide = played
|
||||
? list === 'inbox' || (list === 'queue' && devicePrefs().remove_played !== false) ||
|
||||
? list === 'inbox' || list === 'later' || (list === 'queue' && devicePrefs().remove_played !== false) ||
|
||||
filters.includes('unplayed') || filters.includes('in_progress')
|
||||
: filters.includes('played');
|
||||
offerUndo(e, played ? 'Als gehört markiert' : 'Als ungehört markiert', {hide, run: () => {
|
||||
|
|
@ -1761,18 +1922,19 @@
|
|||
function renderQueue() {
|
||||
const all = queue().map(ep);
|
||||
if (!all.length) {
|
||||
empty('Die Warteschlange ist leer. Unter „Neu“ oder bei einem Abo tippst du auf „+“, um Folgen einzureihen.');
|
||||
empty(v2() ? 'Die Wiedergabeliste ist leer. ▶ bei einer Folge – in der Merkliste, im Strom oder bei einem Abo – spielt sie und stellt sie hierher; ist die Liste durch, hört der Player auf.'
|
||||
: 'Die Warteschlange ist leer. Unter „Neu“ oder bei einem Abo tippst du auf „+“, um Folgen einzureihen.');
|
||||
return;
|
||||
}
|
||||
const items = all.filter(e => inFolder(e.feedId));
|
||||
if (!items.length) { empty(folderEmpty('in der Warteschlange')); return; }
|
||||
if (!items.length) { empty(folderEmpty('in der ' + QNAME())); return; }
|
||||
const total = items.reduce((n, e) => n + shown(Math.max(0, (e.duration || 0) - (e.pos || 0)), e.feedId), 0);
|
||||
if (selected) {
|
||||
root.append(selectionBar(items, [
|
||||
{label: 'Nach oben', run: ids => N.podcastReorder(JSON.stringify(ids.concat(queue().map(q => q.episode__id).filter(id => !ids.includes(id)))))},
|
||||
{label: 'Nach unten', run: ids => N.podcastReorder(JSON.stringify(queue().map(q => q.episode__id).filter(id => !ids.includes(id)).concat(ids)))},
|
||||
{label: 'Gehört', run: ids => { N.podcastMarkPlayedAll(JSON.stringify(ids), true); stopSelecting(); }},
|
||||
{label: 'Aus Warteschlange', svg: 'listx', run: ids => { N.podcastDequeueAll(JSON.stringify(ids)); stopSelecting(); }},
|
||||
{label: v2() ? 'Aus der Liste' : 'Aus Warteschlange', svg: 'listx', run: ids => { N.podcastDequeueAll(JSON.stringify(ids)); stopSelecting(); }},
|
||||
discardAction(),
|
||||
]));
|
||||
} else {
|
||||
|
|
@ -1805,6 +1967,74 @@
|
|||
root.append(list);
|
||||
}
|
||||
|
||||
// The Merkliste: by podcast, each one folds (this device remembers which are open); in a
|
||||
// podcast its newest episode first. Heard or „Folge löschen“ takes an episode off.
|
||||
let laterOpen = new Set();
|
||||
try { laterOpen = new Set(JSON.parse(localStorage.getItem('nui_pod_later_open') || '[]')); } catch (e) {}
|
||||
|
||||
function toggleLaterOpen(feedId) {
|
||||
if (laterOpen.has(feedId)) laterOpen.delete(feedId); else laterOpen.add(feedId);
|
||||
try { localStorage.setItem('nui_pod_later_open', JSON.stringify([...laterOpen])); } catch (e) {}
|
||||
render();
|
||||
}
|
||||
|
||||
function renderLater() {
|
||||
const all = later().map(ep);
|
||||
if (!all.length) {
|
||||
empty('Die Merkliste ist leer. Mit + im Strom oder bei einem Abo merkst du dir Folgen; neue Folgen deiner Stamm-Podcasts kommen von selbst hierher (und werden geladen).');
|
||||
return;
|
||||
}
|
||||
const items = all.filter(e => inFolder(e.feedId));
|
||||
if (!items.length) { empty(folderEmpty('auf der Merkliste')); return; }
|
||||
if (selected) {
|
||||
root.append(selectionBar(items, [
|
||||
{label: 'Abspielen', run: ids => {
|
||||
N.podcastEnqueue(JSON.stringify(items.filter(e => ids.includes(e.id)).map(e => e.raw)), 'front', currentId());
|
||||
toast(`${nEpisodes(ids.length)} vorn in der Wiedergabeliste`);
|
||||
stopSelecting();
|
||||
}},
|
||||
{label: 'In die Liste', run: ids => {
|
||||
N.podcastEnqueue(JSON.stringify(items.filter(e => ids.includes(e.id)).map(e => e.raw)), 'back', currentId());
|
||||
toast(`${nEpisodes(ids.length)} am Ende der Wiedergabeliste`);
|
||||
stopSelecting();
|
||||
}},
|
||||
{label: 'Gehört', run: ids => { N.podcastMarkPlayedAll(JSON.stringify(ids), true); stopSelecting(); }},
|
||||
{label: 'Herausnehmen', svg: 'listx', run: ids => { N.podcastLaterRemove(JSON.stringify(ids)); stopSelecting(); }},
|
||||
DL && {label: 'Laden', run: ids => downloadMany(items.filter(e => ids.includes(e.id)))},
|
||||
discardAction(),
|
||||
]));
|
||||
} else {
|
||||
const total = items.reduce((n, e) => n + shown(Math.max(0, (e.duration || 0) - (e.pos || 0)), e.feedId), 0);
|
||||
const head = el('div', 'nui-pod-head');
|
||||
head.append(el('div', 'nui-pod-summary', `${nEpisodes(items.length)} · ${minutes(total)}` +
|
||||
(podFolder != null && items.length < all.length ? ` · von ${all.length}` : '')),
|
||||
listTools([button('☐', 'Mehrere auswählen', () => startSelecting())]));
|
||||
root.append(head);
|
||||
}
|
||||
// By podcast, in the order of their newest episode (the list comes newest first).
|
||||
const groups = new Map();
|
||||
for (const e of items) {
|
||||
if (!groups.has(e.feedId)) groups.set(e.feedId, []);
|
||||
groups.get(e.feedId).push(e);
|
||||
}
|
||||
const list = el('div', 'nui-pod-list nui-pod-later');
|
||||
for (const [feedId, eps] of groups) {
|
||||
const open = !!selected || laterOpen.has(feedId) || groups.size === 1;
|
||||
const head = el('button', 'nui-pod-group' + (open ? ' open' : ''));
|
||||
head.type = 'button';
|
||||
head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
head.append(art(eps[0].feedArt, null, null, eps[0].feed));
|
||||
const text = el('span', 'nui-pod-group-text');
|
||||
text.append(el('span', 'nui-pod-group-title', eps[0].feed || ''),
|
||||
el('span', 'nui-pod-meta', open ? nEpisodes(eps.length) : `${nEpisodes(eps.length)} · ${eps[0].title}`));
|
||||
head.append(text, el('span', 'nui-pod-group-count', String(eps.length)), el('span', 'nui-pod-chev', open ? '▾' : '›'));
|
||||
head.addEventListener('click', () => { if (!selected) toggleLaterOpen(feedId); });
|
||||
list.append(head);
|
||||
if (open) for (const e of eps) list.append(episodeRow(e, {showDate: true, list: 'later'}));
|
||||
}
|
||||
root.append(list);
|
||||
}
|
||||
|
||||
function renderInbox() {
|
||||
const all = inbox().map(ep);
|
||||
if (!all.length) {
|
||||
|
|
@ -2091,7 +2321,10 @@
|
|||
row.addEventListener('click', () => toggleSelected(f.id));
|
||||
return row;
|
||||
}
|
||||
if (f.new_episodes === 'ignore') row.append(el('span', 'nui-pod-badge', 'still'));
|
||||
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'));
|
||||
} else if (f.new_episodes === 'ignore') row.append(el('span', 'nui-pod-badge', 'still'));
|
||||
else if (f.auto_queue || f.new_episodes === 'queue') row.append(el('span', 'nui-pod-badge', 'auto'));
|
||||
const n = counter().value(f);
|
||||
if (n) {
|
||||
|
|
@ -2220,6 +2453,20 @@
|
|||
}
|
||||
|
||||
const NEW_WHERE = {inbox: 'in „Neu“', queue: 'in die Warteschlange', ignore: 'nirgends'};
|
||||
// With the two lists: the three levels of a subscription (docs/podcasts.md).
|
||||
const LEVELS = [
|
||||
{value: 'queue', label: 'Stamm', hint: 'neue Folgen auf die Merkliste, geladen'},
|
||||
{value: 'play', label: 'Stamm, gleich in die Wiedergabeliste', hint: 'neue Folgen spielen als Nächstes, geladen'},
|
||||
{value: 'inbox', label: 'Strom', hint: 'neue Folgen im Strom – nichts abzuarbeiten, nichts geladen'},
|
||||
{value: 'ignore', label: 'Auf Abruf', hint: 'nicht im Strom; auf der Seite des Podcasts und in der Suche'},
|
||||
];
|
||||
const levelOf = f => LEVELS.find(l => l.value === (f.new_episodes || 'inbox')) || LEVELS[2];
|
||||
const REFRESH = [
|
||||
{value: '', label: 'Automatisch', hint: 'nach seinem Rhythmus: wer täglich kommt, stündlich; seltene täglich'},
|
||||
{value: 'hourly', label: 'Stündlich'},
|
||||
{value: 'daily', label: 'Täglich'},
|
||||
{value: 'weekly', label: 'Wöchentlich'},
|
||||
];
|
||||
|
||||
function filterWords(live) {
|
||||
return [live.include_words && `nur „${live.include_words}“`, live.exclude_words && `ohne „${live.exclude_words}“`,
|
||||
|
|
@ -2237,9 +2484,15 @@
|
|||
if ('skip_silence' in live && live.skip_silence != null) parts.push(live.skip_silence ? 'Stille überspringen' : 'Stille nie überspringen');
|
||||
const words = filterWords(live);
|
||||
if (words) parts.push(words);
|
||||
if (live.new_episodes === 'queue' || (!('new_episodes' in live) && live.auto_queue)) parts.push('neue Folgen → Warteschlange');
|
||||
else if (live.new_episodes === 'ignore') parts.push('neue Folgen: nirgends');
|
||||
if (live.auto_download && N.podcastDownloads) parts.push('Auto-Download');
|
||||
if (v2() && 'new_episodes' in live) {
|
||||
parts.push(levelOf(live).label);
|
||||
const r = REFRESH.find(x => x.value === (live.refresh_interval || ''));
|
||||
if (live.refresh_interval && r) parts.push('abrufen: ' + r.label.toLowerCase());
|
||||
} else {
|
||||
if (live.new_episodes === 'queue' || (!('new_episodes' in live) && live.auto_queue)) parts.push('neue Folgen → Warteschlange');
|
||||
else if (live.new_episodes === 'ignore') parts.push('neue Folgen: nirgends');
|
||||
if (live.auto_download && N.podcastDownloads) parts.push('Auto-Download');
|
||||
}
|
||||
if (live.notify) parts.push('Benachrichtigen');
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
|
@ -2253,9 +2506,25 @@
|
|||
const choose = (head, choices) => () => openSheet(head, choices);
|
||||
const items = [];
|
||||
const pos = (POSITIONS.find(p => p.value === settings().auto_queue_position) || POSITIONS[1]).label.toLowerCase();
|
||||
if (v2() && 'new_episodes' in live && N.podcastFeedSettings) {
|
||||
const lv = levelOf(live);
|
||||
items.push({icon: '◇', label: 'Stufe: ' + lv.label, hint: lv.hint, active: lv.value !== 'inbox',
|
||||
action: choose(`„${title}“ ist …`, LEVELS.map(l => ({
|
||||
icon: l.value === lv.value ? '●' : '○', label: l.label, hint: l.hint, active: l.value === lv.value,
|
||||
action: () => save({new_episodes: l.value}),
|
||||
})))});
|
||||
if ('refresh_interval' in live) {
|
||||
const r = REFRESH.find(x => x.value === (live.refresh_interval || '')) || REFRESH[0];
|
||||
items.push({icon: '⟳', label: 'Abrufen: ' + r.label, hint: r.hint || null, active: !!live.refresh_interval,
|
||||
action: choose('Wie oft der Server nachsieht', REFRESH.map(x => ({
|
||||
icon: x.value === r.value ? '●' : '○', label: x.label, hint: x.hint || null, active: x.value === r.value,
|
||||
action: () => save({refresh_interval: x.value}),
|
||||
})))});
|
||||
}
|
||||
}
|
||||
if ('new_episodes' in live && N.podcastFeedSettings) {
|
||||
const ne = live.new_episodes || 'inbox';
|
||||
items.push({icon: '◇', label: 'Neue Folgen', hint: NEW_WHERE[ne] || NEW_WHERE.inbox, active: ne !== 'inbox',
|
||||
if (!v2()) items.push({icon: '◇', label: 'Neue Folgen', hint: NEW_WHERE[ne] || NEW_WHERE.inbox, active: ne !== 'inbox',
|
||||
action: choose(`Neue Folgen von „${title}“ …`, [
|
||||
{icon: ne === 'inbox' ? '●' : '○', label: 'In „Neu“', hint: 'du entscheidest selbst', active: ne === 'inbox', action: () => save({new_episodes: 'inbox'})},
|
||||
{icon: ne === 'queue' ? '●' : '○', label: 'Gleich in die Warteschlange', hint: pos, active: ne === 'queue', action: () => save({new_episodes: 'queue'})},
|
||||
|
|
@ -2263,7 +2532,7 @@
|
|||
action: () => save({new_episodes: 'ignore'})},
|
||||
])});
|
||||
// AntennaPod's per-podcast auto-download: new episodes land on the phone by themselves.
|
||||
if ('auto_download' in live && N.podcastDownloads) {
|
||||
if ('auto_download' in live && N.podcastDownloads && !v2()) {
|
||||
items.push({icon: '↓', label: 'Automatisch herunterladen', hint: live.auto_download ? 'an: neue Folgen im WLAN, im Download-Limit' : 'aus',
|
||||
active: !!live.auto_download, action: () => save({auto_download: !live.auto_download})});
|
||||
}
|
||||
|
|
@ -2321,7 +2590,7 @@
|
|||
if ('kind' in live && N.podcastFeedSettings) {
|
||||
const book = isBook(live);
|
||||
items.push({icon: '▤', label: book ? 'Hörbuch (unter Hören → Hörbücher)' : 'Als Hörbuch',
|
||||
hint: book ? 'tippen: wieder als Podcast' : 'Folgen als Kapitel unter Hören → Hörbücher, nicht in „Neu“',
|
||||
hint: book ? 'tippen: wieder als Podcast' : 'Folgen als Kapitel unter Hören → Hörbücher, nicht ' + (v2() ? 'im Strom' : 'in „Neu“'),
|
||||
active: book, action: () => save({kind: book ? 'podcast' : 'audiobook'})});
|
||||
}
|
||||
if (hasFolders() && N.podcastFeedSettings) {
|
||||
|
|
@ -2595,7 +2864,7 @@
|
|||
let epList = null; // {key, items, total, loading, error}
|
||||
|
||||
// The folder too: "Folgen" with a folder chosen is all episodes of that category.
|
||||
const epKey = () => epFilters.slice().sort().join(',') + '|' + epSort + '|' + JSON.stringify(podFolder);
|
||||
const epKey = () => epFilters.slice().sort().join(',') + '|' + epSort + '|' + JSON.stringify(podFolder) + '|' + (view === 'stream' ? 's' : '');
|
||||
|
||||
function setEpisodeList(filters, sort) {
|
||||
// The history goes by the last listening; leaving it, by date again.
|
||||
|
|
@ -2620,8 +2889,10 @@
|
|||
else epList.loading = true;
|
||||
if (!quiet) render();
|
||||
const sorting = EP_SORTS.find(s => s.id === epSort) || EP_SORTS[0];
|
||||
// The Strom: every subscription but the „Auf Abruf“ ones (docs/podcasts.md).
|
||||
const base = view === 'stream' ? ['stream'] : [];
|
||||
const params = new URLSearchParams({
|
||||
filter: epFilters.filter(id => id !== 'downloaded').join(','), sort: sorting.server || sorting.id, limit: '100',
|
||||
filter: base.concat(epFilters.filter(id => id !== 'downloaded')).join(','), sort: sorting.server || sorting.id, limit: '100',
|
||||
offset: String(offset),
|
||||
});
|
||||
if (podFolder != null && hasFolders()) params.set('folder', podFolder);
|
||||
|
|
@ -2668,6 +2939,12 @@
|
|||
root.append(head);
|
||||
}
|
||||
const list = el('div', 'nui-pod-list');
|
||||
if (view === 'stream' && !selected && (epSort === 'new' || epSort === 'rare')) {
|
||||
streamRows(list, items);
|
||||
root.append(list);
|
||||
moreButton();
|
||||
return;
|
||||
}
|
||||
appendRows(list, items, epSort, e => {
|
||||
const o = e.raw;
|
||||
const row = episodeRow(e, {showFeed: true, showDate: epSort !== 'played', list: downloadsOnly(epFilters) ? 'downloads' : 'episodes',
|
||||
|
|
@ -2682,6 +2959,10 @@
|
|||
return row;
|
||||
});
|
||||
root.append(list);
|
||||
moreButton();
|
||||
}
|
||||
|
||||
function moreButton() {
|
||||
if (epList.items.length < epList.total) {
|
||||
const more = el('button', 'btn nui-pod-more', epList.loading ? 'Wird geladen …' : 'Weitere laden');
|
||||
more.type = 'button';
|
||||
|
|
@ -2691,6 +2972,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
// The Strom (docs/podcasts.md): by period; in one, the podcasts most listened to first
|
||||
// (heard_365: episodes heard in 12 months), every one heard at all before the others,
|
||||
// among those the rarely publishing first. Each podcast once per period: its newest
|
||||
// episode and „+n ›“ for the others. New since the last look: a small mark.
|
||||
const streamOpen = new Set();
|
||||
|
||||
function streamRank(feedId) {
|
||||
const f = feeds().find(x => x.id === feedId) || {};
|
||||
return [f.heard_365 || 0, typeof f.interval_days === 'number' ? f.interval_days : 0];
|
||||
}
|
||||
|
||||
function streamRows(list, items) {
|
||||
const before = Date.parse(state.stream_seen_before || '') || 0;
|
||||
const fresh = e => !!before && Date.parse(e.raw.discovered_at || '') > before;
|
||||
const rank = new Map();
|
||||
const rankOf = id => { if (!rank.has(id)) rank.set(id, streamRank(id)); return rank.get(id); };
|
||||
for (const sec of timeSections(items, false)) {
|
||||
list.append(el('div', 'nui-pod-when', sec.label));
|
||||
const groups = new Map();
|
||||
sec.items.forEach((e, i) => {
|
||||
if (!groups.has(e.feedId)) groups.set(e.feedId, {first: i, eps: []});
|
||||
groups.get(e.feedId).eps.push(e);
|
||||
});
|
||||
const ordered = [...groups.entries()].sort((a, b) => {
|
||||
const [ha, ia] = rankOf(a[0]);
|
||||
const [hb, ib] = rankOf(b[0]);
|
||||
if (ha !== hb) return hb - ha;
|
||||
if (!ha && ia !== ib) return ib - ia;
|
||||
return a[1].first - b[1].first;
|
||||
});
|
||||
for (const [feedId, g] of ordered) {
|
||||
const key = sec.label + '|' + feedId;
|
||||
const open = streamOpen.has(key);
|
||||
const shownEps = open ? g.eps : g.eps.slice(0, 1);
|
||||
shownEps.forEach((e, i) => {
|
||||
const row = episodeRow(e, {showFeed: true, showDate: true, list: 'stream', fresh: fresh(e), filters: epFilters});
|
||||
if (i > 0) row.classList.add('nui-pod-bundled');
|
||||
list.append(row);
|
||||
});
|
||||
if (g.eps.length > 1) {
|
||||
const rest = g.eps.length - 1;
|
||||
const freshRest = !open && g.eps.slice(1).some(fresh);
|
||||
const b = el('button', 'nui-pod-bundle' + (freshRest ? ' nui-pod-fresh' : ''),
|
||||
open ? 'weniger ‹' : `+${rest} ${rest === 1 ? 'weitere' : 'weitere'} von ${g.eps[0].feed || 'diesem Podcast'} ›`);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', () => {
|
||||
if (open) streamOpen.delete(key); else streamOpen.add(key);
|
||||
render();
|
||||
});
|
||||
list.append(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- favourites -------------------------------------------------------------------------
|
||||
|
||||
function favoriteItem(e) {
|
||||
|
|
@ -4876,8 +5212,10 @@
|
|||
window.__dioraPodcasts = function (json) {
|
||||
state = typeof json === 'string' ? parse(json, state) : json;
|
||||
moveFeedListsToServer();
|
||||
listsOnOnce();
|
||||
render();
|
||||
};
|
||||
setTimeout(listsOnOnce, 2000);
|
||||
|
||||
// The app won't stream this episode over mobile data without a yes ("Nachfragen", RadioService.mayStream).
|
||||
window.__dioraAskStream = function (id, title) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copied from diora-android bd98557 2026-10-19: the podcast parts of web-native/static/native.css
|
||||
/* Copied from diora-android ef914bd 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,22 @@ 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; }
|
||||
/* 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; }
|
||||
.nui-pod-group .nui-pod-art, .nui-pod-group img { width: 40px; height: 40px; flex: none; }
|
||||
.nui-pod-group-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
|
||||
.nui-pod-group-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nui-pod-group .nui-pod-meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nui-pod-group-count { font-size: 12px; color: var(--nui-muted); flex: none; }
|
||||
.nui-pod-chev { flex: none; width: 18px; text-align: center; color: var(--nui-muted); }
|
||||
.nui-pod-later .nui-pod-row { padding-left: 14px; }
|
||||
.nui-pod-mini { min-width: 34px; padding: 0 4px; font-size: 17px; color: var(--nui-muted); flex: none; }
|
||||
.nui-pod-mini.on { color: var(--accent, #e63946); }
|
||||
.nui-pod-fresh .nui-pod-title::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #e63946); margin-right: 6px; vertical-align: middle; }
|
||||
.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; }
|
||||
.nui-pod-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue