/*
* 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; Suche across all of them. 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},
// The search across diora (docs/search.md, search.js): beside Home · Lesen · Hören on a
// phone, at the foot of the sidebar (above Abos & Quellen) and of the wide bar ("side").
{id: 'suche', group: 'suche', label: 'Suche', icon: '⌕', panel: 'suche', account: true, side: true},
// Managing every subscription (docs/sources.md): at the foot of the sidebar, in the app
// menu on a phone — not a tab of its own.
{id: 'sources', group: 'more', label: 'Abos & Quellen', icon: '⚙', panel: 'sources', account: true, footer: true},
];
const GROUPS = [
{id: 'home', label: 'Home', icon: '⌂'},
{id: 'lesen', label: 'Lesen', icon: '▤'},
{id: 'hoeren', label: 'Hören', icon: '◉'},
{id: 'suche', label: 'Suche', icon: '⌕'},
];
const byId = {};
SECTIONS.forEach(s => { byId[s.id] = s; });
// The icons (style B: bold and geometric, 2.4 px, square ends — at home with the monospace
// type and crisp on e-ink). A 24 grid; the parts marked "f" fill in when the section is
// the one shown (.active / .nui-active / .on around the icon).
const ICONS = {
home: '',
books: '' +
'',
magazines: '' +
'',
news: '',
blogs: '',
audiobooks: '' +
'' +
'',
podcasts: '' +
'',
radio: '' +
'',
ambient: '' +
'',
focus: '',
sources: '',
suche: '',
menu: '' +
'',
};
ICONS.lesen = ICONS.books;
ICONS.hoeren = ICONS.audiobooks;
function icon(name) {
const body = ICONS[name];
return body ? '' : '';
}
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
// The apps' sidebar: a "side" section stands at its foot, not as a group.
const sidebar = () => html.classList.contains('nui-sidebar');
// 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' && s.id !== 'sources') 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, svg: icon(s.id),
footer: !!s.footer || (!!s.side && sidebar())}));
}
function groups() {
const list = sections();
return GROUPS.map(g => ({id: g.id, label: g.label, icon: g.icon, svg: icon(g.id),
sections: list.filter(s => s.group === g.id && !s.footer).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 && !s.footer);
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];
if (!s.side) save('diora_nav_section', id); // the app opens where one was, not in the search
if (!s.footer) 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();
const foot = sections().filter(s => s.footer);
if (tabs && !nativeUi()) {
// Wide: every section under its group, the "side" ones at the end with the footer's;
// narrow: the groups (their sections on top).
const side = id => !!byId[id].side;
const footWide = sections().filter(s => side(s.id)).concat(foot);
tabs.innerHTML =
'