Podcast-Oberfläche aus diora-android 3d1281c (Listen + Abonnieren-Vorschau), sw v63
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69b22cf2e3
commit
d3e432e165
2 changed files with 153 additions and 5 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v62';
|
const CACHE = 'diora-v63';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
'/static/js/app.js',
|
'/static/js/app.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
|
// 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.
|
// and copy again. The browser host is static/js/podcasts-web.js.
|
||||||
|
|
||||||
|
|
@ -47,6 +47,8 @@
|
||||||
let view = 'queue';
|
let view = 'queue';
|
||||||
let feedView = null; // {feed, episodes} while one podcast is open
|
let feedView = null; // {feed, episodes} while one podcast is open
|
||||||
let searchResults = null;
|
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 refreshing = false;
|
||||||
let selected = null; // Set of episode ids while choosing several (queue or inbox)
|
let selected = null; // Set of episode ids while choosing several (queue or inbox)
|
||||||
let locked = false; // queue locked: no dragging
|
let locked = false; // queue locked: no dragging
|
||||||
|
|
@ -256,12 +258,13 @@
|
||||||
removePlayedItem(),
|
removePlayedItem(),
|
||||||
]),
|
]),
|
||||||
group('▶', 'Abspielen', ['Sprungweiten', 'skip_silence' in prefs && 'Stille', '„gehört“',
|
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_back', 'Zurückspringen', [5, 10, 15, 30]),
|
||||||
skipItem('skip_forward', 'Vorspringen', [10, 15, 30, 45, 60]),
|
skipItem('skip_forward', 'Vorspringen', [10, 15, 30, 45, 60]),
|
||||||
silenceItem(),
|
silenceItem(),
|
||||||
playedItem(),
|
playedItem(),
|
||||||
headsetItem(),
|
headsetItem(),
|
||||||
|
notifyItem(),
|
||||||
]),
|
]),
|
||||||
DL && {icon: '↓', label: 'Downloads', hint: dlSummary(), action: openDownloads},
|
DL && {icon: '↓', label: 'Downloads', hint: dlSummary(), action: openDownloads},
|
||||||
DL && group('⚙︎', 'Download-Einstellungen', 'automatisch, gleichzeitig, Limit, aufräumen', () => [
|
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() {
|
function silenceItem() {
|
||||||
if (!N.podcastSetPref || !('skip_silence' in devicePrefs())) return null;
|
if (!N.podcastSetPref || !('skip_silence' in devicePrefs())) return null;
|
||||||
const on = !!devicePrefs().skip_silence;
|
const on = !!devicePrefs().skip_silence;
|
||||||
|
|
@ -659,7 +685,9 @@
|
||||||
button('⋮', 'Einstellungen', settingsMenu)].filter(Boolean));
|
button('⋮', 'Einstellungen', settingsMenu)].filter(Boolean));
|
||||||
root.append(bar);
|
root.append(bar);
|
||||||
|
|
||||||
if (feedView) {
|
if (preview) {
|
||||||
|
renderPreview();
|
||||||
|
} else if (feedView) {
|
||||||
renderFeed();
|
renderFeed();
|
||||||
} else {
|
} else {
|
||||||
const tabs = el('div', 'nui-pod-tabs');
|
const tabs = el('div', 'nui-pod-tabs');
|
||||||
|
|
@ -1700,7 +1728,7 @@
|
||||||
|
|
||||||
function search(q) {
|
function search(q) {
|
||||||
if (!q) return;
|
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;
|
searchDraft = null;
|
||||||
const test = matcher(q);
|
const test = matcher(q);
|
||||||
const s = searchResults = {
|
const s = searchResults = {
|
||||||
|
|
@ -1759,11 +1787,115 @@
|
||||||
if (!r.ok || !data.ok) throw new Error(data.error || r.status);
|
if (!r.ok || !data.ok) throw new Error(data.error || r.status);
|
||||||
toast(`„${data.title || title}“ abonniert`);
|
toast(`„${data.title || title}“ abonniert`);
|
||||||
refresh(false);
|
refresh(false);
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(navigator.onLine ? 'Dieser Feed lässt sich nicht abonnieren.' : 'Abonnieren braucht eine Verbindung.');
|
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) -----------------------------------------------------------
|
// ---- drag to reorder (touch) -----------------------------------------------------------
|
||||||
|
|
||||||
function dragSort(handle, row, list) {
|
function dragSort(handle, row, list) {
|
||||||
|
|
@ -2950,6 +3082,20 @@
|
||||||
|
|
||||||
// ---- hooks from the app ---------------------------------------------------------------
|
// ---- 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) {
|
window.__dioraPodcasts = function (json) {
|
||||||
state = typeof json === 'string' ? parse(json, state) : json;
|
state = typeof json === 'string' ? parse(json, state) : json;
|
||||||
render();
|
render();
|
||||||
|
|
@ -2996,6 +3142,7 @@
|
||||||
if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' && stopSelecting()) return true;
|
if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' && stopSelecting()) return true;
|
||||||
if (back && back()) return true;
|
if (back && back()) return true;
|
||||||
if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts') {
|
if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts') {
|
||||||
|
if (closePreview()) return true;
|
||||||
if (feedView) { feedView = null; render(); return true; }
|
if (feedView) { feedView = null; render(); return true; }
|
||||||
showTab('books');
|
showTab('books');
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -3005,5 +3152,6 @@
|
||||||
|
|
||||||
// Pull the latest state once per start; the server has done the fetching.
|
// Pull the latest state once per start; the server has done the fetching.
|
||||||
N.podcastRefresh(false);
|
N.podcastRefresh(false);
|
||||||
|
window.__dioraSubscribe();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue