`).join('');
}
box.querySelectorAll('a.lt').forEach(a => a.addEventListener('click', closeSheet));
};
draw(''); el.querySelector('#lnq').oninput = e => draw(e.target.value); setTimeout(() => { if (document.activeElement && document.activeElement.id === 'lnq') document.activeElement.blur(); }, 0); // (no keyboard popping up until you tap the box)
el.querySelector('#lnpin').onclick = () => { closeSheet(); openPinsSheet(); };
}
/** Choose which entries sit on the phone's bottom bar (up to five, in the order you tap them). */
function openPinsSheet() {
const tabs = navTabs(); let pins = navPins();
const el = openSheet('Bottom bar', 'Tap up to five apps for the bar, in the order you want them. Everything else stays under More.', '');
const draw = () => { el.querySelector('#pnl').innerHTML = tabs.map(([id, label]) => { const at = pins.indexOf(id); return ``; }).join('');
el.querySelectorAll('#pnl .row').forEach(b => b.onclick = () => { const id = b.dataset.id; if (pins.includes(id)) pins = pins.filter(x => x !== id); else if (pins.length >= 5) return toast('The bar has room for five'); else pins.push(id); draw(); }); };
draw();
el.querySelector('#pnr').onclick = () => { pins = DEFAULT_PINS.filter(id => tabs.some(t => t[0] === id)); draw(); };
el.querySelector('#pns').onclick = () => { store.set('nav_pins', pins.length ? pins : DEFAULT_PINS); closeSheet(); renderNav(); toast('Bottom bar saved'); };
}
/** Where a "Listen" or "Read" tap goes: the part you used last. */
const lastSub = g => { let l = null; try { l = JSON.parse(localStorage.getItem(nk('nav_last_' + g))); } catch {} return MERGED[g][1].includes(l) && allowed(l) ? l : MERGED[g][1].find(allowed); };
/** The row of buttons under the top bar that switches between the parts of Listen and Read. */
function drawSubnav(name) {
const el = $('#subnav'); if (!el) return; const g = leafOf(name), items = g ? MERGED[g][1].filter(allowed) : [];
if (!g || items.length < 2) { el.hidden = true; el.innerHTML = ''; return; }
try { localStorage.setItem(nk('nav_last_' + g), JSON.stringify(name)); } catch {}
el.innerHTML = items.map(id => `${esc(tabLabel(id))}`).join(''); el.hidden = false;
}
const navIdOf = name => name === 'edit' ? 'files' : (name === 'tasks' || name === 'calendar') ? 'planner' : leafOf(name) || name;
let curRoute = '';
const routesOf = () => ({ video: viewVideo, planner: viewPlanner, home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: args => args[0] === 'classic' ? viewAudio() : args[0] === 'free' ? viewFreeBooks() : viewBooks(args), verse: () => location.replace('#/links'), podcast: args => args[0] === 'vbv' ? viewVbv(args.slice(1)) : viewPodcast(args), radio: viewRadio, bible: args => viewBible(args), listen: () => location.replace('#/' + lastSub('listen')), reading: () => location.replace('#/' + lastSub('reading')), read: args => viewReader(args), music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, signal: viewSignal, links: viewLinks, settings: viewSettings, edit: viewEdit });
let editor;
// Menu on a tablet or computer: collapse it to icons (wide screens), or hide it completely. Remembered on this device.
const navPref = () => { try { return JSON.parse(localStorage.getItem('nav_pref')) || {}; } catch { return {}; } };
function applyNav() { const p = navPref(); document.body.classList.toggle('nav-mini', !!p.mini); document.body.classList.toggle('nav-hidden', !!p.hidden); }
function setNav(patch) { try { localStorage.setItem('nav_pref', JSON.stringify({ ...navPref(), ...patch })); } catch {} applyNav(); }
applyNav();
addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'b' && !/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName || '')) { e.preventDefault(); setNav({ hidden: !navPref().hidden }); } });
// In-app Back / Forward (an installed app has no browser buttons). Each history entry remembers its position so we know what is possible.
let navIdx = 0, navMax = 0, navStarted = false;
const parentHash = () => { const p = location.hash.split('/'); return p.length > 3 ? p.slice(0, -1).join('/') : ''; }; // e.g. #/files/lib/a/b -> #/files/lib/a
function trackNav() {
const s = history.state;
if (s && s.i != null) navIdx = s.i;
else { navIdx = navStarted ? navIdx + 1 : 0; navMax = Math.max(navMax, navIdx); if (navStarted) navMax = navIdx; try { history.replaceState({ ...(s || {}), i: navIdx }, ''); } catch {} }
navStarted = true; navMax = Math.max(navMax, navIdx);
}
function drawNav() {
const el = $('#navbar'); if (!el) return;
const canBack = navIdx > 0 || !!parentHash(), canFwd = navIdx < navMax;
if (!el.dataset.on) {
el.dataset.on = 1;
el.innerHTML = ``;
$('#nvm').onclick = () => setNav({ hidden: !navPref().hidden });
$('#nvb').onclick = () => { if (navIdx > 0) history.back(); else if (parentHash()) location.hash = parentHash(); };
$('#nvf').onclick = () => history.forward();
}
$('#nvb').disabled = !canBack; $('#nvf').disabled = !canFwd;
const t = TABS.find(x => x[0] === (curRoute || activeTab)); $('#nvt').textContent = t ? t[1] : '';
el.hidden = false;
}
addEventListener('popstate', e => { if (e.state && e.state.i != null) { navIdx = e.state.i; drawNav(); } });
function route() {
trackNav();
const [, id = 'home', ...rest] = location.hash.split('/');
const routes = routesOf();
let name = routes[id] ? id : 'home';
if (name === 'edit' ? !(allowed('files') || allowed('office')) : !allowed(name)) { name = 'home'; if (location.hash !== '#/home') history.replaceState(null, '', '#/home'); } // not one of their tabs
try { editor?.destroyEditor(); } catch {}
editor = null;
document.body.classList.toggle('editing', name === 'edit');
document.body.classList.remove('inedit', 'onaudio');
sg.timers.forEach(clearInterval); sg.timers = []; // (Signal screens start their own)
const aw = $('#audiowrap'); if (aw) aw.className = 'off'; // hide (not destroy) the audiobooks frame
curRoute = name; navTouch(navIdOf(name)); drawSubnav(name);
renderNav(navIdOf(name));
drawNav();
routes[name](rest.map(decodeURIComponent));
sgDockSync(name);
$('#view').scrollTop = 0;
}
function tile(href, color, ic, title, sub, external) {
return `${icon(ic)}${esc(title)}${esc(sub)}`;
}
// ---- Views
/** The Home app tiles, grouped by kind like the menu. */
function homeApps() {
const T = {
files: () => tile('#/files', '#2f9e6e', 'files', 'Files', 'Seafile'),
music: () => tile('#/music', '#e5a00d', 'music', 'Music', 'Plex'),
video: () => tile('#/video', '#c0392b', 'film', 'Movies & TV', 'Plex'),
planner: () => tile('#/planner', '#d9453a', 'cal', 'Planner', allowed('calendar') && allowed('tasks') ? 'Calendar & tasks' : allowed('tasks') ? 'To-do lists' : 'Agenda'),
office: () => tile('#/office', '#e0742b', 'doc', 'Office', 'OnlyOffice'),
audio: () => tile('#/audio', '#8a5cf0', 'audio', 'Audiobooks', 'Listen'),
podcast: () => tile('#/podcast', '#c2417a', 'mic', 'Podcast', 'Podcasts'),
radio: () => tile('#/radio', '#7b4fd6', 'radio', 'Radio', 'The Journey FM & more'),
read: () => tile('#/read', '#2b8a5e', 'book', 'Books', 'EPUB, PDF, DOCX'),
signal: () => tile('#/signal', '#3a76f0', 'chat', 'Signal', sg.unread ? sg.unread + ' unread' : 'Messages'),
links: () => tile('#/links', '#0f9d9a', 'link', 'Links', allLinks().length + ' saved'),
mail: () => tile('#/mail', '#4f7cff', 'mail', 'Mail', mailUnread ? mailUnread + ' unread' : 'Outlook.com'),
};
T.bible = () => tile('#/bible', '#8b5a2b', 'bible', 'Bible', 'Berean Standard Bible');
const HOME_GROUPS = [['Work', ['planner', 'files', 'office', 'links']], ['Communicate', ['mail', 'signal']], ['Listen', ['music', 'radio', 'podcast', 'audio']], ['Read and watch', ['bible', 'read', 'video']]];
return HOME_GROUPS.map(([label, ids]) => [label, ids.filter(id => T[id] && allowed(id))]).filter(g => g[1].length).map(([label, ids]) => `
${esc(label)}
${ids.map(id => T[id]()).join('')}
`).join('');
}
function viewHome() {
const h = new Date().getHours();
const evening = h >= 17 || h < 5; // after work the media comes first; during the day the agenda and tasks do
const greet = h < 5 ? 'Good night' : h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
const recents = store.get('recents', []).slice(0, 5);
const t = typeof cur === 'function' ? cur() : null;
const today = new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
$('#view').innerHTML = `