Compare commits
No commits in common. "649cf0f3407a8fad229994ba939ddbcb79e0786b" and "561776556f63c6581ee0f94eaa98136ad0fdfdf9" have entirely different histories.
649cf0f340
...
561776556f
6 changed files with 32 additions and 588 deletions
|
|
@ -260,10 +260,6 @@ class Stats:
|
|||
downloads_created: int = 0
|
||||
downloads_missing: int = 0
|
||||
downloads_offered: bool = True
|
||||
ignored_left_inbox: int = 0
|
||||
queue_removed: int = 0
|
||||
removed_titles: list = field(default_factory=list)
|
||||
diora_only_left_inbox: int = 0
|
||||
backup_date: datetime | None = None
|
||||
seconds: float = 0.0
|
||||
|
||||
|
|
@ -296,21 +292,13 @@ def _columns(db, table):
|
|||
|
||||
|
||||
class AntennaPodImport:
|
||||
def __init__(self, user, path, backup_date, global_auto_download=False, offer_downloads=True,
|
||||
quiet_diora_only=False, replace=False):
|
||||
def __init__(self, user, path, backup_date, global_auto_download=False, offer_downloads=True):
|
||||
self.user = user
|
||||
self.path = path
|
||||
self.backup_date = backup_date
|
||||
self.global_auto_download = global_auto_download
|
||||
# False: the app doesn't offer AntennaPod's downloads (they stay in AntennaPod).
|
||||
self.offer_downloads = offer_downloads
|
||||
# True: subscriptions only diora has keep their episodes, but the
|
||||
# ones out before the backup leave the inbox.
|
||||
self.quiet_diora_only = quiet_diora_only
|
||||
# A clean slate: AntennaPod's state is the new start. Podcasts only
|
||||
# diora has are unsubscribed, the queue is AntennaPod's alone, and
|
||||
# AntennaPod wins also over later changes in diora.
|
||||
self.replace = replace
|
||||
self.stats = Stats(backup_date=backup_date)
|
||||
|
||||
def run(self, dry_run=False):
|
||||
|
|
@ -344,8 +332,6 @@ class AntennaPodImport:
|
|||
self.new_feed = PodcastSettings.new_feed_defaults(user)
|
||||
|
||||
feed_map = self._import_feeds(db) # AntennaPod feed id -> diora feed id
|
||||
if self.replace:
|
||||
self._remove_diora_only(feed_map)
|
||||
queue_items = [r[0] for r in db.execute('SELECT feeditem FROM Queue ORDER BY id')]
|
||||
favorite_items = {r[0] for r in db.execute('SELECT feeditem FROM Favorites')}
|
||||
st.queue_backup = len(queue_items)
|
||||
|
|
@ -354,7 +340,6 @@ class AntennaPodImport:
|
|||
states, not_new, item_episode, downloads = self._import_episodes(
|
||||
db, feed_map, set(queue_items), favorite_items)
|
||||
self._import_progress(states, not_new)
|
||||
self._quiet_inbox(feed_map, states)
|
||||
self._import_queue(queue_items, item_episode)
|
||||
self._import_downloads(settings, downloads, item_episode)
|
||||
|
||||
|
|
@ -597,7 +582,7 @@ class AntennaPodImport:
|
|||
def diora_newer(p, activity):
|
||||
# Changed in diora after the backup was made (and after the
|
||||
# episode was last played in AntennaPod): diora's state stays.
|
||||
return not self.replace and p.updated_at > backup and (activity is None or p.updated_at > activity)
|
||||
return p.updated_at > backup and (activity is None or p.updated_at > activity)
|
||||
|
||||
new_rows, changed, favorite_only, dismiss_only = [], [], [], []
|
||||
for ep_id, s in states.items():
|
||||
|
|
@ -644,63 +629,6 @@ class AntennaPodImport:
|
|||
for ids in _chunks(dismiss_only):
|
||||
EpisodeProgress.objects.filter(pk__in=ids).update(dismissed=True)
|
||||
|
||||
def _quiet_inbox(self, feed_map, states):
|
||||
"""Episodes AntennaPod never showed as new, though diora has them in
|
||||
the inbox: those of podcasts whose new episodes are ignored (AntennaPod
|
||||
stopped fetching archived ones, so it never saw them), and with
|
||||
quiet_diora_only those of podcasts AntennaPod doesn't have. Only ones
|
||||
out before the backup; AntennaPod's own new ones stay."""
|
||||
st = self.stats
|
||||
user = self.user
|
||||
backup = self.backup_date
|
||||
imported = set(feed_map.values())
|
||||
feeds = set(PodcastFeed.objects.filter(pk__in=imported, new_episodes=PodcastFeed.NEW_IGNORE)
|
||||
.values_list('pk', flat=True))
|
||||
diora_only = set()
|
||||
if self.quiet_diora_only:
|
||||
diora_only = set(PodcastFeed.objects.filter(user=user).exclude(pk__in=imported).values_list('pk', flat=True))
|
||||
if not feeds and not diora_only:
|
||||
return
|
||||
keep = {ep_id for ep_id, s in states.items() if not s.dismissed}
|
||||
hidden = EpisodeProgress.objects.filter(user=user).filter(Q(played=True) | Q(dismissed=True))
|
||||
rows = (PodcastEpisode.objects.filter(feed_id__in=feeds | diora_only)
|
||||
.filter(Q(pub_date__lte=backup) | Q(pub_date__isnull=True))
|
||||
.exclude(id__in=hidden.values('episode_id'))
|
||||
.values_list('id', 'feed_id'))
|
||||
ids = []
|
||||
for ep_id, feed_id in rows:
|
||||
if ep_id in keep:
|
||||
continue
|
||||
ids.append(ep_id)
|
||||
if feed_id in feeds:
|
||||
st.ignored_left_inbox += 1
|
||||
else:
|
||||
st.diora_only_left_inbox += 1
|
||||
# Changed in diora after the backup: diora's state stays.
|
||||
newer = set()
|
||||
for chunk in ([] if self.replace else _chunks(ids)):
|
||||
newer.update(EpisodeProgress.objects.filter(user=user, episode_id__in=chunk, updated_at__gt=backup)
|
||||
.values_list('episode_id', flat=True))
|
||||
ids = [i for i in ids if i not in newer]
|
||||
for chunk in _chunks(ids):
|
||||
EpisodeProgress.objects.filter(user=user, episode_id__in=chunk, dismissed=False).update(dismissed=True)
|
||||
have = set(EpisodeProgress.objects.filter(user=user, episode_id__in=chunk)
|
||||
.values_list('episode_id', flat=True))
|
||||
with _given_updated_at():
|
||||
EpisodeProgress.objects.bulk_create(
|
||||
[EpisodeProgress(user=user, episode_id=i, dismissed=True, updated_at=backup)
|
||||
for i in chunk if i not in have],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
def _remove_diora_only(self, feed_map):
|
||||
"""Unsubscribe the podcasts the backup doesn't have (their episodes,
|
||||
states and queue entries go with them)."""
|
||||
st = self.stats
|
||||
gone = PodcastFeed.objects.filter(user=self.user).exclude(pk__in=set(feed_map.values()))
|
||||
st.removed_titles = sorted(gone.values_list('title', flat=True))
|
||||
gone.delete()
|
||||
|
||||
# -- queue --------------------------------------------------------------
|
||||
|
||||
def _import_queue(self, queue_items, item_episode):
|
||||
|
|
@ -719,12 +647,6 @@ class AntennaPodImport:
|
|||
return
|
||||
in_front = set(front)
|
||||
rest = [ep_id for ep_id in before if ep_id not in in_front]
|
||||
if self.replace and rest:
|
||||
for chunk in _chunks(rest):
|
||||
PodcastQueue.objects.filter(user=user, episode_id__in=chunk).delete()
|
||||
st.queue_removed = len(rest)
|
||||
before = [ep_id for ep_id in before if ep_id in in_front]
|
||||
rest = []
|
||||
st.queue_appended = len(rest)
|
||||
present = set(before)
|
||||
PodcastQueue.objects.bulk_create(
|
||||
|
|
@ -786,8 +708,7 @@ def summary_lines(st, username, path, dry_run, global_auto_download):
|
|||
+ (' -- DRY RUN, nothing was written' if dry_run else ''),
|
||||
f'Feeds: {st.backup_feeds} in backup, {subscribed} subscribed ({st.not_subscribed} not subscribed '
|
||||
f'ignored); {st.matched} matched, {st.created} created, {len(st.skipped_feeds)} skipped (not http/https), '
|
||||
f'{st.duplicates} duplicates'
|
||||
+ (f', {len(st.removed_titles)} only in diora unsubscribed (--replace)' if st.removed_titles else ''),
|
||||
f'{st.duplicates} duplicates',
|
||||
f'Settings: changed: {settings}',
|
||||
f' {st.not_kept_updated} feeds not kept updated in AntennaPod -> new episodes "ignore"',
|
||||
f'Auto-download: on for {st.auto_download_on} feeds ({st.auto_download_explicit} on in AntennaPod, '
|
||||
|
|
@ -802,20 +723,15 @@ def summary_lines(st, username, path, dry_run, global_auto_download):
|
|||
f'{st.left_inbox} taken out of the inbox (not new in AntennaPod)',
|
||||
f'Favourites: {st.favorites_backup} in backup; {st.favorites_before} before, {st.favorites_after} after',
|
||||
f'Queue: {st.queue_backup} in backup ({st.queue_missing} not found); {st.queue_before} before, '
|
||||
f'{st.queue_after} after ({st.queue_appended} diora entries after AntennaPod\'s'
|
||||
+ (f', {st.queue_removed} removed (--replace)' if st.queue_removed else '') + ')'
|
||||
f'{st.queue_after} after ({st.queue_appended} diora entries after AntennaPod\'s)'
|
||||
+ (f'; kept sorted by "{st.keep_sorted}"' if st.keep_sorted else ''),
|
||||
f'Inbox: {st.inbox_before} before, {st.inbox_after} after; out of it: {st.ignored_left_inbox} of podcasts '
|
||||
f'whose new episodes are ignored (AntennaPod never showed them)'
|
||||
+ (f', {st.diora_only_left_inbox} of podcasts only diora has' if st.diora_only_left_inbox else ''),
|
||||
f'Inbox: {st.inbox_before} before, {st.inbox_after} after',
|
||||
f'Downloads: {st.downloads} on the phone in AntennaPod ({st.downloads_bytes / 1_000_000:.0f} MB) '
|
||||
+ ('-> offered to the app for download' if st.downloads_offered else '-> not offered (--no-downloads)')
|
||||
+ (f' ({st.downloads_created} episodes created for them)' if st.downloads_created else '')
|
||||
+ (f'; {st.downloads_missing} not found (feeds not imported, no audio URL)' if st.downloads_missing else ''),
|
||||
f'Created feeds ({len(st.created_titles)}):',
|
||||
*(f' - {t}' for t in st.created_titles),
|
||||
*([f'Unsubscribed feeds ({len(st.removed_titles)}):'] if st.removed_titles else []),
|
||||
*(f' - {t}' for t in st.removed_titles),
|
||||
f'Skipped feeds ({len(st.skipped_feeds)}):',
|
||||
*(f' - {t} ({u})' for t, u in st.skipped_feeds),
|
||||
f'Done in {st.seconds:.1f} s.',
|
||||
|
|
|
|||
|
|
@ -25,12 +25,6 @@ class Command(BaseCommand):
|
|||
help='Do everything inside a transaction that is rolled back, print the summary')
|
||||
parser.add_argument('--global-auto-download', choices=('on', 'off'), default='off',
|
||||
help="Auto-download for feeds that use AntennaPod's global setting (default off)")
|
||||
parser.add_argument('--replace', action='store_true',
|
||||
help="A clean slate: unsubscribe podcasts the backup doesn't have, keep only "
|
||||
"AntennaPod's queue, AntennaPod wins over later changes in diora")
|
||||
parser.add_argument('--quiet-diora-only', action='store_true',
|
||||
help='Podcasts only diora has (not in the backup): their episodes out before the backup '
|
||||
'leave the inbox (the subscriptions stay)')
|
||||
parser.add_argument('--no-downloads', action='store_true',
|
||||
help="Don't offer AntennaPod's downloaded episodes to the app (they stay in AntennaPod)")
|
||||
parser.add_argument('--backup-date',
|
||||
|
|
@ -59,9 +53,7 @@ class Command(BaseCommand):
|
|||
global_auto_download = options['global_auto_download'] == 'on'
|
||||
try:
|
||||
stats = AntennaPodImport(user, path, backup_date, global_auto_download,
|
||||
offer_downloads=not options['no_downloads'],
|
||||
quiet_diora_only=options['quiet_diora_only'],
|
||||
replace=options['replace']).run(dry_run=options['dry_run'])
|
||||
offer_downloads=not options['no_downloads']).run(dry_run=options['dry_run'])
|
||||
except AntennaPodImportError as e:
|
||||
raise CommandError(str(e))
|
||||
for line in summary_lines(stats, user.username, path, options['dry_run'], global_auto_download):
|
||||
|
|
|
|||
|
|
@ -576,71 +576,6 @@ class ImportDownloadsViewTests(TestCase):
|
|||
self.assertEqual(len(PodcastSettings.objects.get(user=self.user).import_downloads), 4)
|
||||
|
||||
|
||||
class QuietInboxTests(ImportTestCase):
|
||||
"""What AntennaPod never showed as new leaves diora's inbox too."""
|
||||
|
||||
def inbox(self):
|
||||
hidden = EpisodeProgress.objects.filter(user=self.user, dismissed=True).values('episode_id')
|
||||
return sorted(PodcastEpisode.objects.filter(feed__user=self.user).exclude(id__in=hidden)
|
||||
.values_list('guid', flat=True))
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
archived = self.diora_feed('https://example.org/archived')
|
||||
self.ap_feed = self.ap.feed('https://example.org/archived', keep_updated=0)
|
||||
self.ap.item(self.ap_feed, 'ap-new', read=-1)
|
||||
self.episode(archived, 'ap-new', pub_date=T0)
|
||||
self.episode(archived, 'unknown-old', pub_date=T0 + timedelta(days=3)) # AntennaPod never fetched it
|
||||
self.episode(archived, 'unknown-late', pub_date=BACKUP_DATE + timedelta(days=1))
|
||||
self.episode(archived, 'undated')
|
||||
self.ap.feed('https://example.org/kept')
|
||||
kept = self.diora_feed('https://example.org/kept')
|
||||
self.episode(kept, 'kept-unknown', pub_date=T0) # not archived: stays
|
||||
only = self.diora_feed('https://example.org/only-diora')
|
||||
self.episode(only, 'only-old', pub_date=T0)
|
||||
self.episode(only, 'only-late', pub_date=BACKUP_DATE + timedelta(days=1))
|
||||
|
||||
def test_archived_podcasts(self):
|
||||
out = self.run_import()
|
||||
self.assertEqual(self.inbox(), ['ap-new', 'kept-unknown', 'only-late', 'only-old', 'unknown-late'])
|
||||
self.assertIn('out of it: 2 of podcasts whose new episodes are ignored (AntennaPod never showed them)\n', out)
|
||||
p = self.progress(PodcastEpisode.objects.get(guid='unknown-old'))
|
||||
self.assertEqual((p.played, p.dismissed, p.updated_at), (False, True, BACKUP_DATE))
|
||||
|
||||
def test_quiet_diora_only(self):
|
||||
out = self.run_import('--quiet-diora-only')
|
||||
self.assertEqual(self.inbox(), ['ap-new', 'kept-unknown', 'only-late', 'unknown-late'])
|
||||
self.assertIn(', 1 of podcasts only diora has', out)
|
||||
|
||||
def test_replace_is_a_clean_slate(self):
|
||||
archived = PodcastFeed.objects.get(rss_url='https://example.org/archived')
|
||||
ap_new = PodcastEpisode.objects.get(guid='ap-new')
|
||||
old = PodcastEpisode.objects.get(guid='unknown-old')
|
||||
late = PodcastEpisode.objects.get(guid='unknown-late')
|
||||
PodcastQueue.objects.create(user=self.user, episode=late, position=0)
|
||||
other = PodcastEpisode.objects.get(guid='kept-unknown')
|
||||
PodcastQueue.objects.create(user=self.user, episode=other, position=1)
|
||||
EpisodeProgress.objects.create(user=self.user, episode=old, position_seconds=30) # changed in diora now
|
||||
queued = self.ap.item(self.ap_feed, 'unknown-late', read=0, pub=None)
|
||||
self.ap.queue(queued)
|
||||
out = self.run_import('--replace')
|
||||
self.assertFalse(PodcastFeed.objects.filter(rss_url='https://example.org/only-diora').exists())
|
||||
self.assertFalse(PodcastEpisode.objects.filter(guid__startswith='only-').exists())
|
||||
self.assertEqual(self.queue(), [late.id])
|
||||
self.assertTrue(self.progress(old).dismissed) # AntennaPod wins
|
||||
self.assertIn('ap-new', self.inbox())
|
||||
self.assertTrue(archived.episodes.filter(id=ap_new.id).exists())
|
||||
self.assertIn(', 1 only in diora unsubscribed (--replace)', out)
|
||||
self.assertIn(', 1 removed (--replace))', out)
|
||||
self.assertIn('Unsubscribed feeds (1):\n - Diora\n', out)
|
||||
|
||||
def test_diora_newer_stays(self):
|
||||
ep = PodcastEpisode.objects.get(guid='unknown-old')
|
||||
EpisodeProgress.objects.create(user=self.user, episode=ep, position_seconds=30) # updated now
|
||||
self.run_import()
|
||||
self.assertIn('unknown-old', self.inbox())
|
||||
|
||||
|
||||
class AfterFirstRefreshTests(ImportTestCase):
|
||||
"""The way to move: import, let the new feeds be fetched, import again."""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v54';
|
||||
const CACHE = 'diora-v53';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app.js',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android 6c2b071 2026-10-02: web-native/static/native-podcast.js
|
||||
// Copied from diora-android f94c501 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.
|
||||
|
||||
|
|
@ -262,8 +262,7 @@
|
|||
playedItem(),
|
||||
headsetItem(),
|
||||
]),
|
||||
DL && {icon: '↓', label: 'Downloads', hint: dlSummary(), action: openDownloads},
|
||||
DL && group('⚙︎', 'Download-Einstellungen', 'automatisch, gleichzeitig, Limit, aufräumen', () => [
|
||||
DL && group('↓', 'Downloads', 'automatisch, Limit, aufräumen', () => [
|
||||
// Account-wide (server): new subscriptions start with auto-download on or off.
|
||||
'auto_download_default' in s && {icon: s.auto_download_default ? '☑' : '☐', label: 'Neue Abos laden von selbst',
|
||||
hint: s.auto_download_default ? 'an: neue Folgen neuer Abos werden geladen' : 'aus: pro Podcast einschaltbar',
|
||||
|
|
@ -478,8 +477,7 @@
|
|||
if (refreshing) text.append(el('span', 'muted nui-pod-status', 'Wird abgerufen …'));
|
||||
else if (state.server_time) text.append(el('span', 'muted nui-pod-status', 'Stand ' + day(state.server_time) + ', ' +
|
||||
new Date(state.server_time).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})));
|
||||
bar.append(...[text, dlIndicator(), button('⟳', 'Alle Feeds abrufen', () => refresh(true)),
|
||||
button('⋮', 'Einstellungen', settingsMenu)].filter(Boolean));
|
||||
bar.append(text, button('⟳', 'Alle Feeds abrufen', () => refresh(true)), button('⋮', 'Einstellungen', settingsMenu));
|
||||
root.append(bar);
|
||||
|
||||
if (feedView) {
|
||||
|
|
@ -502,8 +500,6 @@
|
|||
tabs.append(b);
|
||||
}
|
||||
root.append(tabs);
|
||||
const offer = importOffer();
|
||||
if (offer) root.append(offer);
|
||||
if (view === 'queue') renderQueue();
|
||||
else if (view === 'inbox') renderInbox();
|
||||
else if (view === 'episodes') renderEpisodes();
|
||||
|
|
@ -513,31 +509,6 @@
|
|||
scrollTop(scroll);
|
||||
}
|
||||
|
||||
// AntennaPod had episodes on the phone: offered once to fetch them again (Wi-Fi only).
|
||||
function importOffer() {
|
||||
const o = state.import_downloads;
|
||||
if (!o || !o.count || !N.podcastImportDownloads || !N.podcastDownloadSettings) return null;
|
||||
const box = el('div', 'nui-pod-offer');
|
||||
const s = parse(N.podcastDownloadSettings(), {});
|
||||
const needed = (Object.keys(downloads).length || 0) + o.count + (s.auto ? s.count || 0 : 0);
|
||||
const raise = s.limit > 0 && needed > s.limit
|
||||
? ` „Höchstens behalten“ geht dafür von ${s.limit} auf ${[5, 10, 25, 50, 100, 500].find(n => n >= needed) || 'unbegrenzt'}.` : '';
|
||||
box.append(el('div', null, `${o.count} ${o.count === 1 ? 'Folge war' : 'Folgen waren'} in AntennaPod heruntergeladen` +
|
||||
(o.bytes ? ` (${size(o.bytes)})` : '') + '. Hier noch einmal laden? Nur im WLAN; sie kommen nicht in die Warteschlange.' + raise));
|
||||
const row = el('div', 'nui-pod-offer-row');
|
||||
row.append(button('Laden', 'Im WLAN herunterladen', () => { N.podcastImportDownloads(true); box.remove(); }, 'btn'),
|
||||
button('Nein danke', 'Nicht laden', () => { N.podcastImportDownloads(false); box.remove(); }, 'btn'));
|
||||
box.append(row);
|
||||
return box;
|
||||
}
|
||||
|
||||
window.__dioraImportDownloads = function (r) {
|
||||
if (!r || !r.ok) toast('Die Liste aus AntennaPod kam nicht an — später noch einmal');
|
||||
else if (r.count) toast(`${r.count} ${r.count === 1 ? 'Folge wird' : 'Folgen werden'} im WLAN geladen` +
|
||||
(r.limit != null ? ` · Höchstens behalten: ${r.limit || 'unbegrenzt'}` : ''));
|
||||
render();
|
||||
};
|
||||
|
||||
function empty(text) {
|
||||
root.append(el('div', 'nui-pod-empty', text));
|
||||
}
|
||||
|
|
@ -1301,15 +1272,11 @@
|
|||
const text = el('button', 'nui-mini-text');
|
||||
text.type = 'button';
|
||||
text.append(el('span', 'nui-mini-station'), el('span', 'nui-mini-track'));
|
||||
text.addEventListener('click', () => (miniKind === 'radio' ? openRadioPlayer() : openPlayer()));
|
||||
text.addEventListener('click', openPlayer);
|
||||
mini.append(art('', 'mini'), text,
|
||||
button('↺', '15 s zurück', () => N.podcastSkip(false), 'nui-pod-skip'),
|
||||
button('☆', 'Sender speichern', () => window.__dioraRadioSave && window.__dioraRadioSave(), 'nui-mini-save'),
|
||||
button('▶', 'Abspielen', () => (miniKind === 'radio'
|
||||
? window.__dioraRadioToggle && window.__dioraRadioToggle() : N.podcastToggle()), 'nui-mini-toggle'));
|
||||
button('▶', 'Abspielen', () => N.podcastToggle(), 'nui-mini-toggle'));
|
||||
dock.prepend(mini);
|
||||
// One player for podcasts and radio: the radio's own bar steps aside.
|
||||
document.documentElement.classList.add('nui-one-player');
|
||||
|
||||
// A "Podcasts" button in the bottom bar, between books and radio.
|
||||
const tabbar = $id('nui-tabbar');
|
||||
|
|
@ -1323,49 +1290,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
// What the bar shows: 'podcast', 'radio' (a station is chosen) or ''.
|
||||
let miniKind = '';
|
||||
|
||||
function radioNow() {
|
||||
return typeof window.__dioraRadioNow === 'function' ? window.__dioraRadioNow() : null;
|
||||
}
|
||||
|
||||
function setArt(box, src) {
|
||||
const img = box.querySelector('img');
|
||||
src = (src || '').replace(/^http:/, 'https:');
|
||||
if (!img && src) {
|
||||
const i = el('img');
|
||||
i.alt = '';
|
||||
i.src = src;
|
||||
i.addEventListener('error', () => i.remove());
|
||||
box.append(i);
|
||||
} else if (img && img.getAttribute('src') !== src) {
|
||||
if (src) img.src = src; else img.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function renderMini() {
|
||||
const isPod = player.kind === 'podcast';
|
||||
document.documentElement.classList.toggle('nui-pod-playing', isPod);
|
||||
if (!mini) return;
|
||||
const radio = isPod ? null : radioNow();
|
||||
miniKind = isPod ? 'podcast' : radio ? 'radio' : '';
|
||||
mini.hidden = !miniKind;
|
||||
mini.classList.toggle('nui-mini-radio', miniKind === 'radio');
|
||||
document.documentElement.classList.toggle('nui-pod-mini-open', !!miniKind);
|
||||
mini.querySelector('.nui-pod-skip').hidden = miniKind !== 'podcast';
|
||||
mini.querySelector('.nui-mini-save').hidden = miniKind !== 'radio' || radio.saved;
|
||||
if (miniKind === 'radio') {
|
||||
mini.querySelector('.nui-mini-station').textContent = radio.name;
|
||||
mini.querySelector('.nui-mini-track').textContent = radio.buffering ? 'Verbinde …'
|
||||
: radio.playing ? (radio.track || '● Live') : 'Angehalten';
|
||||
const t = mini.querySelector('.nui-mini-toggle');
|
||||
t.textContent = radio.playing ? '■' : '▶';
|
||||
t.title = radio.playing ? 'Stopp' : 'Abspielen';
|
||||
setArt(mini.querySelector('.nui-pod-art'), radio.logo);
|
||||
mini.querySelector('.nui-pod-mini-bar span').style.width = radio.playing ? '100%' : '0';
|
||||
return;
|
||||
}
|
||||
mini.hidden = !isPod;
|
||||
document.documentElement.classList.toggle('nui-pod-mini-open', isPod);
|
||||
if (!isPod) return;
|
||||
mini.querySelector('.nui-mini-station').textContent = player.title || '';
|
||||
// The back button names the set skip length (checked now and then, it rarely changes).
|
||||
|
|
@ -1380,7 +1310,11 @@
|
|||
const t = mini.querySelector('.nui-mini-toggle');
|
||||
t.textContent = player.playing ? '❚❚' : '▶';
|
||||
t.title = player.playing ? 'Pause' : 'Abspielen';
|
||||
setArt(mini.querySelector('.nui-pod-art'), player.artwork);
|
||||
const a = mini.querySelector('.nui-pod-art img');
|
||||
const box = mini.querySelector('.nui-pod-art');
|
||||
const src = (player.artwork || '').replace(/^http:/, 'https:');
|
||||
if (!a && src) { const img = el('img'); img.alt = ''; img.src = src; box.append(img); }
|
||||
else if (a && a.getAttribute('src') !== src) { if (src) a.src = src; else a.remove(); }
|
||||
mini.querySelector('.nui-pod-mini-bar span').style.width = player.dur ? Math.min(100, player.pos / player.dur * 100) + '%' : '0';
|
||||
}
|
||||
|
||||
|
|
@ -1633,13 +1567,9 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// The full player: one for both. Which one it shows now.
|
||||
let sheetKind = '';
|
||||
|
||||
function openPlayer() {
|
||||
if (player.kind !== 'podcast') return;
|
||||
closePlayer();
|
||||
sheetKind = 'podcast';
|
||||
sheet = el('div');
|
||||
sheet.id = 'nui-pod-player';
|
||||
sheet.innerHTML = '<div class="nui-pod-player-panel" role="dialog" aria-label="Player"></div>';
|
||||
|
|
@ -1702,75 +1632,8 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// The radio in the same player: the station instead of the episode, "Live"
|
||||
// instead of the seek bar, save / stop / sleep timer as the buttons.
|
||||
function openRadioPlayer() {
|
||||
const r = radioNow();
|
||||
if (!r) return;
|
||||
closePlayer();
|
||||
sheetKind = 'radio';
|
||||
sheet = el('div');
|
||||
sheet.id = 'nui-pod-player';
|
||||
sheet.innerHTML = '<div class="nui-pod-player-panel" role="dialog" aria-label="Radio"></div>';
|
||||
const panel = sheet.firstChild;
|
||||
const top = el('div', 'nui-pod-player-top');
|
||||
top.append(button('⌄', 'Player schließen', closePlayer));
|
||||
const cover = art(r.logo, 'player');
|
||||
cover.classList.add('nui-radio-art');
|
||||
panel.append(top, cover, el('div', 'nui-pod-player-title'), el('div', 'nui-pod-player-feed'),
|
||||
el('div', 'nui-radio-live'));
|
||||
const controls = el('div', 'nui-pod-controls');
|
||||
controls.append(
|
||||
button('☆', 'Sender speichern', () => window.__dioraRadioSave && window.__dioraRadioSave(), 'nui-pod-fav nui-radio-save'),
|
||||
button('▶', 'Abspielen', () => window.__dioraRadioToggle && window.__dioraRadioToggle(), 'nui-pod-play'),
|
||||
button('☾', 'Sleep-Timer', () => sleepMenu(true), 'nui-pod-sleep'),
|
||||
);
|
||||
const extra = el('div', 'nui-pod-extra');
|
||||
const stations = el('button', 'btn nui-radio-stations', 'Sender wechseln');
|
||||
stations.type = 'button';
|
||||
stations.addEventListener('click', () => { closePlayer(); showTab('radio'); });
|
||||
extra.append(stations);
|
||||
panel.append(controls, extra);
|
||||
sheet.addEventListener('click', e => { if (e.target === sheet) closePlayer(); });
|
||||
document.body.append(sheet);
|
||||
renderRadioPlayer();
|
||||
}
|
||||
|
||||
function renderRadioPlayer() {
|
||||
const r = radioNow();
|
||||
if (!r) { closePlayer(); return; }
|
||||
sheet.querySelector('.nui-pod-player-title').textContent = r.name;
|
||||
sheet.querySelector('.nui-pod-player-feed').textContent = r.track || '';
|
||||
const live = sheet.querySelector('.nui-radio-live');
|
||||
live.textContent = r.buffering ? 'Verbinde …' : r.playing ? '● Live' : 'Angehalten';
|
||||
live.classList.toggle('on', r.playing && !r.buffering);
|
||||
const p = sheet.querySelector('.nui-pod-play');
|
||||
p.textContent = r.playing ? '■' : '▶';
|
||||
p.title = r.playing ? 'Stopp' : 'Abspielen';
|
||||
const s = sheet.querySelector('.nui-radio-save');
|
||||
s.textContent = r.saved ? '★' : '☆';
|
||||
s.classList.toggle('on', r.saved);
|
||||
s.disabled = r.saved;
|
||||
s.title = r.saved ? 'Gespeichert' : 'Sender speichern';
|
||||
const sl = sheet.querySelector('.nui-pod-sleep');
|
||||
sl.textContent = sleepLabel() || '☾';
|
||||
sl.classList.toggle('on', !!sleepLabel());
|
||||
setArt(sheet.querySelector('.nui-pod-art-player'), r.logo);
|
||||
}
|
||||
|
||||
// native-radio.js: the station, its track or its state changed.
|
||||
window.__dioraRadioChanged = function () {
|
||||
renderMini();
|
||||
if (sheet && sheetKind === 'radio') renderRadioPlayer();
|
||||
};
|
||||
window.__dioraOpenPlayer = kind => (kind === 'radio' ? openRadioPlayer() : openPlayer());
|
||||
|
||||
function renderPlayer() {
|
||||
if (!sheet) return;
|
||||
if (sheetKind === 'radio') {
|
||||
if (player.kind === 'podcast') closePlayer(); else renderRadioPlayer();
|
||||
return;
|
||||
}
|
||||
if (player.kind !== 'podcast') { closePlayer(); return; }
|
||||
if (sheetEpisode !== player.episodeId) {
|
||||
// The next episode started while the player was open.
|
||||
|
|
@ -1834,7 +1697,7 @@
|
|||
}
|
||||
|
||||
// Minutes count only while playing; shaking the phone starts them over.
|
||||
function sleepMenu(radio) {
|
||||
function sleepMenu() {
|
||||
if (!N.podcastSleep) return;
|
||||
const t = player.sleep || {};
|
||||
const on = !!sleepLabel();
|
||||
|
|
@ -1849,11 +1712,9 @@
|
|||
for (const m of [5, 10, 15, 30, 45, 60, 90]) {
|
||||
items.push({icon: '☾', label: `${m} Minuten`, hint: m === t.last && !on ? 'zuletzt' : null, action: () => N.podcastSleep(m)});
|
||||
}
|
||||
if (radio !== true) {
|
||||
items.push({icon: '⏹', label: 'Am Ende der Folge', action: () => N.podcastSleep(-1)});
|
||||
items.push({icon: '⏹', label: 'Nach 2 Folgen', action: () => N.podcastSleep(-2)});
|
||||
items.push({icon: '⏹', label: 'Nach 3 Folgen', action: () => N.podcastSleep(-3)});
|
||||
}
|
||||
if (N.podcastSleepAuto) {
|
||||
items.push({icon: t.auto ? '☑' : '☐', label: 'Nachts von selbst (22–6 Uhr)',
|
||||
hint: t.auto ? `an: ${t.last || 15} Minuten, wenn eine Folge startet` : 'aus',
|
||||
|
|
@ -1878,15 +1739,7 @@
|
|||
const DL = !!N.podcastDownloads;
|
||||
let downloads = DL ? parse(N.podcastDownloads(), {}) : {};
|
||||
let dlSettings = DL ? parse(N.podcastDownloadSettings(), {}) : {};
|
||||
// All at a glance (Downloads.infoJson): paused, how many run and wait, done of all in this round.
|
||||
let dlInfo = DL && N.podcastDownloadsInfo ? parse(N.podcastDownloadsInfo(), {}) : {};
|
||||
const DL_COUNTS = [1, 2, 3, 5, 10];
|
||||
const DL_PARALLEL = [1, 2, 3, 4];
|
||||
// Why a download waits (DownloadPlan.waitReason).
|
||||
const DL_REASONS = {
|
||||
paused: 'pausiert', wifi: 'wartet auf WLAN', offline: 'wartet auf eine Verbindung', queue: 'in der Reihe',
|
||||
retry: 'neuer Versuch',
|
||||
};
|
||||
// The limit and the clean-up the AntennaPod way (Downloads.kt, DownloadPlan.toClean).
|
||||
const DL_LIMITS = [5, 10, 25, 50, 100, 500, 0];
|
||||
const DL_CLEANUPS = [
|
||||
|
|
@ -1903,8 +1756,7 @@
|
|||
|
||||
function dlStart(e) {
|
||||
N.podcastDownload(JSON.stringify(e.raw));
|
||||
window.__dioraDownloads(Object.assign({}, downloads,
|
||||
{[e.id]: {state: 'queued', progress: 0, bytes: 0, total: 0, title: e.title, reason: 'queue'}}));
|
||||
window.__dioraDownloads(Object.assign({}, downloads, {[e.id]: {state: 'queued', progress: 0, bytes: 0, total: 0}}));
|
||||
toast('Wird heruntergeladen');
|
||||
}
|
||||
|
||||
|
|
@ -1937,15 +1789,7 @@
|
|||
const d = downloads[b.dataset.dl];
|
||||
if (!d || (d.state !== 'queued' && d.state !== 'downloading')) return;
|
||||
b.style.setProperty('--p', (d.progress || 0) + '%');
|
||||
b.firstChild.textContent = d.state === 'queued' ? (d.reason === 'paused' ? '‖' : '…') : d.progress + '';
|
||||
}
|
||||
|
||||
// "wartet auf WLAN", "neuer Versuch um 14:05" …
|
||||
function dlReason(d) {
|
||||
if (d.reason === 'retry' && d.retryAt) {
|
||||
return 'neuer Versuch um ' + new Date(d.retryAt).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'});
|
||||
}
|
||||
return DL_REASONS[d.reason] || 'wartet';
|
||||
b.firstChild.textContent = d.state === 'queued' ? '…' : d.progress + '';
|
||||
}
|
||||
|
||||
function dlMenuItems(e) {
|
||||
|
|
@ -1956,12 +1800,8 @@
|
|||
action: () => dlStart(e)}];
|
||||
}
|
||||
if (d.state === 'done') return [{icon: '✕', label: 'Download löschen', hint: size(d.bytes), action: () => dlDelete(e)}];
|
||||
return [
|
||||
d.state === 'queued' && d.order > 0 && N.podcastDownloadFirst &&
|
||||
{icon: '⇈', label: 'Zuerst laden', hint: 'vor die anderen wartenden', action: () => N.podcastDownloadFirst(e.id)},
|
||||
{icon: '✕', label: 'Download abbrechen', hint: d.state === 'queued' ? dlReason(d) : d.progress + ' %',
|
||||
action: () => dlDelete(e, true)},
|
||||
];
|
||||
return [{icon: '✕', label: 'Download abbrechen', hint: d.state === 'queued' ? 'wartet noch' : d.progress + ' %',
|
||||
action: () => dlDelete(e, true)}];
|
||||
}
|
||||
|
||||
function dlSet(changes) {
|
||||
|
|
@ -1983,10 +1823,6 @@
|
|||
action: () => dlSet({count: n})})))},
|
||||
s.auto && {icon: s.mobile ? '☑' : '☐', label: 'Auch über mobile Daten', hint: s.mobile ? 'nicht nur im WLAN' : 'sonst nur im WLAN',
|
||||
action: () => dlSet({mobile: !s.mobile})},
|
||||
s.parallel != null && {icon: '⇉', label: 'Gleichzeitig laden', hint: s.parallel === 1 ? '1 Folge' : s.parallel + ' Folgen',
|
||||
action: () => openSheet('Gleichzeitig laden', DL_PARALLEL.map(n => ({
|
||||
icon: s.parallel === n ? '●' : '○', label: n === 1 ? '1 Folge' : n + ' Folgen', active: s.parallel === n,
|
||||
hint: n === 3 ? 'Standard' : null, action: () => dlSet({parallel: n})})))},
|
||||
{icon: s.deletePlayed ? '☑' : '☐', label: 'Gehörte Folgen löschen',
|
||||
hint: 'sobald die Folge zu Ende ist – nicht bei Favoriten',
|
||||
action: () => dlSet({deletePlayed: !s.deletePlayed})},
|
||||
|
|
@ -2012,216 +1848,17 @@
|
|||
];
|
||||
}
|
||||
|
||||
window.__dioraDownloads = function (map, info) {
|
||||
window.__dioraDownloads = function (map) {
|
||||
const before = downloads;
|
||||
downloads = (typeof map === 'string' ? parse(map, downloads) : map) || {};
|
||||
if (info) dlInfo = typeof info === 'string' ? parse(info, dlInfo) : info;
|
||||
const keys = Object.keys(downloads);
|
||||
const same = keys.length === Object.keys(before).length &&
|
||||
keys.every(id => before[id] && before[id].state === downloads[id].state && before[id].reason === downloads[id].reason);
|
||||
keys.every(id => before[id] && before[id].state === downloads[id].state);
|
||||
// Only the numbers moved: turn the rings, no new list (keeps a drag or a scroll going).
|
||||
if (same) {
|
||||
document.querySelectorAll('#nui-pod [data-dl]').forEach(dlRing);
|
||||
dlIndicatorText();
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
renderDownloads();
|
||||
if (same) document.querySelectorAll('#nui-pod [data-dl]').forEach(dlRing);
|
||||
else render();
|
||||
};
|
||||
|
||||
function dlCounts() {
|
||||
const c = {running: 0, waiting: 0, failed: 0, done: 0, bytes: 0};
|
||||
for (const d of Object.values(downloads)) {
|
||||
if (d.state === 'downloading') c.running++;
|
||||
else if (d.state === 'queued') c.waiting++;
|
||||
else if (d.state === 'failed') c.failed++;
|
||||
else if (d.state === 'done') { c.done++; c.bytes += d.bytes || 0; }
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function dlSummary() {
|
||||
const c = dlCounts();
|
||||
const parts = [];
|
||||
if (c.running) parts.push(c.running + (c.running === 1 ? ' läuft' : ' laufen'));
|
||||
if (c.waiting) parts.push(c.waiting + (c.waiting === 1 ? ' wartet' : ' warten') + (dlInfo.paused ? ' (pausiert)' : ''));
|
||||
if (c.failed) parts.push(c.failed + ' fehlgeschlagen');
|
||||
if (c.done) parts.push(c.done + ' fertig');
|
||||
return parts.join(' · ') || 'keine';
|
||||
}
|
||||
|
||||
// The top bar's sign while downloads run or wait: "↓ 3/68" (the one loading of all in this round,
|
||||
// like the notification), "‖ 68" while paused. Opens the overview.
|
||||
function dlIndicator() {
|
||||
if (!DL || !N.podcastDownloadsInfo) return null;
|
||||
const b = button('', 'Downloads', openDownloads, 'nui-pod-dlind');
|
||||
b.id = 'nui-pod-dlind';
|
||||
dlIndicatorText(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
function dlIndicatorText(b) {
|
||||
b = b || $id('nui-pod-dlind');
|
||||
if (!b) return;
|
||||
const c = dlCounts();
|
||||
const busy = c.running + c.waiting;
|
||||
b.hidden = !busy;
|
||||
if (!busy) return;
|
||||
const total = Math.max(dlInfo.total || 0, busy);
|
||||
const done = Math.min(dlInfo.done || 0, total);
|
||||
b.textContent = dlInfo.paused ? '‖ ' + busy : c.running ? `↓ ${Math.min(done + 1, total)}/${total}` : '↓ ' + busy;
|
||||
b.title = 'Downloads: ' + dlSummary();
|
||||
b.setAttribute('aria-label', b.title);
|
||||
}
|
||||
|
||||
// ---- the downloads overview: what runs, waits, failed, is done ---------------------------
|
||||
|
||||
let dlSheet = null;
|
||||
let dlBody = null;
|
||||
|
||||
function closeDownloads() {
|
||||
if (!dlSheet) return false;
|
||||
dlSheet.remove();
|
||||
dlSheet = dlBody = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
function openDownloads() {
|
||||
if (!DL) return;
|
||||
closeDownloads();
|
||||
dlSheet = el('div');
|
||||
dlSheet.id = 'nui-pod-player';
|
||||
dlSheet.className = 'nui-pod-notes-sheet nui-pod-dls';
|
||||
const panel = el('div', 'nui-pod-player-panel');
|
||||
const top = el('div', 'nui-pod-player-top');
|
||||
top.append(button('⌄', 'Schließen', closeDownloads));
|
||||
dlBody = el('div', 'nui-pod-dls-body');
|
||||
panel.append(top, el('div', 'nui-pod-player-title', 'Downloads'), dlBody);
|
||||
dlSheet.append(panel);
|
||||
dlSheet.addEventListener('click', ev => { if (ev.target === dlSheet) closeDownloads(); });
|
||||
document.body.append(dlSheet);
|
||||
renderDownloads(true);
|
||||
}
|
||||
|
||||
function dlTitle(d) {
|
||||
return plain(d.title) || 'Folge ' + d.id;
|
||||
}
|
||||
|
||||
// The line under the title: how far, why it waits, what went wrong.
|
||||
function dlStatus(d) {
|
||||
const of = d.total ? `${size(d.bytes)} von ${size(d.total)}` : size(d.bytes);
|
||||
if (d.state === 'downloading') return (d.total ? d.progress + ' % · ' : '') + of;
|
||||
if (d.state === 'failed') return d.error || 'hat nicht geklappt';
|
||||
const parts = [dlReason(d)];
|
||||
if (d.bytes) parts.push(of + ' geladen');
|
||||
if (d.reason === 'retry' && d.error) parts.push(d.error);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function dlActions(d) {
|
||||
const items = [];
|
||||
if (d.state === 'queued' && d.order > 0 && N.podcastDownloadFirst) {
|
||||
items.push({icon: '⇈', label: 'Zuerst laden', hint: 'vor die anderen wartenden', action: () => N.podcastDownloadFirst(d.id)});
|
||||
}
|
||||
if ((d.state === 'failed' || (d.state === 'queued' && d.error)) && N.podcastRetryDownload) {
|
||||
items.push({icon: '↻', label: 'Erneut versuchen', hint: d.error || null, action: () => N.podcastRetryDownload(d.id)});
|
||||
}
|
||||
items.push(d.state === 'failed'
|
||||
? {icon: '✕', label: 'Löschen', danger: true, action: () => N.podcastDeleteDownload(d.id)}
|
||||
: {icon: '✕', label: 'Abbrechen', hint: d.bytes ? 'was schon geladen ist, geht mit' : null, danger: true,
|
||||
action: () => N.podcastDeleteDownload(d.id)});
|
||||
return items;
|
||||
}
|
||||
|
||||
function dlRow(d) {
|
||||
const row = el('div', 'nui-pod-dlrow' + (d.state === 'failed' ? ' nui-pod-dlrow-failed' : ''));
|
||||
row.dataset.dlrow = d.id;
|
||||
const main = el('div', 'nui-pod-main');
|
||||
main.append(el('div', 'nui-pod-title', dlTitle(d)));
|
||||
const f = d.feedId && feeds().find(x => x.id === d.feedId);
|
||||
const meta = el('div', 'nui-pod-meta nui-pod-dlstatus');
|
||||
main.append(meta);
|
||||
if (f) main.append(el('div', 'nui-pod-meta', plain(f.title)));
|
||||
if (d.state === 'downloading' || (d.state === 'queued' && d.bytes)) {
|
||||
const bar = el('div', 'nui-pod-progress nui-pod-stat-bar');
|
||||
bar.append(el('span'));
|
||||
main.append(bar);
|
||||
}
|
||||
row.append(main);
|
||||
// The likely tap right there; all of them in the row's sheet.
|
||||
if (d.state === 'queued' && d.order > 0 && N.podcastDownloadFirst) {
|
||||
row.append(button('⇈', 'Zuerst laden', () => N.podcastDownloadFirst(d.id)));
|
||||
}
|
||||
if (d.state === 'failed' && N.podcastRetryDownload) {
|
||||
row.append(button('↻', 'Erneut versuchen', () => N.podcastRetryDownload(d.id)));
|
||||
}
|
||||
row.append(button('✕', d.state === 'failed' ? 'Löschen' : 'Abbrechen', () => N.podcastDeleteDownload(d.id)));
|
||||
row.addEventListener('click', () => openSheet(dlTitle(d), dlActions(d)));
|
||||
dlRowNumbers(row, d);
|
||||
return row;
|
||||
}
|
||||
|
||||
function dlRowNumbers(row, d) {
|
||||
row.querySelector('.nui-pod-dlstatus').textContent = dlStatus(d);
|
||||
const fill = row.querySelector('.nui-pod-progress > span');
|
||||
if (fill) fill.style.width = (d.total ? Math.round(d.bytes / d.total * 100) : 0) + '%';
|
||||
}
|
||||
|
||||
function renderDownloads(force) {
|
||||
if (!dlBody) return;
|
||||
const all = Object.keys(downloads).map(id => Object.assign({id: +id}, downloads[id]));
|
||||
const running = all.filter(d => d.state === 'downloading');
|
||||
const waiting = all.filter(d => d.state === 'queued').sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
const failed = all.filter(d => d.state === 'failed');
|
||||
const done = all.filter(d => d.state === 'done');
|
||||
// The same rows as before: only the numbers move (keeps a tap or the scroll going).
|
||||
const sig = [running, waiting, failed].map(l => l.map(d => `${d.id}:${d.reason || ''}:${d.error || ''}`).join(','))
|
||||
.join('|') + `|${done.length}|${!!dlInfo.paused}`;
|
||||
if (!force && sig === dlBody.dataset.sig) {
|
||||
for (const d of running.concat(waiting)) {
|
||||
const row = dlBody.querySelector(`[data-dlrow="${d.id}"]`);
|
||||
if (row) dlRowNumbers(row, d);
|
||||
}
|
||||
return;
|
||||
}
|
||||
dlBody.dataset.sig = sig;
|
||||
const panel = dlBody.parentElement;
|
||||
const scroll = panel.scrollTop;
|
||||
dlBody.textContent = '';
|
||||
|
||||
const head = el('div', 'nui-pod-dls-head');
|
||||
head.append(el('div', 'nui-pod-summary', dlSummary()));
|
||||
const tools = el('div', 'nui-pod-offer-row');
|
||||
if (N.podcastPauseDownloads && (running.length || waiting.length || dlInfo.paused)) {
|
||||
const paused = !!dlInfo.paused;
|
||||
tools.append(button(paused ? '▶ Fortsetzen' : '‖ Alle pausieren', paused ? 'Downloads fortsetzen' : 'Alle Downloads pausieren',
|
||||
() => { dlInfo.paused = !paused; N.podcastPauseDownloads(!paused); renderDownloads(true); }, 'btn'));
|
||||
}
|
||||
if (failed.length > 1 && N.podcastRetryDownload) {
|
||||
tools.append(button('↻ Alle erneut', 'Alle fehlgeschlagenen erneut versuchen', () => N.podcastRetryDownload(0), 'btn'));
|
||||
}
|
||||
if (tools.childNodes.length) head.append(tools);
|
||||
dlBody.append(head);
|
||||
|
||||
const section = (title, list) => {
|
||||
if (!list.length) return;
|
||||
dlBody.append(el('div', 'nui-pod-dls-sec', `${title} (${list.length})`));
|
||||
const box = el('div', 'nui-pod-list');
|
||||
for (const d of list) box.append(dlRow(d));
|
||||
dlBody.append(box);
|
||||
};
|
||||
section('Läuft', running);
|
||||
section('Wartet', waiting);
|
||||
section('Fehlgeschlagen', failed);
|
||||
if (done.length) {
|
||||
dlBody.append(el('div', 'nui-pod-dls-sec', 'Fertig'));
|
||||
dlBody.append(el('div', 'nui-pod-meta nui-pod-dls-done',
|
||||
`${done.length} ${done.length === 1 ? 'Folge' : 'Folgen'} auf dem Telefon · ${size(done.reduce((n, d) => n + (d.bytes || 0), 0))}`));
|
||||
}
|
||||
if (!all.length) dlBody.append(el('div', 'nui-pod-empty', 'Keine Downloads. Beim ↓ einer Folge lädst du sie aufs Telefon.'));
|
||||
panel.scrollTop = scroll;
|
||||
}
|
||||
|
||||
// ---- hooks from the app ---------------------------------------------------------------
|
||||
|
||||
window.__dioraPodcasts = function (json) {
|
||||
|
|
@ -2264,7 +1901,6 @@
|
|||
const back = window.__dioraBack;
|
||||
window.__dioraBack = function () {
|
||||
if (closeStats()) return true;
|
||||
if (closeDownloads()) return true;
|
||||
if (closeNotes()) return true;
|
||||
if (closePlayer()) return true;
|
||||
if (typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' && stopSelecting()) return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copied from diora-android 6c2b071 2026-10-02: the podcast parts of web-native/static/native.css
|
||||
/* Copied from diora-android 62f3a69 2026-10-02: 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. */
|
||||
|
||||
|
|
@ -120,18 +120,6 @@
|
|||
|
||||
html.nui-podcasts #tab-podcasts > :not(#nui-pod) { display: none !important; }
|
||||
html.nui-pod-playing #nui-mini { display: none !important; }
|
||||
/* One player for both: the podcast bar shows the radio too. */
|
||||
html.nui-one-player #nui-mini { display: none !important; }
|
||||
.nui-mini-radio .nui-pod-art,
|
||||
.nui-radio-art { display: flex; align-items: center; justify-content: center; }
|
||||
.nui-mini-radio .nui-pod-art:empty::after,
|
||||
.nui-radio-art:empty::after { content: '◉'; color: var(--accent); font-size: 22px; }
|
||||
.nui-radio-art:empty::after { font-size: 96px; }
|
||||
.nui-mini-radio .nui-pod-art img,
|
||||
.nui-radio-art img { object-fit: contain; background: #fff; }
|
||||
.nui-radio-live { text-align: center; font-size: 13px; color: var(--nui-muted); letter-spacing: 0.04em; }
|
||||
.nui-radio-live.on { color: var(--accent); font-weight: 700; }
|
||||
.nui-radio-stations { align-self: center; }
|
||||
html.nui-radio.nui-pod-mini-open body {
|
||||
padding-bottom: calc(124px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
|
@ -507,26 +495,6 @@ body.eink .nui-pod-dl-ring > span {
|
|||
conic-gradient(#000 var(--p, 0%), #ccc 0);
|
||||
}
|
||||
|
||||
/* the top bar's "↓ 3/68" and the downloads overview (openDownloads) */
|
||||
.nui-pod-dlind { min-width: 0 !important; padding: 0 8px; font-size: 13px !important; font-weight: 700; color: var(--accent); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.nui-pod-dlind[hidden] { display: none !important; }
|
||||
.nui-pod-dls .nui-pod-player-panel { min-height: 50%; }
|
||||
.nui-pod-dls-head { display: flex; flex-direction: column; gap: 2px; }
|
||||
.nui-pod-dls-head .nui-pod-summary { padding: 0 2px; text-align: center; }
|
||||
.nui-pod-dls-head .nui-pod-offer-row { justify-content: center; }
|
||||
.nui-pod-dls-head .btn { min-width: 0 !important; padding: 0 14px; font-size: 15px !important; }
|
||||
.nui-pod-dls-sec { margin-top: 10px; padding: 4px 2px; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--nui-muted); border-bottom: 1px solid var(--border); }
|
||||
.nui-pod-dlrow { display: flex; align-items: center; gap: 4px; min-height: 56px; padding: 6px 0; border-bottom: 1px solid var(--border); -webkit-tap-highlight-color: transparent; }
|
||||
.nui-pod-dlrow:active { background: #141414; }
|
||||
.nui-pod-dlrow .nui-pod-title { font-size: 14px; -webkit-line-clamp: 1; }
|
||||
.nui-pod-dlrow .nui-icon { font-size: 18px; color: var(--nui-muted); }
|
||||
.nui-pod-dlrow-failed .nui-pod-dlstatus { color: var(--yellow); }
|
||||
.nui-pod-dls-done { padding: 8px 2px; white-space: normal; }
|
||||
body.eink .nui-pod-dlind { color: #000; }
|
||||
body.eink .nui-pod-dlrow:active { background: #ddd; }
|
||||
body.eink .nui-pod-dlrow-failed .nui-pod-dlstatus { color: #000; font-weight: 700; }
|
||||
body.eink .nui-pod-dls-sec { color: #000; border-bottom-color: #000; }
|
||||
|
||||
/* tab row scrolls sideways without a visible bar; the lock emoji in the app's greys */
|
||||
.nui-pod-tabs::-webkit-scrollbar { display: none; }
|
||||
.nui-pod-lock { filter: grayscale(1); opacity: 0.6; }
|
||||
|
|
@ -548,8 +516,6 @@ body.eink .nui-pod-star, body.eink .nui-pod-fav.on { color: #000 !important; }
|
|||
|
||||
/* feed errors, chapter navigation, transcript */
|
||||
.nui-pod-feederr { font-size: 12px; color: var(--yellow); }
|
||||
.nui-pod-offer { padding: 10px 12px; margin: 8px 0; border: 1px solid var(--nui-muted); border-radius: 6px; line-height: 1.45; font-size: 14px; }
|
||||
.nui-pod-offer-row { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.nui-pod-feederr-box { padding: 8px 10px; margin: 4px 0 8px; border: 1px solid var(--yellow); border-radius: 6px; line-height: 1.4; }
|
||||
.nui-pod-chapnav { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
|
||||
.nui-pod-chapnow { flex: 1; min-width: 0; text-align: center; font-size: 13px; color: var(--nui-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
|
@ -559,6 +525,5 @@ body.eink .nui-pod-star, body.eink .nui-pod-fav.on { color: #000 !important; }
|
|||
.nui-pod-speaker { color: inherit; }
|
||||
body.eink .nui-pod-feederr { color: #000; }
|
||||
body.eink .nui-pod-feederr-box { border-color: #000; }
|
||||
body.eink .nui-pod-offer { border-color: #000; }
|
||||
body.eink .nui-pod-seg { color: #444; }
|
||||
body.eink .nui-pod-seg.current { color: #000; background: #e6e6e6; }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue