diff --git a/static/js/sw.js b/static/js/sw.js index ebee697..1160419 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v62'; +const CACHE = 'diora-v63'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 52a5cf1..d76d0cf 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android 99528f2 2026-10-02: web-native/static/native-podcast.js +// Copied from diora-android 3d1281c 2026-10-02: 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. @@ -47,6 +47,8 @@ let view = 'queue'; 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 + let previewSeq = 0; let refreshing = false; let selected = null; // Set of episode ids while choosing several (queue or inbox) let locked = false; // queue locked: no dragging @@ -256,12 +258,13 @@ removePlayedItem(), ]), group('▶', 'Abspielen', ['Sprungweiten', 'skip_silence' in prefs && 'Stille', '„gehört“', - 'headset_next' in prefs && 'Kopfhörer'].filter(Boolean).join(', '), () => [ + 'headset_next' in prefs && 'Kopfhörer', 'notify_buttons' in prefs && 'Benachrichtigung'].filter(Boolean).join(', '), () => [ skipItem('skip_back', 'Zurückspringen', [5, 10, 15, 30]), skipItem('skip_forward', 'Vorspringen', [10, 15, 30, 45, 60]), silenceItem(), playedItem(), headsetItem(), + notifyItem(), ]), DL && {icon: '↓', label: 'Downloads', hint: dlSummary(), action: openDownloads}, DL && group('⚙︎', 'Download-Einstellungen', 'automatisch, gleichzeitig, Limit, aufräumen', () => [ @@ -310,6 +313,29 @@ ])}; } + // The notification's two extra buttons (app only: NotifyButtons.kt), like AntennaPod's. + const NOTIFY = [ + {key: 'next', label: 'Folge überspringen', hint: 'zählt als gehört, die nächste spielt'}, + {key: 'chapter', label: 'Nächstes Kapitel', hint: 'wenn die Folge Kapitel hat'}, + {key: 'speed', label: 'Tempo', hint: '1× · 1,2× · 1,5× · 1,75× · 2×'}, + {key: 'sleep', label: 'Sleep-Timer', hint: '15 Minuten an / aus'}, + ]; + function notifyItem() { + if (!N.podcastSetNotifyButtons || !Array.isArray(devicePrefs().notify_buttons)) return null; + const now = devicePrefs().notify_buttons; + const names = keys => keys.map(k => (NOTIFY.find(n => n.key === k) || {}).label).filter(Boolean).join(', ') || 'keine'; + const sheet = chosen => openSheet('Zwei Knöpfe in der Benachrichtigung', NOTIFY.map(n => ({ + icon: chosen.includes(n.key) ? '☑' : '☐', label: n.label, hint: n.hint, + // A third one replaces the one chosen first. + action: () => { + const next = chosen.includes(n.key) ? chosen.filter(k => k !== n.key) : chosen.concat(n.key).slice(-2); + N.podcastSetNotifyButtons(next.join(',')); + toast('Benachrichtigung: ' + names(next)); + }, + }))); + return {icon: '⊞', label: 'Knöpfe in der Benachrichtigung', hint: names(now), action: () => sheet(now)}; + } + function silenceItem() { if (!N.podcastSetPref || !('skip_silence' in devicePrefs())) return null; const on = !!devicePrefs().skip_silence; @@ -659,7 +685,9 @@ button('⋮', 'Einstellungen', settingsMenu)].filter(Boolean)); root.append(bar); - if (feedView) { + if (preview) { + renderPreview(); + } else if (feedView) { renderFeed(); } else { const tabs = el('div', 'nui-pod-tabs'); @@ -1700,7 +1728,7 @@ function search(q) { if (!q) return; - if (/^https?:\/\//i.test(q)) { subscribe(q, q); return; } + if (/^https?:\/\//i.test(q)) { if (N.podcastPreview) openPreview(q); else subscribe(q, q); return; } searchDraft = null; const test = matcher(q); const s = searchResults = { @@ -1759,11 +1787,115 @@ if (!r.ok || !data.ok) throw new Error(data.error || r.status); toast(`„${data.title || title}“ abonniert`); refresh(false); + return true; } catch (e) { toast(navigator.onLine ? 'Dieser Feed lässt sich nicht abonnieren.' : 'Abonnieren braucht eine Verbindung.'); + return false; } } + // A feed from another app (shared link, pcast:// …) or a pasted address: + // the app fetches it (N.podcastPreview, FeedPreview.kt) and the page shows + // title, cover, description and the newest episodes before subscribing. + function openPreview(url) { + if (!N.podcastPreview) { subscribe(url, url); return; } + closePlayer(); + feedView = null; + selected = null; + preview = {id: ++previewSeq, url}; + if (typeof showTab === 'function') showTab('podcasts'); + render(); + scrollTop(0); + N.podcastPreview(preview.id, url); + } + + function closePreview() { + if (!preview) return false; + preview = null; + render(); + return true; + } + + function renderPreview() { + const p = preview; + const d = p.data || {}; + const head = el('div', 'nui-pod-feedhead'); + head.append(button('‹', 'Zurück', closePreview), art(d.artwork, 'big')); + const info = el('div', 'nui-pod-main'); + info.append(el('div', 'nui-pod-title', plain(d.title) || 'Podcast'), + el('div', 'nui-pod-meta', plain(d.author) || p.url)); + head.append(info); + root.append(head); + if (p.choices) { + root.append(el('div', 'nui-pod-empty', 'Diese Seite verlinkt mehrere Feeds. Welcher ist es?')); + const list = el('div', 'nui-pod-list'); + for (const c of p.choices) { + const row = el('div', 'nui-pod-row'); + const main = el('div', 'nui-pod-main'); + main.append(el('div', 'nui-pod-title', plain(c.title) || c.url), el('div', 'nui-pod-meta', c.url)); + row.append(main); + row.addEventListener('click', () => openPreview(c.url)); + list.append(row); + } + root.append(list); + return; + } + if (p.error) { + // The server may still manage (it fetches the feed itself). + const box = el('div', 'nui-pod-offer', p.error); + const actions = el('div', 'nui-pod-offer-row'); + const anyway = el('button', 'btn', 'Trotzdem abonnieren'); + anyway.type = 'button'; + anyway.addEventListener('click', () => subscribeFromPreview(p.url, p.url, anyway)); + actions.append(anyway); + box.append(actions); + root.append(box); + return; + } + if (!p.data) { empty('Feed wird geladen …'); return; } + const known = feeds().find(f => f.rss_url === d.url || f.rss_url === p.url); + const actions = el('div', 'nui-pod-offer-row'); + const go = el('button', 'btn', known ? 'Abonniert – öffnen' : 'Abonnieren'); + go.type = 'button'; + go.addEventListener('click', () => { + if (known) { preview = null; openFeed(known); return; } + subscribeFromPreview(d.url, d.title, go); + }); + actions.append(go); + root.append(actions); + if (d.description) { + const desc = el('div', 'nui-pod-offer', plain(d.description)); + desc.style.whiteSpace = 'pre-line'; + root.append(desc); + } + const eps = d.episodes || []; + if (eps.length) { + const list = el('div', 'nui-pod-list'); + for (const e of eps) { + const row = el('div', 'nui-pod-row'); + row.append(art(e.artwork || d.artwork)); + const main = el('div', 'nui-pod-main'); + main.append(el('div', 'nui-pod-title', plain(e.title)), + el('div', 'nui-pod-meta', [e.duration && minutes(e.duration), day(e.date)].filter(Boolean).join(' · '))); + row.append(main); + list.append(row); + } + root.append(list); + } + } + + async function subscribeFromPreview(url, title, btn) { + btn.disabled = true; + const ok = await subscribe(url, title); + btn.disabled = false; + if (!ok) return; + preview = null; + view = 'feeds'; + try { localStorage.setItem('nui_pod_view', view); } catch (e) {} + render(); + scrollTop(0); + } + // ---- drag to reorder (touch) ----------------------------------------------------------- function dragSort(handle, row, list) { @@ -2950,6 +3082,20 @@ // ---- hooks from the app --------------------------------------------------------------- + // A podcast address from another app is waiting (MainActivity): show its preview. + window.__dioraSubscribe = function () { + const url = N.podcastTakeSubscribe ? N.podcastTakeSubscribe() : ''; + if (url) openPreview(url); + }; + + window.__dioraPreview = function (id, d) { + if (!preview || preview.id !== id) return; + if (d && d.ok) preview = {id, url: d.url, data: d}; + else if (d && d.choices && d.choices.length) preview = {id, url: preview.url, choices: d.choices}; + else preview = {id, url: preview.url, error: (d && d.error) || 'Dieser Feed ließ sich nicht laden.'}; + render(); + }; + window.__dioraPodcasts = function (json) { state = typeof json === 'string' ? parse(json, state) : json; render(); @@ -2996,6 +3142,7 @@ if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' && stopSelecting()) return true; if (back && back()) return true; if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts') { + if (closePreview()) return true; if (feedView) { feedView = null; render(); return true; } showTab('books'); return true; @@ -3005,5 +3152,6 @@ // Pull the latest state once per start; the server has done the fetching. N.podcastRefresh(false); + window.__dioraSubscribe(); }); })();