Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts, Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen (DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben. - Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books. - Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“). - Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann. - Ein Link auf einen Abschnitt (#news …) schlägt die Startseite. Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls daraus; bis dahin bleibt das hier unveröffentlicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
5.6 KiB
JavaScript
153 lines
5.6 KiB
JavaScript
'use strict';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Init
|
|
// ---------------------------------------------------------------------------
|
|
|
|
(function init() {
|
|
// Migrate PBKDF2-derived key stored by login/register form
|
|
if (window.USER_ID) {
|
|
const pending = localStorage.getItem('diora_pending_enc_key');
|
|
if (pending) {
|
|
localStorage.setItem(`diora_enc_key_${window.USER_ID}`, pending);
|
|
localStorage.removeItem('diora_pending_enc_key');
|
|
}
|
|
}
|
|
|
|
// Populate saved stations from server-side context if available
|
|
if (typeof INITIAL_SAVED !== 'undefined' && Array.isArray(INITIAL_SAVED)) {
|
|
// The server already renders saved stations in the template; nothing extra needed.
|
|
// But if JS-rendered saved tab were needed we'd call addSavedRow here.
|
|
}
|
|
|
|
// Restore persisted volume, fall back to slider default
|
|
const volSlider = $('volume');
|
|
if (volSlider) {
|
|
const saved = localStorage.getItem('diora_volume');
|
|
const vol = saved !== null ? parseInt(saved, 10) : parseInt(volSlider.value, 10);
|
|
setVolume(vol);
|
|
}
|
|
|
|
// Restore persisted read-aloud language
|
|
const ttsLangSelect = $('reader-tts-lang');
|
|
if (ttsLangSelect) ttsLangSelect.value = ttsLang;
|
|
|
|
// Load recommendations on page load
|
|
loadRecommendations();
|
|
|
|
// Initialise focus timer display
|
|
renderTimer();
|
|
|
|
// Initialise mood/genre chips
|
|
initMoodChips();
|
|
|
|
// Initialise curated station lists
|
|
initCuratedLists();
|
|
|
|
// Show curated lists again when search input is cleared
|
|
const searchInput = document.getElementById('search-input');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function () {
|
|
if (this.value === '') {
|
|
const curated = document.getElementById('curated-lists');
|
|
if (curated) curated.style.display = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load focus session stats
|
|
loadFocusStats();
|
|
|
|
// Apply encrypted wallpaper (if set)
|
|
applyEncryptedBackground();
|
|
|
|
// Init book drop zone
|
|
initBookDropZone();
|
|
const showReadToggle = $('book-show-read-toggle');
|
|
if (showReadToggle) showReadToggle.checked = _bookShowRead;
|
|
|
|
// Restore active tab: a URL hash (shared/bookmarked link, or browser
|
|
// back/forward) wins over the last-used tab remembered in localStorage —
|
|
// except when offline, where books (usable offline) always takes over.
|
|
const offlineOverride = !navigator.onLine && IS_AUTHENTICATED;
|
|
const hashTab = (location.hash || '').replace(/^#/, '').split('/')[0];
|
|
if (window.DioraNav) { // sections (static/js/nav.js)
|
|
DioraNav.restore(offlineOverride);
|
|
showRadioTab(localStorage.getItem('diora_active_radio_tab') || 'saved');
|
|
} else if (!offlineOverride && TOP_TABS.includes(hashTab)) {
|
|
_routeFromHash();
|
|
} else {
|
|
const savedTab = localStorage.getItem('diora_active_tab') || 'radio';
|
|
const savedRadioTab = localStorage.getItem('diora_active_radio_tab') || 'saved';
|
|
showTab(offlineOverride ? 'books' : savedTab);
|
|
showRadioTab(savedRadioTab);
|
|
}
|
|
|
|
// React to connectivity changes
|
|
window.addEventListener('offline', () => {
|
|
showOfflineBanner(true);
|
|
if (IS_AUTHENTICATED) showTab('books');
|
|
});
|
|
window.addEventListener('online', async () => {
|
|
showOfflineBanner(false);
|
|
await _syncPendingAnnotations();
|
|
loadBookList(); // sends the positions read offline first (the progress outbox)
|
|
});
|
|
|
|
// Show banner immediately if already offline at startup
|
|
if (!navigator.onLine) showOfflineBanner(true);
|
|
|
|
// Reading position: positions reached by the reader's own doing count (see _noteReadingPosition).
|
|
{
|
|
const content = $('reader-content');
|
|
if (content) {
|
|
for (const type of ['wheel', 'touchstart', 'mousedown']) {
|
|
content.addEventListener(type, _noteReaderInteraction, {passive: true, capture: true});
|
|
}
|
|
}
|
|
document.addEventListener('keydown', () => { if (currentBookId) _noteReaderInteraction(); }, true);
|
|
if (IS_AUTHENTICATED) _flushProgressOutbox();
|
|
}
|
|
|
|
// Footnote hover (desktop) + tap (mobile) via event delegation
|
|
const _readerContentEl = $('reader-content');
|
|
if (_readerContentEl) {
|
|
_readerContentEl.addEventListener('mouseover', e => {
|
|
const link = e.target.closest('[data-footnote-ref]');
|
|
if (!link) return;
|
|
clearTimeout(_footnotePopoverTimer);
|
|
_footnotePopoverTimer = setTimeout(() => showFootnotePopover(link), 120);
|
|
});
|
|
_readerContentEl.addEventListener('mouseout', e => {
|
|
if (!e.target.closest('[data-footnote-ref]')) return;
|
|
_footnotePopoverTimer = setTimeout(dismissFootnotePopover, 250);
|
|
});
|
|
_readerContentEl.addEventListener('click', e => {
|
|
const link = e.target.closest('[data-footnote-ref]');
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
if (currentFootnotePopover?._refLink === link) {
|
|
dismissFootnotePopover();
|
|
} else {
|
|
showFootnotePopover(link);
|
|
}
|
|
});
|
|
|
|
// Text selection → highlight color/note popover
|
|
_readerContentEl.addEventListener('mouseup', handleReaderSelection);
|
|
_readerContentEl.addEventListener('touchend', e => {
|
|
const target = e.target;
|
|
const touch = e.changedTouches && e.changedTouches[0];
|
|
const clientX = touch ? touch.clientX : undefined;
|
|
const clientY = touch ? touch.clientY : undefined;
|
|
setTimeout(() => handleReaderSelection({target, clientX, clientY}), 50);
|
|
});
|
|
}
|
|
|
|
// Margin ("page margin") panel — click a marker to view/edit, click empty
|
|
// space to anchor a new freeform note at that line's height
|
|
const _readerMarginEl = $('reader-margin');
|
|
if (_readerMarginEl) {
|
|
_readerMarginEl.addEventListener('click', handleMarginClick);
|
|
}
|
|
})();
|