Home: neue Startseite mit Weiterlesen, Weiterhören, Radio-Favoriten und Ambient; sw v87
- static/js/home.js rendert #tab-home aus dem, was die Seite schon hat: zuletzt gelesenes Buch (und drei weitere), laufende oder nächste Folge mit Restzeit und „N neue Folgen“, Favoriten-Sender (sonst die ersten gespeicherten) und zuletzt gehörter Sender, Drift mit der zuletzt gewählten Klangwelt und Sleep-Timer. Läuft im Browser, in diora desktop und in der Android-App (dort mit static/js/ kopiert). - app.js: Tab 'home' (TOP_TABS), window.__dioraAmbientStart() lädt Drift bei Bedarf und startet es; die gewählte Klangwelt wird gemerkt und beim Laden wiederhergestellt. - podcasts-web.js: Startseite Home, wenn nichts anderes gewählt ist. - native-podcast.js aus diora-android 7dcc0c6 (Home in der Startseiten-Wahl, __dioraPodcastView). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ae72b1d67a
commit
8b9c28cda1
6 changed files with 378 additions and 16 deletions
|
|
@ -1179,7 +1179,7 @@ async function editNotes(pk, current) {
|
|||
// Tabs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOP_TABS = ['radio', 'focus', 'podcasts', 'books', 'ambient'];
|
||||
const TOP_TABS = ['home', 'radio', 'focus', 'podcasts', 'books', 'ambient'];
|
||||
const RADIO_SUB_TABS = ['search', 'saved', 'history'];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1237,6 +1237,7 @@ function showTab(name) {
|
|||
|
||||
if (name === 'books') loadBookList();
|
||||
if (name === 'ambient') _ambientLoad();
|
||||
if (name === 'home' && window.__dioraHomeShow) window.__dioraHomeShow(); // static/js/home.js
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1260,8 +1261,20 @@ function _ambientLoad() {
|
|||
f.addEventListener('load', () => {
|
||||
_ambientVolume();
|
||||
try {
|
||||
f.contentDocument.getElementById('mode').addEventListener('change', () =>
|
||||
window.__dioraAmbientChanged && window.__dioraAmbientChanged());
|
||||
const sel = f.contentDocument.getElementById('mode');
|
||||
// The sound world chosen last comes back (Drift itself starts with its default).
|
||||
let last = '';
|
||||
try { last = localStorage.getItem('diora_ambient_mode') || ''; } catch (e) {}
|
||||
if (last && last !== sel.value && [...sel.options].some(o => o.value === last)) {
|
||||
f.contentWindow.postMessage({type: 'ambient', cmd: 'mode', mode: last}, '*');
|
||||
}
|
||||
sel.addEventListener('change', () => {
|
||||
try {
|
||||
localStorage.setItem('diora_ambient_mode', sel.value);
|
||||
localStorage.setItem('diora_ambient_mode_label', sel.selectedOptions[0] ? sel.selectedOptions[0].text : '');
|
||||
} catch (e) {}
|
||||
if (window.__dioraAmbientChanged) window.__dioraAmbientChanged();
|
||||
});
|
||||
} catch (e) {}
|
||||
});
|
||||
f.src = f.dataset.src;
|
||||
|
|
@ -1290,6 +1303,19 @@ window.__dioraAmbientCmd = function (cmd, value) {
|
|||
if (f && f.contentWindow && f.getAttribute('src')) f.contentWindow.postMessage({type: 'ambient', cmd, value}, '*');
|
||||
};
|
||||
|
||||
// Start Drift from elsewhere (Home): load the frame if needed, then start it — with the
|
||||
// sound world chosen last, which the frame's load handler above sets.
|
||||
window.__dioraAmbientStart = function () {
|
||||
const f = _ambientFrame();
|
||||
if (!f) return;
|
||||
const go = () => window.__dioraAmbientCmd('start');
|
||||
let ready = false;
|
||||
try { ready = !!f.getAttribute('src') && f.contentDocument.readyState === 'complete' && !!f.contentDocument.getElementById('mode'); } catch (e) {}
|
||||
if (ready) { go(); return; }
|
||||
f.addEventListener('load', () => setTimeout(go, 0), {once: true});
|
||||
_ambientLoad();
|
||||
};
|
||||
|
||||
// The radio or a podcast starts: Ambient stops, and the player bar is theirs again.
|
||||
window.__dioraAmbientStop = function () {
|
||||
if (_ambientPlaying) window.__dioraAmbientCmd('stop');
|
||||
|
|
|
|||
311
static/js/home.js
Normal file
311
static/js/home.js
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
/**
|
||||
* diora Home: what you were in the middle of, one click away — the book you're
|
||||
* reading, the next episode, 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
|
||||
* and Drift (app.js __dioraAmbient*). 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 '<span class="home-bar"><span style="width:' + pct + '%"></span></span>';
|
||||
}
|
||||
|
||||
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 = '<div class="home-card home-empty">' +
|
||||
(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.') +
|
||||
' <button type="button" class="btn btn-sm" data-act="tab" data-tab="books">Zu den Büchern</button></div>';
|
||||
} else {
|
||||
const b = recent[0];
|
||||
body = '<button type="button" class="home-card home-main" data-act="book" data-id="' + esc(b.id) + '">' +
|
||||
'<span class="home-kicker">Weiterlesen</span>' +
|
||||
'<span class="home-title">' + esc(b.title) + '</span>' +
|
||||
(b.author ? '<span class="home-sub">' + esc(b.author) + '</span>' : '') +
|
||||
'<span class="home-state">' + bar(b.scroll_fraction) + '<span>' + Math.round((b.scroll_fraction || 0) * 100) + ' %</span></span>' +
|
||||
'</button>';
|
||||
const more = recent.slice(1, 4);
|
||||
if (more.length) {
|
||||
body += '<div class="home-list">' + more.map(o =>
|
||||
'<button type="button" class="home-row" data-act="book" data-id="' + esc(o.id) + '">' +
|
||||
'<span class="home-row-text">' + esc(o.title) + '</span>' +
|
||||
'<span class="home-row-side">' + Math.round((o.scroll_fraction || 0) * 100) + ' %</span></button>').join('') + '</div>';
|
||||
}
|
||||
}
|
||||
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,
|
||||
'<button type="button" class="btn btn-sm" data-act="pod-here">Hier weiterhören</button>');
|
||||
} else if (player.kind === 'podcast') {
|
||||
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,
|
||||
'<button type="button" class="btn btn-sm home-play" data-act="' + (player.playing ? 'pod-pause' : 'pod-resume') +
|
||||
'" data-id="' + esc(player.episodeId) + '">' + (player.playing ? '■ Pause' : '▶ Weiter') + '</button>');
|
||||
} 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,
|
||||
'<button type="button" class="btn btn-sm home-play" data-act="pod-queue">▶ Abspielen</button>');
|
||||
} else {
|
||||
card = '<div class="home-card home-empty">Die Warteschlange ist leer.</div>';
|
||||
}
|
||||
const extra = '<div class="home-chips">' +
|
||||
(queue.length > 1 ? '<button type="button" class="home-chip" data-act="tab" data-tab="podcasts">' + queue.length + ' in der Warteschlange</button>' : '') +
|
||||
(fresh ? '<button type="button" class="home-chip" data-act="pod-inbox">' + fresh + ' neue Folge' + (fresh === 1 ? '' : 'n') + '</button>' : '') +
|
||||
'</div>';
|
||||
return section('Podcasts', card + extra, 'podcasts');
|
||||
}
|
||||
|
||||
function podCard(kicker, title, feed, art, leftSec, fraction, button) {
|
||||
return '<div class="home-card home-main home-pod">' +
|
||||
(art ? '<img class="home-art" alt="" loading="lazy" src="' + esc(art) + '">' : '<span class="home-art"></span>') +
|
||||
'<span class="home-pod-text" data-act="pod-open">' +
|
||||
'<span class="home-kicker">' + esc(kicker) + '</span>' +
|
||||
'<span class="home-title">' + esc(title) + '</span>' +
|
||||
'<span class="home-sub">' + esc(feed) + (leftSec > 30 ? ' · noch ' + minutes(leftSec) : '') + '</span>' +
|
||||
'<span class="home-state">' + bar(fraction) + '</span></span>' +
|
||||
button + '</div>';
|
||||
}
|
||||
|
||||
// ---- 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 <html>).
|
||||
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 = '<div class="home-card home-empty">Noch keine Sender gespeichert. ' +
|
||||
'<button type="button" class="btn btn-sm" data-act="tab" data-tab="radio">Sender suchen</button></div>';
|
||||
} else {
|
||||
body = '<div class="home-stations">' + chips + '</div>';
|
||||
}
|
||||
if (!online()) body += '<div class="home-note">Offline — Radio braucht eine Verbindung.</div>';
|
||||
return section(favs.length ? 'Radio · Favoriten' : 'Radio', body, 'radio');
|
||||
}
|
||||
|
||||
function chip(url, name, playing, prefix) {
|
||||
return '<button type="button" class="home-station' + (playing ? ' home-on' : '') + '" data-act="station" data-url="' +
|
||||
esc(url) + '" data-name="' + esc(name) + '">' +
|
||||
'<span class="home-station-icon">' + (playing ? '■' : '▶') + '</span>' +
|
||||
'<span class="home-station-name">' + esc(prefix) + esc(name) + '</span></button>';
|
||||
}
|
||||
|
||||
// ---- 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 = '<div class="home-card home-main home-amb">' +
|
||||
'<span class="home-amb-icon">∿</span>' +
|
||||
'<span class="home-pod-text" data-act="tab" data-tab="ambient">' +
|
||||
'<span class="home-kicker">' + (info.playing ? 'Läuft gerade' : 'Drift') + '</span>' +
|
||||
'<span class="home-title">' + esc(label || 'Ambient') + '</span>' +
|
||||
'<span class="home-sub">Generative Klangwelten' + esc(left) + '</span></span>' +
|
||||
(document.getElementById('nui-amb-sleep') ? '<button type="button" class="btn btn-sm" data-act="amb-sleep" title="Sleep-Timer">☾</button>' : '') +
|
||||
'<button type="button" class="btn btn-sm home-play" data-act="' + (info.playing ? 'amb-stop' : 'amb-start') + '">' +
|
||||
(info.playing ? '■ Stopp' : '▶ Starten') + '</button></div>';
|
||||
return section('Ambient', body, 'ambient');
|
||||
}
|
||||
|
||||
// ---- page ---------------------------------------------------------------------
|
||||
|
||||
function section(title, body, tabName) {
|
||||
return '<section class="home-section"><div class="home-head"><h2>' + esc(title) + '</h2>' +
|
||||
'<button type="button" class="home-more" data-act="tab" data-tab="' + tabName + '">Alle ›</button></div>' + body + '</section>';
|
||||
}
|
||||
|
||||
function greeting() {
|
||||
const h = new Date().getHours();
|
||||
return h < 5 ? 'Gute Nacht' : h < 11 ? 'Guten Morgen' : h < 18 ? 'Hallo' : 'Guten Abend';
|
||||
}
|
||||
|
||||
let lastHtml = '';
|
||||
let host = null;
|
||||
|
||||
function render() {
|
||||
if (!visible()) return;
|
||||
if (!host) {
|
||||
host = document.createElement('div');
|
||||
host.className = 'home';
|
||||
tab.appendChild(host);
|
||||
}
|
||||
let html = '<div class="home-hello">' + greeting() + '</div>';
|
||||
for (const part of [booksSection, podcastSection, 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 === '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 === '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 = 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; }',
|
||||
'#tab-home .home-hello { font-size: 22px; font-weight: 700; margin: 4px 0 18px; letter-spacing: 0.02em; }',
|
||||
'#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(--nui-muted, #9a9a9a); }',
|
||||
'#tab-home .home-more { background: none; border: none; color: var(--nui-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: #0b0b0b; 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(--nui-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(--nui-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(--nui-muted, #9a9a9a); }',
|
||||
'#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: #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: #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(--nui-muted, #9a9a9a); font-size: 12px; }',
|
||||
'#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: #0b0b0b; 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(--nui-muted, #9a9a9a); }',
|
||||
'@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: #141414; } }',
|
||||
'.reader-theme-bright #tab-home, body.eink #tab-home .home-card { background: #fff; }',
|
||||
].join('\n');
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
|
|
@ -1873,15 +1873,18 @@
|
|||
// The shared player: what plays on the other devices, and from now on every change.
|
||||
startSync();
|
||||
|
||||
// The start page chosen in the podcasts' settings (nui_start_page, this browser; none: the
|
||||
// last used tab, the web's way). Opening diora shows it; reloading and back/forward keep the tab.
|
||||
// The start page chosen in the podcasts' settings (nui_start_page, this browser): Home
|
||||
// unless chosen otherwise; '' = the last used tab, the web's old way. Opening diora shows
|
||||
// it; reloading and back/forward keep the tab.
|
||||
(function openStartPage() {
|
||||
let start = '';
|
||||
try { start = localStorage.getItem('nui_start_page') || ''; } catch (e) {}
|
||||
let start = null;
|
||||
try { start = localStorage.getItem('nui_start_page'); } catch (e) {}
|
||||
if (start === null) start = document.getElementById('tab-home') ? 'home' : '';
|
||||
if (!start || typeof showTab !== 'function') return;
|
||||
const nav = performance.getEntriesByType ? performance.getEntriesByType('navigation')[0] : null;
|
||||
if (nav && nav.type !== 'navigate') return;
|
||||
const tab = start === 'queue' || start === 'inbox' ? 'podcasts' : start === 'radio' ? 'radio' : 'books';
|
||||
const tab = start === 'queue' || start === 'inbox' ? 'podcasts' : start === 'radio' ? 'radio'
|
||||
: start === 'home' && document.getElementById('tab-home') ? 'home' : 'books';
|
||||
if (typeof _currentTopTab === 'undefined' || _currentTopTab !== tab) showTab(tab);
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v86';
|
||||
const CACHE = 'diora-v87';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app.js',
|
||||
'/static/js/podcasts-web.js',
|
||||
'/static/js/home.js',
|
||||
'/static/podcasts/native-podcast.js',
|
||||
'/static/podcasts/podcast-ui.css',
|
||||
'/static/podcasts/podcasts-web.css',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android 1b935ff 2026-10-04: web-native/static/native-podcast.js
|
||||
// Copied from diora-android 7dcc0c6 2026-10-04: 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.
|
||||
|
||||
|
|
@ -34,9 +34,11 @@
|
|||
const VIEW_IDS = VIEWS.map(v => v.id).concat('search');
|
||||
// The app's start page (this device; native.js opens it, the browser host too).
|
||||
const IN_APP = !window.DioraPodcastHost;
|
||||
const START_PAGES = [
|
||||
!IN_APP && {id: '', label: 'Zuletzt benutzt', hint: 'Standard im Browser'},
|
||||
{id: 'books', label: 'Bücher', hint: IN_APP ? 'Standard' : null},
|
||||
// A function: the app loads this file in <head>, before #tab-home exists.
|
||||
const startPages = () => [
|
||||
document.getElementById('tab-home') && {id: 'home', label: 'Home', hint: 'Standard'},
|
||||
!IN_APP && {id: '', label: 'Zuletzt benutzt'},
|
||||
{id: 'books', label: 'Bücher', hint: IN_APP && !document.getElementById('tab-home') ? 'Standard' : null},
|
||||
{id: 'queue', label: 'Podcasts: Warteschlange'},
|
||||
{id: 'inbox', label: 'Podcasts: Neu'},
|
||||
{id: 'radio', label: 'Radio'},
|
||||
|
|
@ -44,7 +46,8 @@
|
|||
function startPage() {
|
||||
let v = null;
|
||||
try { v = localStorage.getItem('nui_start_page'); } catch (e) {}
|
||||
return START_PAGES.some(p => p.id === v) ? v : START_PAGES[0].id;
|
||||
const pages = startPages();
|
||||
return pages.some(p => p.id === v) ? v : pages[0].id;
|
||||
}
|
||||
const POSITIONS = [
|
||||
{value: 'back', label: 'Ans Ende'},
|
||||
|
|
@ -556,9 +559,9 @@
|
|||
// Which page the app opens with (this device).
|
||||
function startPageItem() {
|
||||
const now = startPage();
|
||||
const label = id => (START_PAGES.find(p => p.id === id) || START_PAGES[0]).label;
|
||||
const label = id => (startPages().find(p => p.id === id) || startPages()[0]).label;
|
||||
return {icon: '⌂', label: 'Startseite', hint: label(now) + (IN_APP ? ' · beim Öffnen der App' : ' · beim Öffnen von diora'),
|
||||
action: () => openSheet('Beim Öffnen zeigen …', START_PAGES.filter(p => p.id !== 'radio' || !IN_APP ||
|
||||
action: () => openSheet('Beim Öffnen zeigen …', startPages().filter(p => p.id !== 'radio' || !IN_APP ||
|
||||
document.documentElement.classList.contains('nui-radio')).map(p => ({
|
||||
icon: p.id === now ? '●' : '○', label: p.label, hint: p.hint || null, active: p.id === now,
|
||||
action: () => {
|
||||
|
|
@ -3487,6 +3490,17 @@
|
|||
if (sheet && sheetKind === 'radio') renderRadioPlayer();
|
||||
};
|
||||
window.__dioraOpenPlayer = kind => (kind === 'radio' && !remote() ? openRadioPlayer() : openPlayer());
|
||||
// Home (diora-web static/js/home.js): one of the lists, e.g. "inbox" for the new episodes.
|
||||
window.__dioraPodcastView = function (id) {
|
||||
if (VIEW_IDS.indexOf(id) < 0) return;
|
||||
selected = null;
|
||||
feedView = null;
|
||||
preview = null;
|
||||
view = id;
|
||||
try { localStorage.setItem('nui_pod_view', view); } catch (e) {}
|
||||
render();
|
||||
scrollTop(0);
|
||||
};
|
||||
|
||||
function renderPlayer() {
|
||||
if (!sheet) return;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
|
||||
<!-- ===== TABS ===== -->
|
||||
<div class="tabs" id="tabs">
|
||||
{# Order = app.js TOP_TABS. Home needs an account: hidden, but kept in place for the order. #}
|
||||
<button class="tab-btn" onclick="showTab('home')"{% if not user.is_authenticated %} style="display:none;"{% endif %}>Home</button>
|
||||
<button class="tab-btn active" onclick="showTab('radio')">Radio</button>
|
||||
<button class="tab-btn" onclick="showTab('focus')">Fokus</button>
|
||||
<button class="tab-btn" onclick="showTab('podcasts')">Podcasts</button>
|
||||
|
|
@ -255,6 +257,10 @@
|
|||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- ===== HOME TAB ===== -->
|
||||
{% comment %}Filled by static/js/home.js: the book in reading, the next episode, stations, Ambient.{% endcomment %}
|
||||
<section class="tab-panel" id="tab-home" style="display:none;"></section>
|
||||
|
||||
<!-- ===== AMBIENT TAB ===== -->
|
||||
{% comment %}"Drift", a generative ambient synthesizer (static/ambient/), in a frame of its own: its
|
||||
script and styles stay apart from the page's. Loaded the first time the tab opens.{% endcomment %}
|
||||
|
|
@ -338,4 +344,5 @@
|
|||
</script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/podcasts-web.js"></script>
|
||||
<script src="/static/js/home.js"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue