diff --git a/books/views.py b/books/views.py
index c1322ae..a36797f 100644
--- a/books/views.py
+++ b/books/views.py
@@ -204,12 +204,21 @@ def save_progress(request, pk):
if isinstance(raw_anchor, str) and re.fullmatch(r'\d{1,7}:\d(\.\d{1,6})?', raw_anchor):
position_anchor = raw_anchor
+ force = bool(body.get('force', False))
+
progress, _ = EBookProgress.objects.get_or_create(
user=request.user,
book=book,
)
- progress.scroll_fraction = scroll_fraction
- progress.position_anchor = position_anchor
+ # Always advance to the furthest-read position unless the client
+ # explicitly forces a reset (e.g. "start over" button in the reader).
+ if force:
+ progress.scroll_fraction = scroll_fraction
+ progress.position_anchor = position_anchor
+ else:
+ if scroll_fraction >= progress.scroll_fraction:
+ progress.scroll_fraction = scroll_fraction
+ progress.position_anchor = position_anchor
progress.save(update_fields=['scroll_fraction', 'position_anchor', 'updated_at'])
return JsonResponse({'ok': True})
diff --git a/static/js/app.js b/static/js/app.js
index 9ad6f4e..95d13cc 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -2788,29 +2788,10 @@ async function _saveBookMeta(books) {
try {
const db = await _openBookCacheDb();
const ids = new Set(books.map(b => String(b.id)));
- // Only load existing entries when there are books with pending offline progress
- const pendingIds = new Set(
- JSON.parse(localStorage.getItem(_PENDING_PROGRESS_KEY) || '[]').map(String)
- );
- const existing = pendingIds.size > 0 ? await new Promise(resolve => {
- const req = db.transaction(_BOOK_META_STORE).objectStore(_BOOK_META_STORE).getAll();
- req.onsuccess = e => resolve(Object.fromEntries((e.target.result || []).map(b => [b.id, b])));
- req.onerror = () => resolve({});
- }) : {};
await new Promise(resolve => {
const tx = db.transaction(_BOOK_META_STORE, 'readwrite');
const store = tx.objectStore(_BOOK_META_STORE);
- for (const b of books) {
- if (pendingIds.has(String(b.id))) {
- const loc = existing[b.id];
- if (loc) {
- // Keep local progress until it has been pushed to the server
- store.put({...b, scroll_fraction: loc.scroll_fraction, position_anchor: loc.position_anchor, last_read: loc.last_read});
- continue;
- }
- }
- store.put(b);
- }
+ for (const b of books) store.put(b);
store.openCursor().onsuccess = e => {
const cursor = e.target.result;
if (!cursor) return;
@@ -2879,8 +2860,7 @@ async function _loadAnnotationsCache(bookId, type) {
} catch { return null; }
}
-const _PENDING_SYNC_KEY = 'diora_pending_sync';
-const _PENDING_PROGRESS_KEY = 'diora_pending_progress';
+const _PENDING_SYNC_KEY = 'diora_pending_sync';
function _markPendingSync(bookId, type) {
try {
@@ -2917,36 +2897,6 @@ async function _syncPendingAnnotations() {
} catch {}
}
-function _markPendingProgress(bookId) {
- try {
- const pending = new Set(JSON.parse(localStorage.getItem(_PENDING_PROGRESS_KEY) || '[]').map(String));
- pending.add(String(bookId));
- localStorage.setItem(_PENDING_PROGRESS_KEY, JSON.stringify([...pending]));
- } catch {}
-}
-
-async function _syncPendingProgress() {
- try {
- const pending = JSON.parse(localStorage.getItem(_PENDING_PROGRESS_KEY) || '[]');
- if (!pending.length) return;
- const localMeta = await _loadCachedBookMeta();
- const localById = Object.fromEntries(localMeta.map(b => [b.id, b]));
- const remaining = [];
- for (const bookId of pending) {
- const loc = localById[bookId];
- if (!loc) continue;
- try {
- await fetch(`/books/${bookId}/progress/`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
- body: JSON.stringify({scroll_fraction: loc.scroll_fraction, position_anchor: loc.position_anchor || ''}),
- });
- } catch { remaining.push(bookId); }
- }
- localStorage.setItem(_PENDING_PROGRESS_KEY, JSON.stringify(remaining));
- } catch {}
-}
-
// Reader settings
let readerSettings = { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark',
fontFamily: 'serif', noBold: false,
@@ -3032,20 +2982,21 @@ async function loadBookList() {
decrypted.push({id: b.id, title: `Book #${b.id}`, author: '', type: 'epub', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false});
}
}
- // Use local progress for books that have unsaved offline changes
- const _pendingIds = new Set(JSON.parse(localStorage.getItem(_PENDING_PROGRESS_KEY) || '[]').map(String));
- if (_pendingIds.size > 0) {
- const _localMeta = await _loadCachedBookMeta();
- const _localById = Object.fromEntries(_localMeta.map(b => [b.id, b]));
- for (const b of decrypted) {
- if (_pendingIds.has(String(b.id))) {
- const loc = _localById[b.id];
- if (loc) {
- b.scroll_fraction = loc.scroll_fraction;
- b.position_anchor = loc.position_anchor;
- b.last_read = loc.last_read;
- }
- }
+ // If local cache is further ahead than the server, push it and use it for display.
+ // Server uses max-semantics so this is always safe.
+ const _localMeta = await _loadCachedBookMeta();
+ const _localById = Object.fromEntries(_localMeta.map(b => [b.id, b]));
+ for (const b of decrypted) {
+ const loc = _localById[b.id];
+ if (loc && loc.scroll_fraction > b.scroll_fraction) {
+ fetch(`/books/${b.id}/progress/`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
+ body: JSON.stringify({scroll_fraction: loc.scroll_fraction, position_anchor: loc.position_anchor || ''}),
+ }).catch(() => {});
+ b.scroll_fraction = loc.scroll_fraction;
+ b.position_anchor = loc.position_anchor;
+ b.last_read = loc.last_read;
}
}
@@ -3761,7 +3712,7 @@ async function deleteBook(bookId) {
} catch (e) {}
}
-async function saveReaderProgress() {
+async function saveReaderProgress(force = false) {
if (!currentBookId) return;
const contentEl = $('reader-content');
if (!contentEl) return;
@@ -3780,24 +3731,25 @@ async function saveReaderProgress() {
_currentPositionAnchor = anchor;
}
- // Always update local cache so offline re-opens land at the right position
_updateCachedBookProgress(currentBookId, fraction, anchor);
- // Cache for sendBeacon on unload
- _lastProgressBeacon = {
- url: `/books/${currentBookId}/progress/`,
- body: JSON.stringify({scroll_fraction: fraction, position_anchor: anchor}),
- };
+ const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...(force && {force: true})});
+ _lastProgressBeacon = {url: `/books/${currentBookId}/progress/`, body};
try {
await fetch(`/books/${currentBookId}/progress/`, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
- body: _lastProgressBeacon.body,
+ body,
});
- } catch (e) {
- if (!navigator.onLine) _markPendingProgress(currentBookId);
- }
+ if (force) {
+ const toast = document.createElement('div');
+ toast.className = 'reader-toast';
+ toast.textContent = 'Position gesetzt';
+ document.body.appendChild(toast);
+ setTimeout(() => toast.remove(), 1800);
+ }
+ } catch (e) {}
}
function closeReader() {
@@ -5202,8 +5154,7 @@ function openRadioSidebar() {
window.addEventListener('online', async () => {
showOfflineBanner(false);
await _syncPendingAnnotations();
- await _syncPendingProgress();
- loadBookList();
+ loadBookList(); // loadBookList pushes any locally-advanced progress automatically
});
// Show banner immediately if already offline at startup
diff --git a/static/js/sw.js b/static/js/sw.js
index 9d6b9c6..2860d89 100644
--- a/static/js/sw.js
+++ b/static/js/sw.js
@@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use.
*/
-const CACHE = 'diora-v13';
+const CACHE = 'diora-v14';
const PODCAST_CACHE = 'diora-podcast-v1';
const SHELL = [
'/static/css/app.css',
diff --git a/templates/radio/player.html b/templates/radio/player.html
index 79d6d24..76fba67 100644
--- a/templates/radio/player.html
+++ b/templates/radio/player.html
@@ -338,11 +338,12 @@
-
-
-
-
-
+
+
+
+
+
+