/* * The navigation (docs/navigation.md): diora's sections, sorted by what one does — * Home; Lesen: Bücher, Zeitschriften, Nachrichten, Blogs; Hören: Hörbücher, Podcasts, * Radio, Ambient. One definition for every platform: the web page's #tabs are drawn * here, the apps' tab bar and sidebar (native-radio.js) from DioraNav.groups() and * DioraNav.sections(). A section is a panel (showTab, as before) plus a view in it. * On a phone, Lesen and Hören show their sections as a row on top (#nav-segments). * Loaded before the app scripts (99-init.js restores the section through it). * Chrome-61-safe: no ?. / ??. */ (function () { 'use strict'; const html = document.documentElement; const authed = typeof IS_AUTHENTICATED === 'undefined' || !!IS_AUTHENTICATED; const SECTIONS = [ {id: 'home', group: 'home', label: 'Home', icon: '⌂', panel: 'home', account: true}, {id: 'books', group: 'lesen', label: 'Bücher', icon: '▤', panel: 'books', view: 'books'}, {id: 'magazines', group: 'lesen', label: 'Zeitschriften', icon: '▥', panel: 'books', view: 'magazines', account: true}, {id: 'news', group: 'lesen', label: 'Nachrichten', icon: '☰', panel: 'feeds', view: 'news', account: true}, {id: 'blogs', group: 'lesen', label: 'Blogs', icon: '✎', panel: 'feeds', view: 'blogs', account: true}, {id: 'audiobooks', group: 'hoeren', label: 'Hörbücher', icon: '◐', panel: 'audiobooks', account: true, needsPodcasts: true}, {id: 'podcasts', group: 'hoeren', label: 'Podcasts', icon: '◎', panel: 'podcasts'}, {id: 'radio', group: 'hoeren', label: 'Radio', icon: '◉', panel: 'radio'}, {id: 'ambient', group: 'hoeren', label: 'Ambient', icon: '∿', panel: 'ambient'}, // The focus timer: the web page's own (the apps never had it). {id: 'focus', group: 'hoeren', label: 'Fokus', icon: '◷', panel: 'focus', webOnly: true}, ]; const GROUPS = [ {id: 'home', label: 'Home', icon: '⌂'}, {id: 'lesen', label: 'Lesen', icon: '▤'}, {id: 'hoeren', label: 'Hören', icon: '◉'}, ]; const byId = {}; SECTIONS.forEach(s => { byId[s.id] = s; }); const nativeUi = () => html.classList.contains('nui-radio'); // the apps' own bars (native-radio.js) const readerApp = () => html.classList.contains('nui-reader-app'); const noRead = () => html.classList.contains('nui-noread'); // TV // Audiobooks need the podcast player: the page has them where it loads audiobooks.js // (diora Reader's build leaves it out). Known before the player's script has run. const podcastHost = () => !!document.querySelector('script[src$="/static/js/audiobooks.js"]'); function available(s) { if (!document.getElementById('tab-' + s.panel)) return false; if (s.account && !authed) return false; if (s.webOnly && nativeUi()) return false; if (s.needsPodcasts && !podcastHost()) return false; if (readerApp() && s.group !== 'lesen') return false; // diora Reader: reading only if (noRead() && s.group === 'lesen') return false; // TV: no reading return true; } function sections() { return SECTIONS.filter(available).map(s => ({id: s.id, group: s.group, label: s.label, icon: s.icon})); } function groups() { const list = sections(); return GROUPS.map(g => ({id: g.id, label: g.label, icon: g.icon, sections: list.filter(s => s.group === g.id).map(s => s.id)})).filter(g => g.sections.length); } function load(key) { try { return localStorage.getItem(key); } catch (e) { return null; } } function save(key, value) { try { localStorage.setItem(key, value); } catch (e) {} } let current = null; const listeners = []; // A section of `group`: the one used last there, else its first. function lastOf(group) { const list = sections().filter(s => s.group === group); const last = load('diora_nav_last_' + group); const found = list.filter(s => s.id === last)[0]; return (found || list[0] || {}).id || null; } // What a name means: a section, a group (its last section), or a panel (its section). function resolve(name) { if (byId[name] && available(byId[name])) return name; if (GROUPS.some(g => g.id === name)) return lastOf(name); const inPanel = sections().filter(s => byId[s.id].panel === name); if (!inPanel.length) return null; const last = lastOf(byId[inPanel[0].id].group); return inPanel.some(s => s.id === last) ? last : inPanel[0].id; } function applyView(s) { if (s.panel === 'books' && window.__dioraBooksView) window.__dioraBooksView(s.view); if (s.panel === 'feeds' && window.__dioraFeedsView) window.__dioraFeedsView(s.view); } function setCurrent(id) { current = id; const s = byId[id]; save('diora_nav_section', id); save('diora_nav_last_' + s.group, id); syncHash(); render(); listeners.slice().forEach(fn => { try { fn(id); } catch (e) {} }); } let going = false; function go(name) { wrapShowTab(); const id = resolve(name); if (!id) return false; const s = byId[id]; applyView(s); // first: the panel then shows the chosen view going = true; try { window.showTab(s.panel); } finally { going = false; } // with everything hooked onto it setCurrent(id); return true; } // Anything that shows a panel directly (Home's cards, the offline switch to the books, // a notification …): the section follows. Hooked on when first needed: this file comes // before the app's scripts, which define showTab. let wrapped = false; function wrapShowTab() { if (wrapped || typeof window.showTab !== 'function') return; wrapped = true; const showTab = window.showTab; window.showTab = function (name) { const r = showTab.apply(this, arguments); if (!going) { const cur = current && byId[current]; if (!cur || cur.panel !== name) { const id = resolve(name); if (id) { if (byId[id].view) applyView(byId[id]); setCurrent(id); } } } return r; }; } // ---- the URL hash: #books, #news, #audiobooks … (old ones like #feeds, #radio/saved too) let routing = false; function syncHash() { if (routing) return; let hash = current; if (current === 'radio') hash += '/' + (load('diora_active_radio_tab') || 'saved'); if (location.hash.slice(1) !== hash) history.pushState(null, '', '#' + hash); } function route() { const parts = (location.hash || '').replace(/^#/, '').split('/').filter(Boolean); if (!parts.length || !resolve(parts[0])) return false; routing = true; try { go(parts[0]); if (parts[0] === 'radio' && parts[1] && typeof showRadioTab === 'function') showRadioTab(parts[1]); } finally { routing = false; } return true; } // At the start (99-init.js): the hash, else the section used last; offline the books. function restore(offline) { if (offline) { go('books'); return; } if (route()) return; const saved = load('diora_nav_section') || load('diora_active_tab') || 'radio'; if (!go(saved)) go(sections()[0] ? sections()[0].id : 'radio'); } // ---- the web page's bar and the segment row -------------------------------------------- function esc(s) { return String(s).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c])); } function button(id, label, cls, on) { return ''; } let tabs = null; let segs = null; function render() { const cur = current && byId[current]; const gs = groups(); if (tabs && !nativeUi()) { // Wide: every section under its group; narrow: the groups (their sections on top). tabs.innerHTML = '' + ''; } if (segs) { const g = cur ? gs.filter(x => x.id === cur.group)[0] : null; const show = !!g && g.sections.length > 1 && html.classList.contains('nav-bars'); segs.hidden = !show; segs.innerHTML = show ? g.sections.map(id => button(id, byId[id].label, 'nav-seg', current === id)).join('') : ''; } } function setup() { wrapShowTab(); tabs = document.getElementById('tabs'); const first = document.querySelector('section.tab-panel'); if (first) { segs = document.createElement('nav'); segs.id = 'nav-segments'; segs.hidden = true; first.parentNode.insertBefore(segs, first); segs.addEventListener('click', onClick); } if (tabs && !nativeUi()) { html.classList.add('nav-bars'); // the page's own bar knows the sections tabs.addEventListener('click', onClick); } render(); } function onClick(e) { const b = e.target.closest('[data-nav]'); if (!b) return; e.preventDefault(); go(b.dataset.nav); window.scrollTo(0, 0); } window.DioraNav = { sections, groups, current: () => current, go, onChange(fn) { if (typeof fn === 'function') listeners.push(fn); }, // For a bar drawn elsewhere (native-radio.js): it shows the sections itself. barsReady() { html.classList.add('nav-bars'); render(); }, route, restore, }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setup); else setup(); window.addEventListener('popstate', route); window.addEventListener('hashchange', route); // ---- styles ---------------------------------------------------------------------------- const style = document.createElement('style'); style.textContent = [ '#tabs .nav-wide, #tabs .nav-narrow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }', '#tabs .nav-narrow { display: none; }', '#tabs .nav-group-label { margin: 0 4px 0 14px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted, #9a9a9a); }', '@media (max-width: 720px) { #tabs .nav-wide { display: none; } #tabs .nav-narrow { display: flex; } }', '#nav-segments { display: flex; gap: 6px; overflow-x: auto; padding: 8px 0 12px; scrollbar-width: none; }', '#nav-segments[hidden] { display: none; }', '#nav-segments::-webkit-scrollbar { display: none; }', '.nav-seg { flex: none; min-height: 36px; padding: 0 14px; border-radius: 18px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }', '.nav-seg.active { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); }', // Wide web page: every section is in the bar, the row would repeat it. '@media (min-width: 721px) { html:not(.nui-radio) #nav-segments { display: none; } }', // The sidebar (apps, wide): the sections are in it. 'html.nui-sidebar #nav-segments { display: none; }', ].join('\n'); document.head.appendChild(style); })();