diff --git a/static/css/app.css b/static/css/app.css index bbf8d38..3eb712e 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1486,6 +1486,20 @@ body.dnd-mode .timer-display { padding: 4px 0 8px; } +.offline-banner { + position: fixed; + top: var(--nav-h); + left: 0; + right: 0; + background: var(--accent); + color: #fff; + text-align: center; + font-size: 12px; + padding: 4px 8px; + z-index: 250; + pointer-events: none; +} + .book-key-bar { display: flex; align-items: center; diff --git a/static/js/app.js b/static/js/app.js index 5068fff..48909d3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1039,6 +1039,25 @@ function editNotes(pk, current) { const TOP_TABS = ['radio', 'focus', 'podcasts', 'books']; const RADIO_SUB_TABS = ['search', 'saved', 'history']; +function showOfflineBanner(offline) { + let banner = $('offline-banner'); + if (offline) { + if (!banner) { + banner = document.createElement('div'); + banner.id = 'offline-banner'; + banner.className = 'offline-banner'; + banner.textContent = 'Offline — nur gecachte Bücher verfügbar'; + document.body.prepend(banner); + } + const uploadArea = $('book-upload-area'); + if (uploadArea) uploadArea.style.display = 'none'; + } else { + if (banner) banner.remove(); + const uploadArea = $('book-upload-area'); + if (uploadArea) uploadArea.style.display = ''; + } +} + function showTab(name) { TOP_TABS.forEach(p => { const panel = $(`tab-${p}`); @@ -3380,7 +3399,19 @@ async function openBook(bookId) { if (cached) { ({data_ct, data_iv} = cached); } else { - const res = await fetch(`/books/${bookId}/data/`); + let res; + try { + res = await fetch(`/books/${bookId}/data/`); + } catch { + // offline and book not cached yet + overlay.style.display = 'none'; + const toast = document.createElement('div'); + toast.className = 'reader-toast'; + toast.textContent = 'Buch nicht offline verfügbar — einmal online öffnen zum Cachen.'; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 3500); + return; + } ({data_ct, data_iv} = await res.json()); _setCachedBook(bookId, data_ct, data_iv); // fire-and-forget } @@ -5053,12 +5084,25 @@ function openRadioSidebar() { // Init book drop zone initBookDropZone(); - // Restore last active tab + // Restore last active tab — when offline, always go to books (most useful) const savedTab = localStorage.getItem('diora_active_tab') || 'radio'; const savedRadioTab = localStorage.getItem('diora_active_radio_tab') || 'saved'; - showTab(savedTab); + showTab(!navigator.onLine && IS_AUTHENTICATED ? 'books' : savedTab); showRadioTab(savedRadioTab); + // React to connectivity changes + window.addEventListener('offline', () => { + showOfflineBanner(true); + if (IS_AUTHENTICATED) showTab('books'); + }); + window.addEventListener('online', () => { + showOfflineBanner(false); + loadBookList(); // sync any queued changes + }); + + // Show banner immediately if already offline at startup + if (!navigator.onLine) showOfflineBanner(true); + // Hourly background feed refresh (only when authenticated) if (IS_AUTHENTICATED) { setInterval(refreshAllFeeds, 60 * 60 * 1000);