Archived
feat: customizable mobile reading experience
- Backend: Chapter, ReadingProgress, ReadingSettings models - Backend: Chapter API (TOC + content), progress tracking, settings CRUD - Frontend: ReadingPage with chapter navigation - Frontend: TableOfContents drawer - Frontend: ReadingSettingsPanel (theme, font, size, orientation) - Frontend: Custom hooks for settings, chapters, progress tracking - CSS: Mobile-first reading view with sepia/dark/light/paper themes - Route: /reader/:bookId reading view from book detail page - Docs: 001-customizable-mobile-reading-experience.md
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.books.models import Book
|
||||
from apps.books.serializers import BookListSerializer, BookSerializer
|
||||
from apps.books.models import Book, Chapter, ReadingProgress
|
||||
from apps.books.serializers import (
|
||||
BookListSerializer,
|
||||
BookSerializer,
|
||||
ChapterDetailSerializer,
|
||||
ChapterSummarySerializer,
|
||||
ReadingProgressSerializer,
|
||||
)
|
||||
|
||||
|
||||
class BookViewSet(viewsets.ModelViewSet):
|
||||
"""CRUD for books."""
|
||||
"""CRUD for books. Includes nested chapter and progress endpoints."""
|
||||
|
||||
queryset = Book.objects.all()
|
||||
queryset = Book.objects.all().prefetch_related("chapters")
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ["author"]
|
||||
@@ -21,4 +30,46 @@ class BookViewSet(viewsets.ModelViewSet):
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list":
|
||||
return BookListSerializer
|
||||
return BookSerializer
|
||||
return BookSerializer
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def chapters(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""List all chapters for this book (TOC)."""
|
||||
book = self.get_object()
|
||||
chapters = book.chapters.all().order_by("number")
|
||||
serializer = ChapterSummarySerializer(chapters, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["get"], url_path="chapters/(?P<chapter_number>[0-9]+)")
|
||||
def chapter_detail(
|
||||
self, request: Request, pk: int | None = None, chapter_number: str | None = None
|
||||
) -> Response:
|
||||
"""Get a specific chapter with full content."""
|
||||
book = self.get_object()
|
||||
try:
|
||||
chapter = book.chapters.get(number=int(chapter_number))
|
||||
except Chapter.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "Chapter not found."}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
serializer = ChapterDetailSerializer(chapter)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["get", "put"])
|
||||
def progress(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""Get or update reading progress for this book."""
|
||||
book = self.get_object()
|
||||
progress, created = ReadingProgress.objects.get_or_create(
|
||||
user=request.user, book=book
|
||||
)
|
||||
|
||||
if request.method == "GET":
|
||||
serializer = ReadingProgressSerializer(progress)
|
||||
return Response(serializer.data)
|
||||
|
||||
serializer = ReadingProgressSerializer(
|
||||
progress, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user