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)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ['uploaded_at']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"EBook #{self.pk} (user={self.user_id})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EBookProgress(models.Model):
|
|
|
|
|
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='')
|
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}"
|