Books: offline UX — Banner, Auto-Tab, klare Fehlermeldung
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 16s

- Bei Offline-Start oder Wechsel in Flugzeugmodus: roter Banner
  unterhalb der Navbar, automatischer Wechsel zum Books-Tab (einziger
  sinnvoller Tab offline), Upload-Zone wird ausgeblendet
- Bei Rückkehr online: Banner verschwindet, Upload-Zone erscheint wieder,
  Buchliste wird neu synchronisiert
- openBook unterscheidet jetzt zwischen "nicht gecacht" (Toast-Hinweis
  zum einmaligen Online-Öffnen) und echtem Defekt (markBookBroken wie
  bisher) — kein stilles Scheitern mehr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-05-30 00:12:22 +02:00
parent a96591c63f
commit aa562f8b4d
2 changed files with 61 additions and 3 deletions

View file

@ -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;

View file

@ -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);