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
|
|
|
import base64
|
|
|
|
|
import json
|
2026-04-01 15:41:30 +02:00
|
|
|
import re
|
2026-08-17 11:27:16 +02:00
|
|
|
import xml.etree.ElementTree as ET
|
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
|
|
|
|
2026-08-17 11:27:16 +02:00
|
|
|
import requests
|
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.conf import settings
|
|
|
|
|
from django.http import JsonResponse
|
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
|
|
|
|
|
|
from .models import EBook, EBookProgress, EBookHighlights, EBookBookmarks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_auth(request):
|
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 11:27:16 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Library-shelf metadata lookup (DNB, falling back to Open Library)
|
|
|
|
|
#
|
|
|
|
|
# Only ever called on explicit user request (the "Metadaten" book action), never
|
|
|
|
|
# automatically — the server briefly sees the plaintext ISBN for this one proxied
|
|
|
|
|
# request, which is a deliberate, narrow exception to the "server never sees book
|
|
|
|
|
# content" rule (see CLAUDE.md), made because the encryption's real purpose here is
|
|
|
|
|
# to keep the operator from being able to see what's on the platform (piracy
|
|
|
|
|
# liability), not strict user privacy — an ISBN lookup against public library
|
|
|
|
|
# catalogs doesn't undermine that. Nothing from this lookup is persisted server-side;
|
|
|
|
|
# the resulting label is stored only in the client's encrypted meta blob.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# DNB "Sachgruppen der Deutschen Nationalbibliografie" / DDC divisions (hundreds -> tens),
|
|
|
|
|
# used to turn a raw DDC notation like "833.92" into a short shelf label.
|
|
|
|
|
_DDC_DIVISIONS = {
|
|
|
|
|
'000': 'Allgemeines, Informatik', '010': 'Bibliografien', '020': 'Bibliotheks- und Informationswissenschaft',
|
|
|
|
|
'030': 'Enzyklopädien', '050': 'Zeitschriften, fortlaufende Sammelwerke', '060': 'Organisationen, Museumswissenschaft',
|
|
|
|
|
'070': 'Nachrichtenmedien, Journalismus, Verlagswesen', '090': 'Handschriften, seltene Bücher',
|
|
|
|
|
'100': 'Philosophie', '130': 'Parapsychologie, Okkultismus', '150': 'Psychologie',
|
|
|
|
|
'200': 'Religion', '230': 'Christentum, Christliche Theologie', '290': 'Andere Religionen',
|
|
|
|
|
'300': 'Sozialwissenschaften, Soziologie', '310': 'Statistiken', '320': 'Politikwissenschaft',
|
|
|
|
|
'330': 'Wirtschaft', '340': 'Recht', '350': 'Öffentliche Verwaltung, Militärwissenschaft',
|
|
|
|
|
'360': 'Soziale Probleme, Sozialdienste, Versicherungen', '370': 'Erziehung, Schul- und Bildungswesen',
|
|
|
|
|
'380': 'Handel, Kommunikation, Verkehr', '390': 'Gebräuche, Etikette, Folklore',
|
|
|
|
|
'400': 'Sprache, Linguistik', '420': 'Englisch', '430': 'Deutsch, Germanische Sprachen',
|
|
|
|
|
'440': 'Französisch, Romanische Sprachen', '450': 'Italienisch, Rumänisch, Rätoromanisch',
|
|
|
|
|
'460': 'Spanisch, Portugiesisch', '470': 'Latein, Italische Sprachen', '480': 'Griechisch', '490': 'Andere Sprachen',
|
|
|
|
|
'500': 'Naturwissenschaften', '510': 'Mathematik', '520': 'Astronomie', '530': 'Physik', '540': 'Chemie',
|
|
|
|
|
'550': 'Geowissenschaften', '560': 'Paläontologie', '570': 'Biowissenschaften, Biologie',
|
|
|
|
|
'580': 'Pflanzen (Botanik)', '590': 'Tiere (Zoologie)',
|
|
|
|
|
'600': 'Technik', '610': 'Medizin, Gesundheit', '620': 'Ingenieurwissenschaften', '630': 'Landwirtschaft',
|
|
|
|
|
'640': 'Hauswirtschaft', '650': 'Management', '660': 'Chemische Technik', '670': 'Industrielle Fertigung',
|
|
|
|
|
'680': 'Fertigung für spezielle Zwecke', '690': 'Hausbau, Bauhandwerk',
|
|
|
|
|
'700': 'Künste', '710': 'Landschaftsgestaltung, Raumplanung', '720': 'Architektur',
|
|
|
|
|
'730': 'Plastik, Keramik, Metallkunst', '740': 'Zeichnung, angewandte Kunst', '750': 'Malerei',
|
|
|
|
|
'760': 'Grafik, Druckgrafik, Fotografie', '780': 'Musik', '790': 'Freizeit, Darstellende Kunst, Sport',
|
|
|
|
|
'800': 'Literatur', '810': 'Amerikanische Literatur', '820': 'Englische Literatur',
|
|
|
|
|
'830': 'Deutsche Literatur', '840': 'Französische Literatur', '850': 'Italienische Literatur',
|
|
|
|
|
'860': 'Spanische, Portugiesische Literatur', '870': 'Lateinische Literatur', '880': 'Griechische Literatur',
|
|
|
|
|
'890': 'Literaturen in anderen Sprachen',
|
|
|
|
|
'900': 'Geschichte', '910': 'Geografie, Reisen', '920': 'Biografie, Genealogie',
|
|
|
|
|
'930': 'Geschichte des Altertums', '940': 'Geschichte Europas', '950': 'Geschichte Asiens',
|
|
|
|
|
'960': 'Geschichte Afrikas', '970': 'Geschichte Nordamerikas', '980': 'Geschichte Südamerikas',
|
|
|
|
|
'990': 'Geschichte der übrigen Welt',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ddc_label(ddc_raw):
|
|
|
|
|
"""Coarsen a DDC notation ('833.92') down to its nearest known division/class label."""
|
|
|
|
|
digits = re.sub(r'\D', '', ddc_raw or '')
|
|
|
|
|
if len(digits) < 3:
|
|
|
|
|
return None
|
|
|
|
|
tens = digits[:2] + '0'
|
|
|
|
|
if tens in _DDC_DIVISIONS:
|
|
|
|
|
return _DDC_DIVISIONS[tens]
|
|
|
|
|
return _DDC_DIVISIONS.get(digits[0] + '00')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_MARC_NS = '{http://www.loc.gov/MARC21/slim}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lookup_dnb_shelf(isbn):
|
|
|
|
|
url = 'https://services.dnb.de/sru/dnb'
|
|
|
|
|
params = {
|
|
|
|
|
'version': '1.1',
|
|
|
|
|
'operation': 'searchRetrieve',
|
|
|
|
|
'query': f'dnb.num={isbn}',
|
|
|
|
|
'recordSchema': 'MARC21-xml',
|
|
|
|
|
'maximumRecords': '1',
|
|
|
|
|
}
|
|
|
|
|
resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6))
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
root = ET.fromstring(resp.content)
|
|
|
|
|
for datafield in root.iter(f'{_MARC_NS}datafield'):
|
|
|
|
|
if datafield.get('tag') != '082':
|
|
|
|
|
continue
|
|
|
|
|
for subfield in datafield.findall(f'{_MARC_NS}subfield'):
|
|
|
|
|
if subfield.get('code') == 'a' and subfield.text:
|
|
|
|
|
label = _ddc_label(subfield.text)
|
|
|
|
|
if label:
|
|
|
|
|
return label
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lookup_openlibrary_shelf(isbn):
|
|
|
|
|
url = 'https://openlibrary.org/api/books'
|
|
|
|
|
params = {'bibkeys': f'ISBN:{isbn}', 'jscmd': 'data', 'format': 'json'}
|
|
|
|
|
resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6))
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
data = resp.json().get(f'ISBN:{isbn}', {})
|
|
|
|
|
ddc = (data.get('classifications') or {}).get('dewey_decimal_class') or []
|
|
|
|
|
if ddc:
|
|
|
|
|
label = _ddc_label(ddc[0])
|
|
|
|
|
if label:
|
|
|
|
|
return label
|
|
|
|
|
subjects = data.get('subjects') or []
|
|
|
|
|
if subjects:
|
|
|
|
|
return subjects[0].get('name')
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(['GET'])
|
|
|
|
|
def lookup_book_metadata(request):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
isbn = re.sub(r'[^0-9Xx]', '', request.GET.get('isbn', ''))
|
|
|
|
|
if len(isbn) not in (10, 13):
|
|
|
|
|
return JsonResponse({'error': 'invalid ISBN'}, status=400)
|
|
|
|
|
|
|
|
|
|
label = None
|
|
|
|
|
source = None
|
|
|
|
|
try:
|
|
|
|
|
label = _lookup_dnb_shelf(isbn)
|
|
|
|
|
source = 'dnb' if label else None
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
if not label:
|
|
|
|
|
try:
|
|
|
|
|
label = _lookup_openlibrary_shelf(isbn)
|
|
|
|
|
source = 'openlibrary' if label else None
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return JsonResponse({'label': label, 'source': source})
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 20:52:27 +02:00
|
|
|
def _anchor_parts(anchor):
|
|
|
|
|
"""Split a position anchor 'blockIndex:innerFraction' into (block, inner).
|
|
|
|
|
|
|
|
|
|
Returns (-1, 0.0) for empty/invalid anchors (e.g. PDF progress)."""
|
|
|
|
|
if not isinstance(anchor, str) or ':' not in anchor:
|
|
|
|
|
return (-1, 0.0)
|
|
|
|
|
block, _, inner = anchor.partition(':')
|
|
|
|
|
try:
|
|
|
|
|
return (int(block), float(inner))
|
|
|
|
|
except ValueError:
|
|
|
|
|
return (-1, 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _progress_is_further(new_anchor, new_frac, old_anchor, old_frac):
|
|
|
|
|
"""True if the new reading position is at least as far into the book as the
|
|
|
|
|
old one. Compares by anchor block index (precise, decoupled from layout);
|
|
|
|
|
falls back to scroll_fraction only when an anchor is missing (PDFs)."""
|
|
|
|
|
nb, ni = _anchor_parts(new_anchor)
|
|
|
|
|
ob, oi = _anchor_parts(old_anchor)
|
|
|
|
|
if nb >= 0 and ob >= 0:
|
|
|
|
|
return ni >= oi if nb == ob else nb >= ob
|
|
|
|
|
return new_frac >= old_frac
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
@require_http_methods(['GET'])
|
|
|
|
|
def book_list(request):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
books = list(
|
2026-08-04 18:10:33 +02:00
|
|
|
request.user.ebooks.values('id', 'meta_ct', 'meta_iv', 'uploaded_at', 'is_read')
|
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
|
|
|
)
|
|
|
|
|
for b in books:
|
|
|
|
|
b['uploaded_at'] = b['uploaded_at'].isoformat()
|
|
|
|
|
# Include saved scroll_fraction for each book
|
|
|
|
|
progress_map = {
|
2026-04-01 15:41:30 +02:00
|
|
|
p.book_id: (p.scroll_fraction, p.updated_at, p.position_anchor)
|
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
|
|
|
for p in EBookProgress.objects.filter(user=request.user)
|
|
|
|
|
}
|
2026-08-04 18:10:33 +02:00
|
|
|
highlighted_ids = set(
|
|
|
|
|
EBookHighlights.objects.filter(user=request.user).values_list('book_id', flat=True)
|
|
|
|
|
)
|
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
|
|
|
for b in books:
|
2026-03-20 20:41:13 +01:00
|
|
|
prog = progress_map.get(b['id'])
|
|
|
|
|
b['scroll_fraction'] = prog[0] if prog else 0.0
|
|
|
|
|
b['last_read'] = prog[1].isoformat() if prog else None
|
2026-04-01 15:41:30 +02:00
|
|
|
b['position_anchor'] = prog[2] if prog else ''
|
2026-08-04 18:10:33 +02:00
|
|
|
b['has_highlights'] = b['id'] in highlighted_ids
|
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
|
|
|
return JsonResponse(books, safe=False)
|
|
|
|
|
|
|
|
|
|
|
2026-08-04 18:10:33 +02:00
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def set_book_read(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
book.is_read = bool(body.get('is_read', True))
|
|
|
|
|
book.save(update_fields=['is_read'])
|
|
|
|
|
return JsonResponse({'ok': True, 'is_read': book.is_read})
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 10:58:53 +02:00
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def update_book_meta(request, pk):
|
|
|
|
|
"""Update only the encrypted metadata blob (e.g. to assign a folder) without touching book bytes."""
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
meta_ct = body.get('meta_ct', '')
|
|
|
|
|
meta_iv = body.get('meta_iv', '')
|
|
|
|
|
if not meta_ct or not meta_iv:
|
|
|
|
|
return JsonResponse({'error': 'meta_ct, meta_iv required'}, status=400)
|
|
|
|
|
|
|
|
|
|
book.meta_ct = meta_ct
|
|
|
|
|
book.meta_iv = meta_iv
|
|
|
|
|
book.save(update_fields=['meta_ct', 'meta_iv'])
|
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def upload_book(request):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
meta_ct = body.get('meta_ct', '')
|
|
|
|
|
meta_iv = body.get('meta_iv', '')
|
|
|
|
|
data_ct = body.get('data_ct', '')
|
|
|
|
|
data_iv = body.get('data_iv', '')
|
|
|
|
|
|
|
|
|
|
if not all([meta_ct, meta_iv, data_ct, data_iv]):
|
|
|
|
|
return JsonResponse({'error': 'meta_ct, meta_iv, data_ct, data_iv required'}, status=400)
|
|
|
|
|
|
|
|
|
|
# Enforce size limit: ciphertext is plaintext + 16-byte GCM tag
|
|
|
|
|
max_bytes = getattr(settings, 'EBOOK_MAX_BYTES', 10 * 1024 * 1024) + 32
|
|
|
|
|
try:
|
|
|
|
|
raw_size = len(base64.b64decode(data_ct))
|
|
|
|
|
except Exception:
|
|
|
|
|
return JsonResponse({'error': 'invalid base64 in data_ct'}, status=400)
|
|
|
|
|
|
|
|
|
|
if raw_size > max_bytes:
|
2026-04-04 21:01:32 +02:00
|
|
|
return JsonResponse({'error': 'file too large (max 50 MB)'}, status=400)
|
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
|
|
|
|
|
|
|
|
book = EBook.objects.create(
|
|
|
|
|
user=request.user,
|
|
|
|
|
meta_ct=meta_ct,
|
|
|
|
|
meta_iv=meta_iv,
|
|
|
|
|
data_ct=data_ct,
|
|
|
|
|
data_iv=data_iv,
|
|
|
|
|
)
|
2026-04-04 21:01:32 +02:00
|
|
|
EBookProgress.objects.create(user=request.user, book=book)
|
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
|
|
|
return JsonResponse({'ok': True, 'id': book.id})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(['GET'])
|
|
|
|
|
def get_book_data(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
return JsonResponse({'data_ct': book.data_ct, 'data_iv': book.data_iv})
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 17:03:59 +02:00
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def replace_book_data(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
2026-05-25 17:06:52 +02:00
|
|
|
data_ct = body.get('data_ct', '')
|
|
|
|
|
data_iv = body.get('data_iv', '')
|
|
|
|
|
meta_ct = body.get('meta_ct', '')
|
|
|
|
|
meta_iv = body.get('meta_iv', '')
|
2026-05-25 17:03:59 +02:00
|
|
|
|
|
|
|
|
if not all([data_ct, data_iv]):
|
|
|
|
|
return JsonResponse({'error': 'data_ct and data_iv required'}, status=400)
|
|
|
|
|
|
2026-05-25 17:06:52 +02:00
|
|
|
update_fields = ['data_ct', 'data_iv']
|
2026-05-25 17:03:59 +02:00
|
|
|
book.data_ct = data_ct
|
|
|
|
|
book.data_iv = data_iv
|
2026-05-25 17:06:52 +02:00
|
|
|
if meta_ct and meta_iv:
|
|
|
|
|
book.meta_ct = meta_ct
|
|
|
|
|
book.meta_iv = meta_iv
|
|
|
|
|
update_fields += ['meta_ct', 'meta_iv']
|
|
|
|
|
book.save(update_fields=update_fields)
|
2026-05-25 17:03:59 +02:00
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 16:37:41 +02:00
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def rekey_book(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
meta_ct = body.get('meta_ct', '')
|
|
|
|
|
meta_iv = body.get('meta_iv', '')
|
|
|
|
|
data_ct = body.get('data_ct', '')
|
|
|
|
|
data_iv = body.get('data_iv', '')
|
|
|
|
|
|
|
|
|
|
if not all([meta_ct, meta_iv, data_ct, data_iv]):
|
|
|
|
|
return JsonResponse({'error': 'meta_ct, meta_iv, data_ct, data_iv required'}, status=400)
|
|
|
|
|
|
|
|
|
|
book.meta_ct = meta_ct
|
|
|
|
|
book.meta_iv = meta_iv
|
|
|
|
|
book.data_ct = data_ct
|
|
|
|
|
book.data_iv = data_iv
|
|
|
|
|
book.save(update_fields=['meta_ct', 'meta_iv', 'data_ct', 'data_iv'])
|
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def delete_book(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
book.delete()
|
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['POST'])
|
|
|
|
|
def save_progress(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
scroll_fraction = float(body.get('scroll_fraction', 0.0))
|
|
|
|
|
scroll_fraction = max(0.0, min(1.0, scroll_fraction))
|
|
|
|
|
|
2026-04-01 15:41:30 +02:00
|
|
|
raw_anchor = body.get('position_anchor', '')
|
|
|
|
|
position_anchor = ''
|
|
|
|
|
if isinstance(raw_anchor, str) and re.fullmatch(r'\d{1,7}:\d(\.\d{1,6})?', raw_anchor):
|
|
|
|
|
position_anchor = raw_anchor
|
|
|
|
|
|
2026-05-31 15:41:35 +02:00
|
|
|
force = bool(body.get('force', 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
|
|
|
progress, _ = EBookProgress.objects.get_or_create(
|
|
|
|
|
user=request.user,
|
|
|
|
|
book=book,
|
|
|
|
|
)
|
2026-07-10 20:52:27 +02:00
|
|
|
# Always advance to the furthest-read position (by anchor block index, so a
|
|
|
|
|
# transiently wrong scroll_fraction can't freeze the position) unless the
|
|
|
|
|
# client explicitly forces a reset (e.g. "start over" button in the reader).
|
|
|
|
|
if force or _progress_is_further(position_anchor, scroll_fraction,
|
|
|
|
|
progress.position_anchor, progress.scroll_fraction):
|
2026-05-31 15:41:35 +02:00
|
|
|
progress.scroll_fraction = scroll_fraction
|
|
|
|
|
progress.position_anchor = position_anchor
|
2026-04-01 15:41:30 +02:00
|
|
|
progress.save(update_fields=['scroll_fraction', 'position_anchor', 'updated_at'])
|
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
|
|
|
|
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['GET', 'POST'])
|
|
|
|
|
def book_highlights(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
try:
|
|
|
|
|
row = EBookHighlights.objects.get(user=request.user, book=book)
|
|
|
|
|
return JsonResponse({'ct': row.ct, 'iv': row.iv})
|
|
|
|
|
except EBookHighlights.DoesNotExist:
|
|
|
|
|
return JsonResponse({'ct': None, 'iv': None})
|
|
|
|
|
|
|
|
|
|
# POST — upsert
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
ct = body.get('ct', '')
|
|
|
|
|
iv = body.get('iv', '')
|
|
|
|
|
if not ct or not iv:
|
|
|
|
|
return JsonResponse({'error': 'ct and iv required'}, status=400)
|
|
|
|
|
|
|
|
|
|
# Size guard: highlights ≤ 700 KB base64
|
|
|
|
|
try:
|
|
|
|
|
raw_size = len(base64.b64decode(ct))
|
|
|
|
|
except Exception:
|
|
|
|
|
return JsonResponse({'error': 'invalid base64 in ct'}, status=400)
|
2026-04-04 21:10:14 +02:00
|
|
|
if raw_size > getattr(settings, 'HIGHLIGHTS_MAX_BYTES', 700 * 1024):
|
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
|
|
|
return JsonResponse({'error': 'highlights data too large (max 700 KB)'}, status=400)
|
|
|
|
|
|
|
|
|
|
row, _ = EBookHighlights.objects.get_or_create(user=request.user, book=book)
|
|
|
|
|
row.ct = ct
|
|
|
|
|
row.iv = iv
|
|
|
|
|
row.save(update_fields=['ct', 'iv', 'updated_at'])
|
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
|
@require_http_methods(['GET', 'POST'])
|
|
|
|
|
def book_bookmarks(request, pk):
|
|
|
|
|
err = _require_auth(request)
|
|
|
|
|
if err:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
book = EBook.objects.get(pk=pk, user=request.user)
|
|
|
|
|
except EBook.DoesNotExist:
|
|
|
|
|
return JsonResponse({'error': 'not found'}, status=404)
|
|
|
|
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
try:
|
|
|
|
|
row = EBookBookmarks.objects.get(user=request.user, book=book)
|
|
|
|
|
return JsonResponse({'ct': row.ct, 'iv': row.iv})
|
|
|
|
|
except EBookBookmarks.DoesNotExist:
|
|
|
|
|
return JsonResponse({'ct': None, 'iv': None})
|
|
|
|
|
|
|
|
|
|
# POST — upsert
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(request.body)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
|
|
|
|
|
|
ct = body.get('ct', '')
|
|
|
|
|
iv = body.get('iv', '')
|
|
|
|
|
if not ct or not iv:
|
|
|
|
|
return JsonResponse({'error': 'ct and iv required'}, status=400)
|
|
|
|
|
|
|
|
|
|
# Size guard: bookmarks ≤ 100 KB base64
|
|
|
|
|
try:
|
|
|
|
|
raw_size = len(base64.b64decode(ct))
|
|
|
|
|
except Exception:
|
|
|
|
|
return JsonResponse({'error': 'invalid base64 in ct'}, status=400)
|
2026-04-04 21:10:14 +02:00
|
|
|
if raw_size > getattr(settings, 'BOOKMARKS_MAX_BYTES', 100 * 1024):
|
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
|
|
|
return JsonResponse({'error': 'bookmarks data too large (max 100 KB)'}, status=400)
|
|
|
|
|
|
|
|
|
|
row, _ = EBookBookmarks.objects.get_or_create(user=request.user, book=book)
|
|
|
|
|
row.ct = ct
|
|
|
|
|
row.iv = iv
|
|
|
|
|
row.save(update_fields=['ct', 'iv', 'updated_at'])
|
|
|
|
|
return JsonResponse({'ok': True})
|