Podcast-Oberfläche aus diora-android 3d1281c (Listen + Abonnieren-Vorschau), sw v63
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 6m21s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-02 16:29:15 +02:00
parent 69b22cf2e3
commit d3e432e165
2 changed files with 153 additions and 5 deletions

View file

@ -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',

View file

@ -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();
});
})();