from enum import StrEnum from pathlib import Path from django.conf import settings from django.core.validators import FileExtensionValidator from django.db import models from django.db.models.signals import post_delete from django.dispatch import receiver class FontStyle(StrEnum): SANS_SERIF = "sans-serif" SERIF = "serif" MONOSPACE = "monospace" class BackgroundColor(StrEnum): WHITE = "#ffffff" SEPIA = "#f4e4c1" DARK = "#1a1a2e" GREEN = "#c7edcc" class Book(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="books", ) title = models.CharField(max_length=512) author = models.CharField(max_length=256, blank=True, default="") file = models.FileField( upload_to="books/%Y/%m/%d/", validators=[FileExtensionValidator(allowed_extensions=["epub", "pdf"])], ) cover_image = models.ImageField(upload_to="covers/%Y/%m/%d/", blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["user", "-created_at"]), ] def __str__(self) -> str: return self.title def filename(self) -> str: return Path(self.file.name).name @receiver(post_delete, sender=Book) def _auto_delete_file_on_delete(sender: type[Book], instance: Book, **kwargs: object) -> None: """Delete the uploaded file when the Book record is deleted.""" if instance.file: instance.file.delete(save=False) if instance.cover_image: instance.cover_image.delete(save=False) class ReadingProgress(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reading_progress", ) book = models.OneToOneField( Book, on_delete=models.CASCADE, related_name="reading_progress", ) current_position = models.FloatField(default=0.0) """Position in the book as a percentage (0.0 to 100.0)""" last_page = models.IntegerField(default=0) """Last page/paragraph index for granular tracking""" updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name_plural = "reading progress" unique_together = [("user", "book")] def __str__(self) -> str: return f"{self.book.title} — {self.current_position:.1f}%" class ReadingSettings(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reading_settings", ) font_size = models.IntegerField(default=18) """Font size in pixels (min 12, max 36)""" font_style = models.CharField( max_length=20, choices=[(s.value, s.name.replace("_", " ").title()) for s in FontStyle], default=FontStyle.SANS_SERIF.value, ) background_color = models.CharField( max_length=7, choices=[(c.value, c.name.title()) for c in BackgroundColor], default=BackgroundColor.WHITE.value, ) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name_plural = "reading settings" def __str__(self) -> str: return f"Settings for {self.user}"