/** * diora Home: what you were in the middle of, one click away — the book you're * reading, the next episode, the news, your stations, Ambient. * * Built only from what the page already has: the book list (app.js), the * podcast host (window.DioraPodcastHost in the browser and diora desktop, * window.DioraNative in the Android app), the saved stations in the radio tab * Drift (app.js __dioraAmbient*) and the feed reader (feeds.js __dioraFeeds*). Renders into #tab-home; the same file runs * in the browser, diora desktop and the Android app (copied with static/js/). * Written without ?. and ?? so old WebViews can still parse it. */ (function () { 'use strict'; const tab = document.getElementById('tab-home'); if (!tab) return; const $ = id => document.getElementById(id); const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c])); const parse = (s, fallback) => { try { return typeof s === 'string' ? JSON.parse(s) : (s || fallback); } catch (e) { return fallback; } }; const visible = () => tab.style.display !== 'none'; const online = () => navigator.onLine && !document.body.classList.contains('desktop-offline'); function minutes(sec) { const m = Math.max(0, Math.round((sec || 0) / 60)); return m >= 60 ? Math.floor(m / 60) + ' h ' + String(m % 60).padStart(2, '0') + ' min' : m + ' min'; } function bar(fraction) { const pct = Math.max(0, Math.min(100, Math.round((fraction || 0) * 100))); return ''; } function podHost() { if (window.DioraPodcastHost) return window.DioraPodcastHost; if (window.DioraNative && window.DioraNative.podcastState) return window.DioraNative; return null; } // ---- Weiterlesen ------------------------------------------------------------ let askedForBooks = false; function booksSection() { const books = typeof _lastBookListData !== 'undefined' ? _lastBookListData : []; if (!books.length && !askedForBooks && typeof loadBookList === 'function') { askedForBooks = true; // the list renders into the (hidden) books tab; renderBookList tells us loadBookList(); } const recent = typeof _recentlyReadBooks === 'function' ? _recentlyReadBooks(books).filter(b => b.keyOk !== false && !b.is_read) : []; let body; if (!recent.length) { const locked = books.length && books.every(b => b.keyOk === false); body = '
' + (locked ? 'Die Bücher lassen sich mit dem Schlüssel auf diesem Gerät nicht öffnen.' : books.length ? 'Gerade kein Buch angefangen.' : 'Noch keine Bücher.') + '
'; } else { const b = recent[0]; body = ''; const more = recent.slice(1, 4); if (more.length) { body += '
' + more.map(o => '').join('') + '
'; } } return section('Bücher', body, 'books'); } // ---- Weiterhören ------------------------------------------------------------ function podcastSection() { const h = podHost(); if (!h || !document.getElementById('tab-podcasts')) return ''; const state = parse(h.podcastState(), {}) || {}; const player = h.playerState ? parse(h.playerState(), {}) || {} : {}; const queue = state.queue || []; const fresh = state.inbox_total != null ? state.inbox_total : (state.inbox || []).length; let card = ''; if (player.remote && player.remote.kind === 'podcast') { const r = player.remote; card = podCard('Läuft auf ' + (r.device || 'einem anderen Gerät'), r.title || '', r.feed || '', r.artwork || '', r.dur ? (r.dur - (r.pos || 0)) / 1000 : 0, r.dur ? (r.pos || 0) / r.dur : 0, ''); } else if (player.kind === 'podcast' && !player.book) { // an audiobook's chapter: under Hörbücher const left = player.dur ? (player.dur - (player.pos || 0)) / 1000 : 0; card = podCard(player.playing ? 'Läuft gerade' : 'Weiterhören', player.title, player.feed, player.artwork, left, player.dur ? (player.pos || 0) / player.dur : 0, ''); } else if (queue.length) { const q = queue[0]; const dur = q.episode__duration_seconds || 0; const pos = q.position_seconds || 0; card = podCard(pos > 0 ? 'Weiterhören' : 'Als Nächstes', q.episode__title, q.episode__feed__title, q.episode__artwork_url || q.episode__feed__artwork_url, dur ? dur - pos : 0, dur ? pos / dur : 0, ''); } else { card = '
Die Warteschlange ist leer.
'; } const extra = '
' + (queue.length > 1 ? '' : '') + (fresh ? '' : '') + '
'; return section('Podcasts', card + extra, 'podcasts'); } function podCard(kicker, title, feed, art, leftSec, fraction, button) { return '
' + (art ? '' : '') + '' + '' + esc(kicker) + '' + '' + esc(title) + '' + '' + esc(feed) + (leftSec > 30 ? ' · noch ' + minutes(leftSec) : '') + '' + '' + bar(fraction) + '' + button + '
'; } // ---- Nachrichten -------------------------------------------------------------- // The briefing comes from the server, so it's fetched now and then, not every render. let feeds = null; let feedsAt = 0; let feedsBusy = false; function refreshFeeds(maxAgeMs) { if (typeof window.__dioraFeedsSummary !== 'function' || feedsBusy || !online()) return; if (Date.now() - feedsAt < maxAgeMs) return; feedsBusy = true; window.__dioraFeedsSummary().then(r => { feeds = r; }, () => {}).then(() => { feedsAt = Date.now(); feedsBusy = false; render(); }); } function feedsSection() { if (!document.getElementById('tab-feeds') || !feeds || !feeds.state) return ''; const s = feeds.state; const sources = s.sources || []; if (!sources.length) return ''; const hasNews = sources.some(x => x.kind === 'news'); const top = ((feeds.briefing && feeds.briefing.top) || []).slice(0, 4); let body = ''; if (top.length) { body = '
' + top.map(x => { const lead = x.lead || {}; return ''; }).join('') + '
'; } else if (hasNews) { body = '
' + (s.news_new ? 'Keine Top-Themen seit dem letzten Mal.' : 'Nichts Neues seit dem letzten Mal.') + '
'; } const chips = (s.news_new ? '' : '') + (s.blogs_unread ? '' : ''); if (!body && !chips) return ''; return section(hasNews ? 'Nachrichten' : 'Blogs', body + (chips ? '
' + chips + '
' : ''), 'feeds'); } function openFeeds(view) { showTab('feeds'); const b = view && document.querySelector('#tab-feeds [data-view="' + view + '"]:not(.on)'); if (b) b.click(); } // ---- Radio ------------------------------------------------------------------ function stationRows() { return Array.prototype.slice.call(document.querySelectorAll('#saved-tbody tr[data-id]')); } function radioSection() { if (!document.getElementById('tab-radio')) return ''; // The app shows the radio only where it has one (native-radio.js marks ). if (window.DioraNative && !document.documentElement.classList.contains('nui-radio')) return ''; const rows = stationRows(); const favs = rows.filter(r => r.querySelector('.fav-btn.active')); const pick = (favs.length ? favs : rows).slice(0, 8); const cur = typeof currentStation !== 'undefined' ? currentStation : null; const playing = typeof isPlaying !== 'undefined' && isPlaying; let chips = ''; if (cur && !pick.some(r => r.dataset.url === cur.url)) { chips += chip(cur.url, cur.name, playing, 'Zuletzt: '); } chips += pick.map(r => chip(r.dataset.url, r.dataset.name, playing && cur && cur.url === r.dataset.url, '')).join(''); let body; if (!chips) { body = '
Noch keine Sender gespeichert. ' + '
'; } else { body = '
' + chips + '
'; } if (!online()) body += '
Offline — Radio braucht eine Verbindung.
'; return section(favs.length ? 'Radio · Favoriten' : 'Radio', body, 'radio'); } function chip(url, name, playing, prefix) { return ''; } // ---- Ambient ---------------------------------------------------------------- function ambientSection() { if (!document.getElementById('tab-ambient') || typeof window.__dioraAmbientInfo !== 'function') return ''; if (window.DioraNative && !document.querySelector('#nui-tabbar [data-tab="ambient"]') && document.getElementById('nui-tabbar')) return ''; const info = window.__dioraAmbientInfo() || {}; let label = info.mode || ''; if (!label) { try { label = localStorage.getItem('diora_ambient_mode_label') || ''; } catch (e) {} } const h = podHost(); const player = h && h.playerState ? parse(h.playerState(), {}) || {} : {}; const left = player.sleep && player.sleep.leftMs > 0 ? ' · ☾ ' + minutes(player.sleep.leftMs / 1000) : ''; const body = '
' + '∿' + '' + '' + (info.playing ? 'Läuft gerade' : 'Drift') + '' + '' + esc(label || 'Ambient') + '' + 'Generative Klangwelten' + esc(left) + '' + (document.getElementById('nui-amb-sleep') ? '' : '') + '
'; return section('Ambient', body, 'ambient'); } // ---- page --------------------------------------------------------------------- // ---- Hörbücher (static/js/audiobooks.js) ------------------------------------------- function audiobookSection() { const h = podHost(); if (!h || !document.getElementById('tab-audiobooks') || !window.__dioraAudiobookGo) return ''; const player = h.playerState ? parse(h.playerState(), {}) || {} : {}; let books = []; try { books = JSON.parse(localStorage.getItem('diora_ab_books') || '[]') || []; } catch (e) {} let card; if (player.kind === 'podcast' && player.book) { const b = player.book; const left = player.dur ? (player.dur - (player.pos || 0)) / 1000 : 0; card = podCard((player.playing ? 'Läuft gerade' : 'Weiterhören') + ' · Kapitel ' + (b.index + 1) + '/' + b.chapters, player.title, b.title || player.feed, player.artwork, left, player.dur ? (player.pos || 0) / player.dur : 0, ''); } else { const b = books.filter(x => x.next && x.last_played_at)[0]; if (!b) return ''; card = podCard('Weiterhören · Kapitel ' + (b.next.index + 1) + '/' + b.chapters, b.title, b.author || '', b.artwork_url, b.total_seconds ? b.total_seconds - b.heard_seconds : 0, b.total_seconds ? b.heard_seconds / b.total_seconds : 0, ''); } return section('Hörbücher', card, 'audiobooks'); } function section(title, body, tabName) { return '

' + esc(title) + '

' + '
' + body + '
'; } let lastHtml = ''; let host = null; function render() { if (!visible()) return; if (!host) { host = document.createElement('div'); host.className = 'home'; tab.appendChild(host); } let html = '
diora
'; refreshFeeds(3 * 60000); for (const part of [booksSection, audiobookSection, podcastSection, feedsSection, radioSection, ambientSection]) { try { html += part(); } catch (e) { console.warn('home', e); } } if (html !== lastHtml) { lastHtml = html; host.innerHTML = html; } } function playStationFromHome(url, name) { const cur = typeof currentStation !== 'undefined' ? currentStation : null; if (typeof isPlaying !== 'undefined' && isPlaying && cur && cur.url === url) { stopPlayback(false); return; } const row = stationRows().find(r => r.dataset.url === url); const b = row && row.querySelector('button[onclick*="playStation"]'); if (b) b.click(); else playStation(url, name, row ? Number(row.dataset.id) || null : (cur && cur.url === url ? cur.id : null)); } tab.addEventListener('click', e => { const el = e.target.closest('[data-act]'); if (!el || !tab.contains(el)) return; const act = el.dataset.act; const h = podHost(); if (act === 'tab') showTab(el.dataset.tab); else if (act === 'book') openBook(Number(el.dataset.id)); else if (act === 'story') { const id = Number(el.dataset.id); const story = feeds && feeds.briefing && (feeds.briefing.top || []).find(x => x.id === id); if (story && window.__dioraFeedsOpenStory) { window.__dioraFeedsOpenStory(story); feedsAt = 0; } else openFeeds('news'); } else if (act === 'feeds') { openFeeds(el.dataset.view); feedsAt = 0; } else if (act === 'station') playStationFromHome(el.dataset.url, el.dataset.name); else if (act === 'pod-queue' && h) { const q = (parse(h.podcastState(), {}).queue || [])[0]; if (q) h.podcastPlayEpisode(JSON.stringify(q), 0); } else if (act === 'pod-resume' && h) h.podcastPlay(Number(el.dataset.id)); else if (act === 'pod-pause' && h) h.podcastToggle(); else if (act === 'ab-go' && window.__dioraAudiobookGo) window.__dioraAudiobookGo(Number(el.dataset.id)); else if (act === 'pod-here' && h && h.playerTakeOver) h.playerTakeOver(); else if (act === 'pod-open') { if (window.__dioraOpenPlayer && h && parse(h.playerState(), {}).kind) window.__dioraOpenPlayer('podcast'); else showTab('podcasts'); } else if (act === 'pod-inbox') { showTab('podcasts'); if (window.__dioraPodcastView) window.__dioraPodcastView('inbox'); } else if (act === 'amb-start' && window.__dioraAmbientStart) window.__dioraAmbientStart(); else if (act === 'amb-stop' && window.__dioraAmbientCmd) window.__dioraAmbientCmd('stop'); else if (act === 'amb-sleep') { const b = $('nui-amb-sleep'); if (b) b.click(); } setTimeout(render, 300); }); // Fresh whenever it shows, and while it shows (players move on, the book list arrives). window.__dioraHomeShow = function () { refreshFeeds(30000); render(); }; if (typeof window.renderBookList === 'function') { const renderBookList = window.renderBookList; window.renderBookList = function () { const r = renderBookList.apply(this, arguments); render(); return r; }; } setInterval(render, 2000); // The start page may have opened Home before this file ran (podcasts-web.js, native.js). if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', render); else render(); const style = document.createElement('style'); style.textContent = [ '#tab-home .home { max-width: 760px; margin: 0 auto; padding-bottom: 24px; }', // The wordmark as in the web's navbar: red, bold, the page's monospace. '#tab-home .home-brand { font-size: 26px; font-weight: 700; color: var(--accent, #e63946); margin: 4px 0 18px; letter-spacing: 0.05em; }', '#tab-home .home-section { margin-bottom: 22px; }', '#tab-home .home-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }', '#tab-home .home-head h2 { margin: 0; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted, #9a9a9a); }', '#tab-home .home-more { background: none; border: none; color: var(--muted, #9a9a9a); font: inherit; font-size: 12px; cursor: pointer; padding: 4px 0; }', '#tab-home .home-card { display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; text-align: left; }', '#tab-home button.home-card, #tab-home .home-pod-text, #tab-home .home-row, #tab-home .home-station { cursor: pointer; }', '#tab-home .home-main { flex-wrap: nowrap; }', '#tab-home button.home-main { flex-direction: column; align-items: flex-start; gap: 3px; }', '#tab-home .home-empty { color: var(--muted, #9a9a9a); justify-content: space-between; }', '#tab-home .home-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent, #e63946); }', '#tab-home .home-title { font-size: 16px; font-weight: 700; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }', '#tab-home .home-sub { font-size: 13px; color: var(--muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }', '#tab-home .home-state { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--muted, #9a9a9a); }', '#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: var(--raised, #2a2a2a); overflow: hidden; }', '#tab-home .home-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }', '#tab-home .home-pod-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }', '#tab-home .home-art { width: 56px; height: 56px; flex: none; border-radius: 4px; object-fit: cover; background: var(--raised, #1a1a1a); }', '#tab-home .home-amb-icon { width: 56px; height: 56px; flex: none; border-radius: 4px; background: #14102a; color: #b9a7ff; display: inline-flex; align-items: center; justify-content: center; font-size: 26px; }', '#tab-home .home-play { flex: none; min-height: 36px; padding: 0 14px; }', '#tab-home .home-list { margin-top: 6px; }', '#tab-home .home-row { display: flex; justify-content: space-between; gap: 12px; width: 100%; padding: 10px 14px; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; font-size: 14px; text-align: left; }', '#tab-home .home-row-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-row-side { flex: none; color: var(--muted, #9a9a9a); font-size: 12px; }', '#tab-home .home-news { flex-direction: column; align-items: stretch; gap: 0; padding: 4px 0; }', '#tab-home .home-story { align-items: baseline; }', '#tab-home .home-news .home-story:last-child { border-bottom: none; }', '#tab-home .home-story-title { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }', '#tab-home .home-story .home-row-side { max-width: 35%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }', '#tab-home .home-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; }', '#tab-home .home-stations { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }', '#tab-home .home-station { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; font-size: 14px; text-align: left; }', '#tab-home .home-station-icon { color: var(--accent, #e63946); flex: none; }', '#tab-home .home-station-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-on { border-color: var(--accent, #e63946); }', '#tab-home .home-note { margin-top: 8px; font-size: 13px; color: var(--muted, #9a9a9a); }', '@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: var(--hover, #141414); } }', '.reader-theme-bright #tab-home, body.eink #tab-home .home-card { background: #fff; }', ].join('\n'); document.head.appendChild(style); })();