Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
|
|
|
from django.db import models
|
|
|
|
|
from django.contrib.auth.models import User
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EBook(models.Model):
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebooks')
|
|
|
|
|
meta_ct = models.TextField() # base64 AES-GCM ciphertext of {title, author, filename}
|
|
|
|
|
meta_iv = models.CharField(max_length=32) # hex IV for metadata
|
|
|
|
|
data_ct = models.TextField() # base64 AES-GCM ciphertext of raw EPUB bytes
|
|
|
|
|
data_iv = models.CharField(max_length=32) # hex IV for EPUB data
|
|
|
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
2026-08-04 18:10:33 +02:00
|
|
|
is_read = models.BooleanField(default=False)
|
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ['uploaded_at']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"EBook #{self.pk} (user={self.user_id})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EBookProgress(models.Model):
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
|
|
|
"""Where to continue a book: the place it was last read, on any device
|
|
|
|
|
(docs/progress-sync.md), plus the furthest place ever reached."""
|
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_progress')
|
|
|
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='progress')
|
|
|
|
|
scroll_fraction = models.FloatField(default=0.0)
|
2026-04-01 15:41:30 +02:00
|
|
|
position_anchor = models.CharField(max_length=30, blank=True, default='')
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
|
|
|
# When the reader was at that place (ms since epoch, server clock); null before 2026-10-12.
|
|
|
|
|
read_at = models.BigIntegerField(null=True, blank=True)
|
|
|
|
|
device = models.CharField(max_length=40, blank=True, default='')
|
|
|
|
|
furthest_fraction = models.FloatField(default=0.0)
|
|
|
|
|
furthest_anchor = models.CharField(max_length=30, blank=True, default='')
|
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'book')
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"Progress book={self.book_id} user={self.user_id} {self.scroll_fraction:.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EBookHighlights(models.Model):
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_highlights')
|
|
|
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='highlights')
|
|
|
|
|
ct = models.TextField() # base64 AES-GCM ciphertext of JSON array
|
|
|
|
|
iv = models.CharField(max_length=32) # hex IV
|
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'book')
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"Highlights book={self.book_id} user={self.user_id}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EBookBookmarks(models.Model):
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_bookmarks')
|
|
|
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='bookmarks')
|
|
|
|
|
ct = models.TextField()
|
|
|
|
|
iv = models.CharField(max_length=32)
|
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'book')
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"Bookmarks book={self.book_id} user={self.user_id}"
|