From 83ac8bd7334cf81750cfd98cdc7bc49a465a4bd3 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Tue, 13 Dec 2022 12:00:54 -0600 Subject: [PATCH] Reader Refactor Part 2 (#1694) * Updated swiper and some packages for reported security issues * Fixed reading lists promotion not working * Refactor RenameFileForCopy to use iterative recursion, rather than functional. * Ensured that bookmarks are fetched and ordered by Created date. * Fixed a bug where bookmarks were coming back in the correct order, but due to filenames, would not sort correctly. * Default installs to Debug log level given errors users have and Debug not being too noisy * Added jumpbar to bookmarks page * Now added jumpbar to bookmarks * Refactored some code into pipes and added some debug messaging for prefetcher * Try loading next and prev chapter's first/last page to cache so it renders faster * Updated GetImage to do a bound check on max page. Fixed a critical bug in how manga reader updates image elements src to prefetch/load pages. I was not creating a new reference which broke Angular's ability to update DOM on changes. * Refactored the image setting code to use a single method which tries to use a cached image always. * Refactored code to use getPage which favors cache and simplifies image creation code * Started the work to split the canvas renderer into it's own component * Refactored a lot of common methods into a service for the reader to support the upcoming renderer split * Moved components to nested folder. Refactored more code to streamline image sending to child renderer. Added notes across the code to help streamline flow of data and who owns what. * Swapped out SQLite for Memory, but the one from hangfire. Added DisableConcurrentExecution on ProcessChange to avoid duplication when multiple threads execute at once. * Basic split right to left is working with canvas renderer * Left to right and right to left now work * Fixed a bug where pagesplitoption wasn't being updated when modifying menu * Canvas rendering still has a bug with switching between right to left -> left to right on the re-render, it will choose a bad state. All else works fine with it. * Updated canvas renderer to implement the ImageRenderer interface * Canvas renderer is done * Setup single renderer. Need to figure out how to share CSS between renderers and also share some global stuff, like image height. * Refactored code so that image-container is within the renderers themselves. Still broken in scaling, but working towards a solution. * Added double click to shortcut menu * Moved image containers within the renderers * Pushing up for Robbie * nothing new * Move common css to a single scss file * More css consolidation * Fixed a npe in isWideImage * Refactored page updates to renderers to include max pages. Rewrote most of renderer into observables. * Moved bookmark for second page to double renderer * Started hooking in double renderer renderPage() * Fixed height scaling, but now canvas renderer is broken again * Fixed a bug with canvas renderer not moving to next page. Streamlined the code for getting page amounts from the dfferent renderers * Added double click to bookmark for canvas * Stashing the code and taking a break * Nothing much, buffer is still broken * Got double renderer to render at least one page * Double renderer now has access to 5 images at any time, so it can make appropriate decisions on when to render double pages. * Fixed up double rendererer moving backward page calc * Forward logic seems to be working * Cleaned up dead code after testing * Moved a few loggers in folder watching to trace * Everything seems to work fine, time to do double manga renderer * Moved some css around and added the reverse double component * Only execute renderer's pipes when in the correct mode * Still working on double renderer * Fixed scaling issues on double * Updating double logic - Fixed: Fixed an issue where a second page would render when current page was wide. * Hooked up double renderer * Made changes but not sure if im making progress * double manga fixes * Claned some of robbies code * Fixing last page bug * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Fixed GA (#1664) * Bump versions by dotnet-bump-version. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Fixed typeahead and updated manga reader to new layout structure * Fixed book reader fonts lookups * Fixed up some build issues * Fixed a bad import of css image * Some cleanup and rewrote how we log out data. * Renderer can be null on first load when performing some work. * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Post merge cleanup * Again moving the file * Fixed an issue with switching to double renderer and the image not loading for cover image. * Fixed double manga last page repeating twice * Added ability to quickly save a few settings to user preferences from manga reader * Fixed up some success messaging * Single image and canvas could stack, last page on double wouldn't render. * Stashing code, want to work on something else * Suppress a concurrency issue when opening a fresh chapter to read. * Refactored a function into a pipe * Took care of one TODO * Tightened up the logic around single renderer handling fit to screen images. * Added some code to see how long api takes on average. * First pass integration of page dimensions into single renderer and base code * Canvas renderer pass for new page dimensions * On time left, don't use the word left again * Moved the page dimension code into manga service to make it seemless * Hooked in a replacement for image based isWide * Canvas renderer is working again * Double renderer now follows how Komga does it to keep it simple. * Double renderer is working really well so far. * don't use nbsp * Added response caching to file-dimensions and chapter info api * Allow chapter info to send back file dimensions optionally * Fixed an issue with dimensions api locking files on Windows * Refactored all code to use isWidePage * More fixes and cleanup * More double reverse logic * Recently Read stats page will allow you to click the items. Signed-off-by: dependabot[bot] Co-authored-by: Robbie Davis Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API/API.csproj | 6 +- API/Controllers/ReaderController.cs | 6 +- API/Controllers/UsersController.cs | 1 + API/DTOs/Reader/ChapterInfoDto.cs | 2 + API/DTOs/UserPreferencesDto.cs | 5 + ...20221212215914_EmulateBookPref.Designer.cs | 1705 +++++++++++++++++ .../20221212215914_EmulateBookPref.cs | 26 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Entities/AppUserPreferences.cs | 4 + API/Services/ArchiveService.cs | 1 + API/Services/CacheService.cs | 5 +- API/Services/ReaderService.cs | 9 +- UI/Web/src/_manga-reader-common.scss | 29 + .../app/_models/preferences/preferences.ts | 1 + .../src/app/_services/message-hub.service.ts | 7 - UI/Web/src/app/_services/reader.service.ts | 9 +- .../directory-picker.component.html | 2 +- .../manage-email-settings.component.html | 2 +- .../cards/card-item/card-item.component.html | 4 +- .../series-info-cards.component.html | 2 +- .../canvas-renderer.component.html | 2 +- .../canvas-renderer.component.scss | 3 +- .../canvas-renderer.component.ts | 24 +- .../double-renderer.component.html | 4 +- .../double-renderer.component.scss | 1 + .../double-renderer.component.ts | 175 +- .../double-reverse-renderer.component.html | 6 +- .../double-reverse-renderer.component.ts | 317 +-- .../infinite-scroller.component.ts | 2 - .../manga-reader/manga-reader.component.html | 43 +- .../manga-reader/manga-reader.component.scss | 60 - .../manga-reader/manga-reader.component.ts | 239 +-- .../single-renderer.component.html | 22 +- .../single-renderer.component.ts | 52 +- .../app/manga-reader/_models/chapter-info.ts | 5 + .../manga-reader/_models/file-dimension.ts | 4 +- .../manga-reader/_models/reader-setting.ts | 1 + .../src/app/manga-reader/_models/renderer.ts | 2 +- .../manga-reader/_pipes/fitting-icon.pipe.ts | 21 + .../_series/managa-reader.service.ts | 60 +- .../app/manga-reader/manga-reader.module.ts | 2 + .../src/app/manga-reader/swipe.directive.ts | 4 +- .../series-detail.component.html | 4 +- .../library-settings-modal.component.html | 3 + .../server-stats/server-stats.component.html | 2 +- .../server-stats/server-stats.component.ts | 11 +- .../stat-list/stat-list.component.html | 2 +- .../stat-list/stat-list.component.scss | 5 + .../stat-list/stat-list.component.ts | 10 + .../user-preferences.component.html | 14 +- .../user-preferences.component.ts | 6 +- openapi.json | 27 +- 52 files changed, 2261 insertions(+), 701 deletions(-) create mode 100644 API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs create mode 100644 API/Data/Migrations/20221212215914_EmulateBookPref.cs create mode 100644 UI/Web/src/app/manga-reader/_pipes/fitting-icon.pipe.ts diff --git a/API/API.csproj b/API/API.csproj index 652e37a6c..f1b6b43b7 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -8,10 +8,10 @@ true true - + - - + + false diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index b020a3450..6322718e6 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -161,6 +161,7 @@ public class ReaderController : BaseApiController /// /// [HttpGet("file-dimensions")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf"})] public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) { if (chapterId <= 0) return null; @@ -174,9 +175,11 @@ public class ReaderController : BaseApiController /// /// /// Should Kavita extract pdf into images. Defaults to false. + /// Include file dimensions. Only useful for image based reading /// [HttpGet("chapter-info")] - public async Task> GetChapterInfo(int chapterId, bool extractPdf = false) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})] + public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) { if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore var chapter = await _cacheService.Ensure(chapterId, extractPdf); @@ -201,6 +204,7 @@ public class ReaderController : BaseApiController ChapterTitle = dto.ChapterTitle ?? string.Empty, Subtitle = string.Empty, Title = dto.SeriesName, + PageDimensions = _cacheService.GetCachedFileDimensions(chapterId) }; if (info.ChapterTitle is {Length: > 0}) { diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 6636bf519..1738ea539 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -85,6 +85,7 @@ public class UsersController : BaseApiController existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu; existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints; + existingPreferences.EmulateBook = preferencesDto.EmulateBook; existingPreferences.ReaderMode = preferencesDto.ReaderMode; existingPreferences.LayoutMode = preferencesDto.LayoutMode; existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor; diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 842add713..6945c2b24 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -66,4 +66,6 @@ public class ChapterInfoDto : IChapterInfoDto /// Usually just series name, but can include chapter title public string Title { get; set; } + public IEnumerable PageDimensions { get; set; } + } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 6e5d51442..5667f1634 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -37,6 +37,11 @@ public class UserPreferencesDto [Required] public LayoutMode LayoutMode { get; set; } /// + /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages + /// + [Required] + public bool EmulateBook { get; set; } + /// /// Manga Reader Option: Background color of the reader /// [Required] diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs b/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs new file mode 100644 index 000000000..431307ba2 --- /dev/null +++ b/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs @@ -0,0 +1,1705 @@ +// +using System; +using API.Data; +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("20221212215914_EmulateBookPref")] + partial class EmulateBookPref + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + 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("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .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("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.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("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + 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("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + 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("LastModified") + .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("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + 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.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInDashboard") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInSearch") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Devices"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.cs b/API/Data/Migrations/20221212215914_EmulateBookPref.cs new file mode 100644 index 000000000..d2883ba0c --- /dev/null +++ b/API/Data/Migrations/20221212215914_EmulateBookPref.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class EmulateBookPref : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EmulateBook", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EmulateBook", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index a7b1cccb8..c474690ce 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -217,6 +217,9 @@ namespace API.Data.Migrations .HasColumnType("TEXT") .HasDefaultValue("Dark"); + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + b.Property("GlobalPageLayoutMode") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index f29ede382..9e7b65df7 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -36,6 +36,10 @@ public class AppUserPreferences /// public bool ShowScreenHints { get; set; } = true; /// + /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages + /// + public bool EmulateBook { get; set; } = false; + /// /// Manga Reader Option: How many pages to display in the reader at once /// public LayoutMode LayoutMode { get; set; } = LayoutMode.Single; diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index a547dc36d..25c0a1db8 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -409,6 +409,7 @@ public class ArchiveService : IArchiveService private void ExtractArchiveEntities(IEnumerable entries, string extractPath) { _directoryService.ExistOrCreate(extractPath); + // TODO: Look into a Parallel.ForEach foreach (var entry in entries) { entry.WriteToDirectory(extractPath, new ExtractionOptions() diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 5ef4184d3..fcfe6655d 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -60,6 +61,7 @@ public class CacheService : ICacheService public IEnumerable GetCachedFileDimensions(int chapterId) { + var sw = Stopwatch.StartNew(); var path = GetCachePath(chapterId); var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) @@ -74,7 +76,7 @@ public class CacheService : ICacheService for (var i = 0; i < files.Length; i++) { var file = files[i]; - using var image = Image.NewFromStream(File.OpenRead(file), access: Enums.Access.SequentialUnbuffered); + using var image = Image.NewFromFile(file, memory:false, access: Enums.Access.SequentialUnbuffered); dimensions.Add(new FileDimensionDto() { PageNumber = i, @@ -84,6 +86,7 @@ public class CacheService : ICacheService }); } + _logger.LogDebug("File Dimensions call for {Length} images took {Time}ms", dimensions.Count, sw.ElapsedMilliseconds); return dimensions; } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index c14a0425e..453aa77b6 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -225,6 +225,7 @@ public class ReaderService : IReaderService try { + // TODO: Rewrite this code to just pull user object with progress for that particiular appuserprogress, else create it var userProgress = await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId); @@ -241,8 +242,7 @@ public class ReaderService : IReaderService SeriesId = progressDto.SeriesId, ChapterId = progressDto.ChapterId, LibraryId = progressDto.LibraryId, - BookScrollId = progressDto.BookScrollId, - LastModified = DateTime.Now + BookScrollId = progressDto.BookScrollId }); _unitOfWork.UserRepository.Update(userWithProgress); } @@ -253,7 +253,6 @@ public class ReaderService : IReaderService userProgress.VolumeId = progressDto.VolumeId; userProgress.LibraryId = progressDto.LibraryId; userProgress.BookScrollId = progressDto.BookScrollId; - userProgress.LastModified = DateTime.Now; _unitOfWork.AppUserProgressRepository.Update(userProgress); } @@ -267,6 +266,10 @@ public class ReaderService : IReaderService } catch (Exception exception) { + // This can happen when the reader sends 2 events at same time, so 2 threads are inserting and one fails. + if (exception.Message.StartsWith( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s)")) + return true; _logger.LogError(exception, "Could not save progress"); await _unitOfWork.RollbackAsync(); } diff --git a/UI/Web/src/_manga-reader-common.scss b/UI/Web/src/_manga-reader-common.scss index e6ff3aac8..403787e70 100644 --- a/UI/Web/src/_manga-reader-common.scss +++ b/UI/Web/src/_manga-reader-common.scss @@ -45,6 +45,10 @@ img { vertical-align: top; max-width: fit-content; } + + .fit-to-screen.full-width { + max-height: calc(var(--vh)*100); + } } @@ -70,4 +74,29 @@ img { background-color: var(--manga-reader-prev-highlight-bg-color) !important; animation: fadein .5s both; backdrop-filter: blur(10px); +} + + +::ng-deep .image-container.book-shadow.center-double:before { + content: ''; + position: absolute; + top: 0; + left: 50%; + height: 100%; + box-shadow: + 0px 0px calc(17px*3.14) 25px rgb(0 0 0 / 43%), + 0px 0px calc(2px*3.14) 2px rgb(0 0 0 / 43%), + 0px 0px calc(5px*3.14) 4px rgb(0 0 0 / 43%), + 0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%); +} + +@supports (-moz-appearance:none) { + ::ng-deep .image-container.book-shadow.center-double:before { + box-shadow: + 0px 0px calc(17px*3.14) 25px rgb(0 0 0 / 43%), + 0px 0px calc(2px*3.14) 2px rgb(0 0 0 / 43%), + 0px 0px calc(5px*3.14) 4px rgb(0 0 0 / 43%), + 0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%), + 0px 0px 1px 0.5px rgb(0 0 0 / 43%); + } } \ No newline at end of file diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 674dd88ca..a053ac447 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -18,6 +18,7 @@ export interface Preferences { layoutMode: LayoutMode; backgroundColor: string; showScreenHints: boolean; + emulateBook: boolean; // Book Reader bookReaderMargin: number; diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index d454f3866..635425336 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -146,13 +146,6 @@ export class MessageHubService { this.onlineUsersSource.next(usernames); }); - this.hubConnection.on("LogObject", resp => { - console.log(resp); - }); - this.hubConnection.on("LogString", resp => { - console.log(resp); - }); - this.hubConnection.on(EVENTS.ScanSeries, resp => { this.messagesSource.next({ event: EVENTS.ScanSeries, diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index a3a5ed8d2..e9fa4aa65 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -103,8 +103,8 @@ export class ReaderService { return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey); } - getChapterInfo(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId); + getChapterInfo(chapterId: number, includeDimensions = false) { + return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&includeDimensions=' + includeDimensions); } getFileDimensions(chapterId: number) { @@ -194,6 +194,11 @@ export class ReaderService { return parseInt(imageSrc.split('&page=')[1], 10); } + imageUrlToChapterId(imageSrc: string) { + if (imageSrc === undefined || imageSrc === '') { return -1; } + return parseInt(imageSrc.split('chapterId=')[1].split('&')[0], 10); + } + getNextChapterUrl(url: string, nextChapterId: number, incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { const lastSlashIndex = url.lastIndexOf('/'); let newRoute = url.substring(0, lastSlashIndex + 1) + nextChapterId + ''; diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html index 758922079..94e47bae1 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html @@ -30,7 +30,7 @@ -