diff --git a/static/js/app.js b/static/js/app.js
index 5e09935..b1c5e61 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -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');
diff --git a/static/js/home.js b/static/js/home.js
new file mode 100644
index 0000000..d193fc3
--- /dev/null
+++ b/static/js/home.js
@@ -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 '';
+ }
+
+ 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 += '
+ {# Order = app.js TOP_TABS. Home needs an account: hidden, but kept in place for the order. #}
+
@@ -255,6 +257,10 @@
{% endif %}
+
+{% comment %}Filled by static/js/home.js: the book in reading, the next episode, stations, Ambient.{% endcomment %}
+
+
{% 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 @@
+
{% endblock %}