diff --git a/API/Controllers/BaseApiController.cs b/API/Controllers/BaseApiController.cs
index 7806ef660..bee612fa2 100644
--- a/API/Controllers/BaseApiController.cs
+++ b/API/Controllers/BaseApiController.cs
@@ -10,4 +10,8 @@ namespace API.Controllers;
[Authorize]
public class BaseApiController : ControllerBase
{
+ public BaseApiController()
+ {
+
+ }
}
diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs
index e1d7da9e8..75061889d 100644
--- a/API/Controllers/BookController.cs
+++ b/API/Controllers/BookController.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using API.Constants;
using API.Data;
using API.DTOs.Reader;
using API.Entities.Enums;
@@ -40,11 +41,13 @@ public class BookController : BaseApiController
///
///
[HttpGet("{chapterId}/book-info")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])]
public async Task> GetBookInfo(int chapterId)
{
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var bookTitle = string.Empty;
+
switch (dto.SeriesFormat)
{
case MangaFormat.Epub:
@@ -52,6 +55,7 @@ public class BookController : BaseApiController
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0];
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions);
bookTitle = book.Title;
+
break;
}
case MangaFormat.Pdf:
@@ -72,9 +76,9 @@ public class BookController : BaseApiController
break;
}
- return Ok(new BookInfoDto()
+ var info = new BookInfoDto()
{
- ChapterNumber = dto.ChapterNumber,
+ ChapterNumber = dto.ChapterNumber,
VolumeNumber = dto.VolumeNumber,
VolumeId = dto.VolumeId,
BookTitle = bookTitle,
@@ -84,7 +88,13 @@ public class BookController : BaseApiController
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
- });
+ };
+
+
+
+
+
+ return Ok(info);
}
///
@@ -157,7 +167,11 @@ public class BookController : BaseApiController
try
{
- return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
+ var ptocBookmarks =
+ await _unitOfWork.UserTableOfContentRepository.GetPersonalToCForPage(User.GetUserId(), chapterId, page);
+ var annotations = await _unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapter.Id);
+
+ return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl, ptocBookmarks, annotations));
}
catch (KavitaException ex)
{
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index 38a5ad482..41b8420aa 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -15,6 +15,7 @@ using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
+using API.Services.Tasks.Metadata;
using API.SignalR;
using Hangfire;
using Kavita.Common;
@@ -41,6 +42,7 @@ public class ReaderController : BaseApiController
private readonly IEventHub _eventHub;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
+ private readonly IBookService _bookService;
///
public ReaderController(ICacheService cacheService,
@@ -48,7 +50,8 @@ public class ReaderController : BaseApiController
IReaderService readerService, IBookmarkService bookmarkService,
IAccountService accountService, IEventHub eventHub,
IScrobblingService scrobblingService,
- ILocalizationService localizationService)
+ ILocalizationService localizationService,
+ IBookService bookService)
{
_cacheService = cacheService;
_unitOfWork = unitOfWork;
@@ -59,6 +62,7 @@ public class ReaderController : BaseApiController
_eventHub = eventHub;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
+ _bookService = bookService;
}
///
@@ -218,11 +222,10 @@ public class ReaderController : BaseApiController
/// This is generally the first call when attempting to read to allow pre-generation of assets needed for reading
///
/// Should Kavita extract pdf into images. Defaults to false.
- /// Include file dimensions. Only useful for image based reading
+ /// Include file dimensions. Only useful for image-based reading
///
[HttpGet("chapter-info")]
- [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"
- ])]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])]
public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
{
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
@@ -826,6 +829,58 @@ public class ReaderController : BaseApiController
return _readerService.GetTimeEstimate(0, pagesLeft, false);
}
+
+ ///
+ /// For the current user, returns an estimate on how long it would take to finish reading the chapter.
+ ///
+ /// For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases.
+ ///
+ ///
+ ///
+ [HttpGet("time-left-for-chapter")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "chapterId"])]
+ public async Task> GetEstimateToCompletionForChapter(int seriesId, int chapterId)
+ {
+ var userId = User.GetUserId();
+ var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
+ var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
+ if (series == null || chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
+
+ // Patch in the reading progress
+ await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter);
+
+ if (series.Format == MangaFormat.Epub)
+ {
+ // Get the word counts for all the pages
+ var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache
+ if (pageCounts == null) return _readerService.GetTimeEstimate(series.WordCount, 0, true);
+
+ // Sum character counts only for pages that have been read
+ var totalCharactersRead = pageCounts
+ .Where(kvp => kvp.Key <= chapter.PagesRead)
+ .Sum(kvp => kvp.Value);
+
+ var progressCount = WordCountAnalyzerService.GetWordCount(totalCharactersRead);
+ var wordsLeft = series.WordCount - progressCount;
+ return _readerService.GetTimeEstimate(wordsLeft, 0, true);
+ }
+
+ var pagesLeft = chapter.Pages - chapter.PagesRead;
+ return _readerService.GetTimeEstimate(0, pagesLeft, false);
+ }
+
+ ///
+ /// Returns the annotations for the given chapter
+ ///
+ ///
+ ///
+ [HttpGet("annotations")]
+ public async Task>> GetAnnotations(int chapterId)
+ {
+
+ return Ok(await _unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapterId));
+ }
+
///
/// Returns the user's personal table of contents for the given chapter
///
@@ -879,6 +934,12 @@ public class ReaderController : BaseApiController
return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark"));
}
+ // Look up the chapter this PTOC is associated with to get the chapter title (if there is one)
+ var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId);
+ if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist"));
+ var toc = await _bookService.GenerateTableOfContents(chapter);
+ var chapterTitle = BookService.GetChapterTitleFromToC(toc, dto.PageNumber);
+
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()
{
Title = dto.Title.Trim(),
@@ -887,6 +948,8 @@ public class ReaderController : BaseApiController
SeriesId = dto.SeriesId,
LibraryId = dto.LibraryId,
BookScrollId = dto.BookScrollId,
+ SelectedText = dto.SelectedText,
+ ChapterTitle = chapterTitle,
AppUserId = userId
});
await _unitOfWork.CommitAsync();
diff --git a/API/DTOs/Reader/AnnotationDto.cs b/API/DTOs/Reader/AnnotationDto.cs
new file mode 100644
index 000000000..fe0be79e1
--- /dev/null
+++ b/API/DTOs/Reader/AnnotationDto.cs
@@ -0,0 +1,46 @@
+using System;
+using API.Entities;
+using API.Entities.Enums;
+
+namespace API.DTOs.Reader;
+
+///
+/// Represents an annotation on a book
+///
+public sealed record AnnotationDto
+{
+ public int Id { get; set; }
+ ///
+ /// Starting point of the Highlight
+ ///
+ public required string XPath { get; set; }
+ ///
+ /// Ending point of the Highlight. Can be the same as
+ ///
+ public string EndingXPath { get; set; }
+
+ ///
+ /// The text selected.
+ ///
+ public string SelectedText { get; set; }
+ ///
+ /// Rich text Comment
+ ///
+ public string? Comment { get; set; }
+ ///
+ /// The number of characters selected
+ ///
+ public int HighlightCount { get; set; }
+ public bool ContainsSpoiler { get; set; }
+ public int PageNumber { get; set; }
+
+ public HightlightColor HighlightColor { get; set; }
+
+ public required int ChapterId { get; set; }
+
+ public required int OwnerUserId { get; set; }
+ public string OwnerUsername { get; set; }
+
+ public DateTime CreatedUtc { get; set; }
+ public DateTime LastModifiedUtc { get; set; }
+}
diff --git a/API/DTOs/Reader/CreateAnnotationRequest.cs b/API/DTOs/Reader/CreateAnnotationRequest.cs
new file mode 100644
index 000000000..676f90ed7
--- /dev/null
+++ b/API/DTOs/Reader/CreateAnnotationRequest.cs
@@ -0,0 +1,35 @@
+using API.Entities.Enums;
+
+namespace API.DTOs.Reader;
+
+public sealed record CreateAnnotationRequest
+{
+ public int Id { get; set; }
+ ///
+ /// Starting point of the Highlight
+ ///
+ public required string XPath { get; set; }
+ ///
+ /// Ending point of the Highlight. Can be the same as
+ ///
+ public string EndingXPath { get; set; }
+
+ ///
+ /// The text selected.
+ ///
+ public string SelectedText { get; set; }
+ ///
+ /// Rich text Comment
+ ///
+ public string? Comment { get; set; }
+ ///
+ /// The number of characters selected
+ ///
+ public int HighlightCount { get; set; }
+ public bool ContainsSpoiler { get; set; }
+ public int PageNumber { get; set; }
+
+ public HightlightColor HighlightColor { get; set; }
+
+ public required int ChapterId { get; set; }
+}
diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs
index 95272ca58..545e17e47 100644
--- a/API/DTOs/Reader/CreatePersonalToCDto.cs
+++ b/API/DTOs/Reader/CreatePersonalToCDto.cs
@@ -10,4 +10,5 @@ public sealed record CreatePersonalToCDto
public required int PageNumber { get; set; }
public required string Title { get; set; }
public string? BookScrollId { get; set; }
+ public string? SelectedText { get; set; }
}
diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/API/DTOs/Reader/PersonalToCDto.cs
index c979d9d78..66994a7ff 100644
--- a/API/DTOs/Reader/PersonalToCDto.cs
+++ b/API/DTOs/Reader/PersonalToCDto.cs
@@ -4,8 +4,27 @@
public sealed record PersonalToCDto
{
+ public required int Id { get; init; }
public required int ChapterId { get; set; }
+ ///
+ /// The page to bookmark
+ ///
public required int PageNumber { get; set; }
+ ///
+ /// The title of the bookmark. Defaults to Page {PageNumber} if not set
+ ///
public required string Title { get; set; }
+ ///
+ /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page
+ ///
public string? BookScrollId { get; set; }
+ ///
+ /// Text of the bookmark
+ ///
+ public string? SelectedText { get; set; }
+ ///
+ /// Title of the Chapter this PToC was created in
+ ///
+ /// Taken from the ToC
+ public string? ChapterTitle { get; set; }
}
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 7d529b1da..98c7d6980 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -80,6 +80,7 @@ public sealed class DataContext : IdentityDbContext MetadataFieldMapping { get; set; } = null!;
public DbSet AppUserChapterRating { get; set; } = null!;
public DbSet AppUserReadingProfiles { get; set; } = null!;
+ public DbSet AppUserAnnotation { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
{
diff --git a/API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs b/API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs
new file mode 100644
index 000000000..f3c10a534
--- /dev/null
+++ b/API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs
@@ -0,0 +1,3814 @@
+//
+using System;
+using System.Collections.Generic;
+using API.Data;
+using API.Entities.MetadataMatching;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20250708204811_BookAnnotations")]
+ partial class BookAnnotations
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRestriction")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRestrictionIncludeUnknowns")
+ .HasColumnType("INTEGER");
+
+ b.Property("AniListAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("ConfirmationToken")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("HasRunScrobbleEventGeneration")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LastActiveUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("MalAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("MalUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ScrobbleEventGenerationRan")
+ .HasColumnType("TEXT");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserAnnotation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("ContainsSpoiler")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("EndingXPath")
+ .HasColumnType("TEXT");
+
+ b.Property("HighlightColor")
+ .HasColumnType("INTEGER");
+
+ b.Property("HighlightCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("PageNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("SelectedText")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("XPath")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("AppUserAnnotation");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Page")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserBookmark");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserChapterRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HasBeenRated")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("REAL");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserChapterRating");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastSyncUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("MissingSeriesFromSource")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("PrimaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecondaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("Source")
+ .HasColumnType("INTEGER");
+
+ b.Property("SourceUrl")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalSourceCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserCollection");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsProvided")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartFilterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(4);
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SmartFilterId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("AppUserDashboardStream");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserExternalSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Host")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserExternalSource");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserOnDeckRemoval");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowAutomaticWebtoonReaderDetection")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("AniListScrobblingEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundColor")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("#000000");
+
+ b.Property("BlurUnreadSummaries")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderImmersiveMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderWritingStyle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("CollapseSeriesRelationships")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("Locale")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("en");
+
+ b.Property("NoTransitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfScrollMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfSpreadMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfTheme")
+ .HasColumnType("INTEGER");
+
+ b.Property("PromptForDownloadSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShareReviews")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("SwipeToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThemeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WantToReadSync")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ b.HasIndex("ThemeId");
+
+ b.ToTable("AppUserPreferences");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HasBeenRated")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("REAL");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserRating");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserReadingProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowAutomaticWebtoonReaderDetection")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundColor")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("#000000");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderImmersiveMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderWritingStyle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("DisableWidthOverride")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryIds")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfScrollMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfSpreadMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfTheme")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesIds")
+ .HasColumnType("TEXT");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("SwipeToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("WidthOverride")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserReadingProfiles");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserSideNavStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalSourceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsProvided")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartFilterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(5);
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SmartFilterId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("AppUserSideNavStream");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserSmartFilter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Filter")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserSmartFilter");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("SelectedText")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserTableOfContent");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserWantToRead", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserWantToRead");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRatingLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("AlternateSeries")
+ .HasColumnType("TEXT");
+
+ b.Property("AverageExternalRating")
+ .HasColumnType("REAL");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("REAL");
+
+ b.Property("CharacterLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ColoristLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverArtistLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("EditorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property