From 3ab3a10ae79445ebf6d0fe2ecf3d1de989adb398 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 15 Jun 2022 16:43:32 -0500 Subject: [PATCH] New PDF Reader (#1324) * Refactored all the code that opens the reader to use a unified function. Added new library and setup basic pdf reader route. * Progress saving is implemented. Targeting ES6 now. * Customized the toolbar to remove things we don't want, made the download button download with correct filename. Adjusted zoom setting to work well on first load regardless of device. * Stream the pdf file to the UI rather than handling the download ourselves. * Started implementing a custom toolbar. * Fixed up the jump bar calculations * Fixed filtering being broken * Pushing up for Robbie to cleanup the toolbar layout * Added an additional button. Working on logic while robbie takes styling * Tried to fix the code for robbie * Tweaks for fonts * Added button for book mode, but doesn't seem to work after renderer is built * Removed book mode * Removed the old image caching code for pdfs as it's not needed with new reader * Removed the interfaces to extract images from pdf. * Fixed original pagination area not scaling correctly * Integrated series remove events to library detail * Cleaned up the getter naming convention * Cleaned up some of the manga reader code to reduce cluter and improve re-use * Implemented Japanese parser support for volume and chapters. * Fixed a bug where resetting scroll in manga reader wasn't working * Fixed a bug where word count grew on each scan. * Removed unused variable * Ensure we calculate word count on files with their own cache timestamp * Adjusted size of reel headers * Put some code in for moving on original image with keyboard, but it's not in use. * Cleaned up the css for the pdf reader * Cleaned up the code * Tweaked the list item so we show scrollbar now when fully read --- API.Tests/Parser/MangaParserTests.cs | 3 + API.Tests/Services/CacheServiceTests.cs | 4 +- API/Controllers/BookController.cs | 34 +- API/Controllers/ReaderController.cs | 29 +- ...0220615190640_LastFileAnalysis.Designer.cs | 1570 +++++++++++++++++ .../20220615190640_LastFileAnalysis.cs | 27 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Data/Repositories/MangaFileRepository.cs | 27 + API/Data/UnitOfWork.cs | 2 + API/Entities/MangaFile.cs | 4 + API/Helpers/CacheHelper.cs | 20 + API/Parser/Parser.cs | 12 + API/Services/BookService.cs | 1 + API/Services/CacheService.cs | 42 +- API/Services/ReadingItemService.cs | 4 +- .../Metadata/WordCountAnalyzerService.cs | 20 +- UI/Web/angular.json | 7 +- UI/Web/e2e/tsconfig.json | 2 +- UI/Web/package-lock.json | 14 + UI/Web/package.json | 1 + UI/Web/src/app/_services/reader.service.ts | 30 +- UI/Web/src/app/app-routing.module.ts | 4 + .../book-reader/book-reader.component.ts | 6 +- .../card-details-modal.component.ts | 6 +- .../card-detail-drawer.component.ts | 6 +- .../card-detail-layout.component.html | 2 +- .../card-detail-layout.component.ts | 24 +- .../cards/list-item/list-item.component.html | 3 +- .../carousel-reel.component.html | 2 +- .../carousel-reel.component.scss | 5 + .../library-detail.component.ts | 38 +- .../app/manga-reader/fullscreen-icon.pipe.ts | 15 + .../manga-reader/manga-reader.component.html | 25 +- .../manga-reader/manga-reader.component.ts | 170 +- .../app/manga-reader/manga-reader.module.ts | 4 +- .../src/app/pdf-reader/pdf-reader.module.ts | 21 + .../pdf-reader/pdf-reader.router.module.ts | 17 + .../pdf-reader/pdf-reader.component.html | 79 + .../pdf-reader/pdf-reader.component.scss | 12 + .../pdf-reader/pdf-reader.component.ts | 191 ++ .../reading-list-detail.component.ts | 8 +- .../series-detail/series-detail.component.ts | 7 +- .../app/shared/_services/download.service.ts | 4 +- .../app/shared/_services/utility.service.ts | 10 + UI/Web/tsconfig.json | 2 +- 45 files changed, 2309 insertions(+), 208 deletions(-) create mode 100644 API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs create mode 100644 API/Data/Migrations/20220615190640_LastFileAnalysis.cs create mode 100644 API/Data/Repositories/MangaFileRepository.cs create mode 100644 UI/Web/src/app/manga-reader/fullscreen-icon.pipe.ts create mode 100644 UI/Web/src/app/pdf-reader/pdf-reader.module.ts create mode 100644 UI/Web/src/app/pdf-reader/pdf-reader.router.module.ts create mode 100644 UI/Web/src/app/pdf-reader/pdf-reader/pdf-reader.component.html create mode 100644 UI/Web/src/app/pdf-reader/pdf-reader/pdf-reader.component.scss create mode 100644 UI/Web/src/app/pdf-reader/pdf-reader/pdf-reader.component.ts diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index e93161594..10c7d3583 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -71,6 +71,8 @@ namespace API.Tests.Parser [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画卷2第25话", "2")] [InlineData("63권#200", "63")] [InlineData("시즌34삽화2", "34")] + [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")] + [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename)); @@ -253,6 +255,7 @@ namespace API.Tests.Parser [InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")] [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")] [InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")] + [InlineData("[ハレム]ナナとカオル ~高校生のSMごっこ~ 第10話", "10")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename)); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index d5a8d4bee..c29a78036 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -279,8 +279,8 @@ namespace API.Tests.Services } } }; - cs.GetCachedEpubFile(1, c); - Assert.Same($"{DataDirectory}1.epub", cs.GetCachedEpubFile(1, c)); + cs.GetCachedFile(c); + Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c)); } #endregion diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 7b4b49a9f..867cac070 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -37,11 +38,34 @@ namespace API.Controllers { var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); var bookTitle = string.Empty; - if (dto.SeriesFormat == MangaFormat.Epub) + switch (dto.SeriesFormat) { - var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First(); - using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions); - bookTitle = book.Title; + case MangaFormat.Epub: + { + var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First(); + using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions); + bookTitle = book.Title; + break; + } + case MangaFormat.Pdf: + { + var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First(); + if (string.IsNullOrEmpty(bookTitle)) + { + // Override with filename + bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath); + } + + break; + } + case MangaFormat.Image: + break; + case MangaFormat.Archive: + break; + case MangaFormat.Unknown: + break; + default: + throw new ArgumentOutOfRangeException(); } return Ok(new BookInfoDto() @@ -209,7 +233,7 @@ namespace API.Controllers public async Task> GetBookPage(int chapterId, [FromQuery] int page) { var chapter = await _cacheService.Ensure(chapterId); - var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter); + var path = _cacheService.GetCachedFile(chapter); using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions); var mappings = await _bookService.CreateKeyToPageMappingAsync(book); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 58975da4a..718ad753d 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -11,7 +11,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Services; -using API.Services.Tasks; using API.SignalR; using Hangfire; using Microsoft.AspNetCore.Mvc; @@ -45,6 +44,34 @@ namespace API.Controllers _eventHub = eventHub; } + /// + /// Returns the PDF for the chapterId. + /// + /// API Key for user to validate they have access + /// + /// + [HttpGet("pdf")] + public async Task GetPdf(int chapterId) + { + + var chapter = await _cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest("There was an issue finding pdf file for reading"); + + try + { + var path = _cacheService.GetCachedFile(chapter); + if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should."); + + Response.AddCacheHeader(path, TimeSpan.FromMinutes(60).Seconds); + return PhysicalFile(path, "application/pdf", Path.GetFileName(path), true); + } + catch (Exception) + { + _cacheService.CleanupChapters(new []{ chapterId }); + throw; + } + } + /// /// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading. /// diff --git a/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs b/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs new file mode 100644 index 000000000..4c5a53f7f --- /dev/null +++ b/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs @@ -0,0 +1,1570 @@ +// +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("20220615190640_LastFileAnalysis")] + partial class LastFileAnalysis + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + 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("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .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("FileName") + .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("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("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .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("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.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("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("ChapterId") + .HasColumnType("INTEGER"); + + 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("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("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("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .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("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("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.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.ClientCascade) + .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.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("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + 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/20220615190640_LastFileAnalysis.cs b/API/Data/Migrations/20220615190640_LastFileAnalysis.cs new file mode 100644 index 000000000..b1fac2ae4 --- /dev/null +++ b/API/Data/Migrations/20220615190640_LastFileAnalysis.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class LastFileAnalysis : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastFileAnalysis", + table: "MangaFile", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastFileAnalysis", + table: "MangaFile"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 59f5ec61c..55dbfbecd 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -516,6 +516,9 @@ namespace API.Data.Migrations b.Property("Format") .HasColumnType("INTEGER"); + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs new file mode 100644 index 000000000..64101324a --- /dev/null +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -0,0 +1,27 @@ +using API.Entities; +using AutoMapper; +using Microsoft.EntityFrameworkCore; + +namespace API.Data.Repositories; + +public interface IMangaFileRepository +{ + void Update(MangaFile file); +} + +public class MangaFileRepository : IMangaFileRepository +{ + private readonly DataContext _context; + private readonly IMapper _mapper; + + public MangaFileRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Update(MangaFile file) + { + _context.Entry(file).State = EntityState.Modified; + } +} diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index fb3e28c07..2e99ac32d 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -22,6 +22,7 @@ public interface IUnitOfWork IGenreRepository GenreRepository { get; } ITagRepository TagRepository { get; } ISiteThemeRepository SiteThemeRepository { get; } + IMangaFileRepository MangaFileRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -58,6 +59,7 @@ public class UnitOfWork : IUnitOfWork public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper); public ITagRepository TagRepository => new TagRepository(_context, _mapper); public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper); + public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context, _mapper); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index 8cd99f3e1..5117dfb4e 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -25,6 +25,10 @@ namespace API.Entities /// /// This gets updated anytime the file is scanned public DateTime LastModified { get; set; } + /// + /// Last time file analysis ran on this file + /// + public DateTime LastFileAnalysis { get; set; } // Relationship Mapping diff --git a/API/Helpers/CacheHelper.cs b/API/Helpers/CacheHelper.cs index 1d1cc247a..28eb59a63 100644 --- a/API/Helpers/CacheHelper.cs +++ b/API/Helpers/CacheHelper.cs @@ -14,6 +14,7 @@ public interface ICacheHelper bool CoverImageExists(string path); bool HasFileNotChangedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile); + bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile); } @@ -62,6 +63,25 @@ public class CacheHelper : ICacheHelper || _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified))); } + /// + /// Has the file been modified since last scan or is user forcing an update + /// + /// + /// + /// + /// + public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile) + { + if (firstFile == null) return false; + if (forceUpdate) return true; + return _fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan) + || _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified); + // return firstFile != null && + // (!forceUpdate && + // !(_fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan) + // || _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified))); + } + /// /// Determines if a given coverImage path exists /// diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index ff6296057..c5d947b26 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -126,6 +126,10 @@ namespace API.Parser new Regex( @"시즌(?\d+(\-|~)?\d+?)", MatchOptions, RegexTimeout), + // Japanese Volume: n巻 -> Volume n + new Regex( + @"(?\d+(?:(\-)\d+)?)巻", + MatchOptions, RegexTimeout), }; private static readonly Regex[] MangaSeriesRegex = new[] @@ -368,6 +372,10 @@ namespace API.Parser new Regex( @"제?(?\d+)권", MatchOptions, RegexTimeout), + // Japanese Volume: n巻 -> Volume n + new Regex( + @"(?\d+(?:(\-)\d+)?)巻", + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicChapterRegex = new[] @@ -489,6 +497,10 @@ namespace API.Parser new Regex( @"제?(?\d+\.?\d+)(화|장)", MatchOptions, RegexTimeout), + // Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル ~高校生のSMごっこ~ 第1話 + new Regex( + @"第?(?\d+(?:.\d+|-\d+)?)話", + MatchOptions, RegexTimeout), }; private static readonly Regex[] MangaEditionRegex = { // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index c73b412ae..68ba03134 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -47,6 +47,7 @@ namespace API.Services /// /// /// Where the files will be extracted to. If doesn't exist, will be created. + [Obsolete("This method of reading is no longer supported. Please use native pdf reader")] void ExtractPdfImages(string fileFilePath, string targetDirectory); Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary mappings, int page); diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index d2f6d336e..e9bb693eb 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -29,7 +29,7 @@ namespace API.Services void CleanupBookmarks(IEnumerable seriesIds); string GetCachedPagePath(Chapter chapter, int page); string GetCachedBookmarkPagePath(int seriesId, int page); - string GetCachedEpubFile(int chapterId, Chapter chapter); + string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files); Task CacheBookmarkForSeries(int userId, int seriesId); void CleanupBookmarkCache(int seriesId); @@ -73,14 +73,13 @@ namespace API.Services } /// - /// Returns the full path to the cached epub file. If the file does not exist, will fallback to the original. + /// Returns the full path to the cached file. If the file does not exist, will fallback to the original. /// - /// /// /// - public string GetCachedEpubFile(int chapterId, Chapter chapter) + public string GetCachedFile(Chapter chapter) { - var extractPath = GetCachePath(chapterId); + var extractPath = GetCachePath(chapter.Id); var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); if (!(_directoryService.FileSystem.FileInfo.FromFileName(path).Exists)) { @@ -89,6 +88,7 @@ namespace API.Services return path; } + /// /// Caches the files for the given chapter to CacheDirectory /// @@ -136,25 +136,25 @@ namespace API.Services extraPath = file.Id + string.Empty; } - if (file.Format == MangaFormat.Archive) + switch (file.Format) { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); - } - else if (file.Format == MangaFormat.Pdf) - { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); - } - else if (file.Format == MangaFormat.Epub) - { - removeNonImages = false; - if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + case MangaFormat.Archive: + _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + break; + case MangaFormat.Epub: + case MangaFormat.Pdf: { - _logger.LogError("{Archive} does not exist on disk", files[0].FilePath); - throw new KavitaException($"{files[0].FilePath} does not exist on disk"); - } + removeNonImages = false; + if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + { + _logger.LogError("{File} does not exist on disk", files[0].FilePath); + throw new KavitaException($"{files[0].FilePath} does not exist on disk"); + } - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + _directoryService.ExistOrCreate(extractPath); + _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + break; + } } } diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index a5130c747..3b2e0bf4c 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -110,15 +110,13 @@ public class ReadingItemService : IReadingItemService { switch (format) { - case MangaFormat.Pdf: - _bookService.ExtractPdfImages(fileFilePath, targetDirectory); - break; case MangaFormat.Archive: _archiveService.ExtractArchive(fileFilePath, targetDirectory); break; case MangaFormat.Image: _imageService.ExtractImages(fileFilePath, targetDirectory, imageCount); break; + case MangaFormat.Pdf: case MangaFormat.Unknown: case MangaFormat.Epub: break; diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index aaa8e4762..f4675051b 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -55,7 +55,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); var stopwatch = Stopwatch.StartNew(); - var totalTime = 0L; _logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, @@ -64,7 +63,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++) { if (chunkInfo.TotalChunks == 0) continue; - totalTime += stopwatch.ElapsedMilliseconds; stopwatch.Restart(); _logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}", @@ -145,26 +143,30 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService private async Task ProcessSeries(Series series, bool forceUpdate = false, bool useFileName = true) { var isEpub = series.Format == MangaFormat.Epub; - + series.WordCount = 0; foreach (var volume in series.Volumes) { + volume.WordCount = 0; foreach (var chapter in volume.Chapters) { // This compares if it's changed since a file scan only - if (!_cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, forceUpdate, - chapter.Files.FirstOrDefault()) && chapter.WordCount != 0) + var firstFile = chapter.Files.FirstOrDefault(); + if (firstFile == null) return; + if (!_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate, + firstFile)) continue; if (series.Format == MangaFormat.Epub) { long sum = 0; var fileCounter = 1; - foreach (var file in chapter.Files.Select(file => file.FilePath)) + foreach (var file in chapter.Files) { + var filePath = file.FilePath; var pageCounter = 1; try { - using var book = await EpubReader.OpenBookAsync(file, BookService.BookReaderOptions); + using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions); var totalPages = book.Content.Html.Values; foreach (var bookPage in totalPages) @@ -174,7 +176,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress, - ProgressEventType.Updated, useFileName ? file : series.Name)); + ProgressEventType.Updated, useFileName ? filePath : series.Name)); sum += await GetWordCountFromHtml(bookPage); pageCounter++; } @@ -190,6 +192,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService return; } + file.LastFileAnalysis = DateTime.Now; + _unitOfWork.MangaFileRepository.Update(file); } chapter.WordCount = sum; diff --git a/UI/Web/angular.json b/UI/Web/angular.json index 56bc8a3e1..adca7906a 100644 --- a/UI/Web/angular.json +++ b/UI/Web/angular.json @@ -30,7 +30,12 @@ "tsConfig": "tsconfig.app.json", "assets": [ "src/assets", - "src/site.webmanifest" + "src/site.webmanifest", + { + "glob": "**/*", + "input": "node_modules/ngx-extended-pdf-viewer/assets/", + "output": "/assets/" + } ], "sourceMap": { "hidden": false, diff --git a/UI/Web/e2e/tsconfig.json b/UI/Web/e2e/tsconfig.json index 0782539c0..beb3f1cf1 100644 --- a/UI/Web/e2e/tsconfig.json +++ b/UI/Web/e2e/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "outDir": "../out-tsc/e2e", "module": "commonjs", - "target": "es2018", + "target": "es2020", "types": [ "jasmine", "node" diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 535d8fa75..7944d68d4 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -9230,6 +9230,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9677,6 +9682,15 @@ "tslib": "^2.3.0" } }, + "ngx-extended-pdf-viewer": { + "version": "13.5.2", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-13.5.2.tgz", + "integrity": "sha512-dbGozWdfjHosHtJXRbM7zZQ8Zojdpv2/5e68767htvPRQ2JCUtRN+u6NwA59k+sNpNCliHhjaeFMXfWEWEHDMQ==", + "requires": { + "lodash.deburr": "^4.1.0", + "tslib": "^2.3.0" + } + }, "ngx-file-drop": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-13.0.0.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index d92242b4e..a089bf6f4 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -39,6 +39,7 @@ "lazysizes": "^5.3.2", "ng-circle-progress": "^1.6.0", "ngx-color-picker": "^12.0.0", + "ngx-extended-pdf-viewer": "^13.5.2", "ngx-file-drop": "^13.0.0", "ngx-infinite-scroll": "^13.0.2", "ngx-toastr": "^14.2.1", diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index a167e2ac8..8d6986259 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -5,10 +5,14 @@ import { ChapterInfo } from '../manga-reader/_models/chapter-info'; import { UtilityService } from '../shared/_services/utility.service'; import { Chapter } from '../_models/chapter'; import { HourEstimateRange } from '../_models/hour-estimate-range'; +import { MangaFormat } from '../_models/manga-format'; import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; import { PageBookmark } from '../_models/page-bookmark'; import { ProgressBookmark } from '../_models/progress-bookmark'; +export const CHAPTER_ID_DOESNT_EXIST = -1; +export const CHAPTER_ID_NOT_FETCHED = -2; + @Injectable({ providedIn: 'root' }) @@ -21,6 +25,22 @@ export class ReaderService { constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } + getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { + if (format === undefined) format = MangaFormat.ARCHIVE; + + if (format === MangaFormat.EPUB) { + return ['library', libraryId, 'series', seriesId, 'book', chapterId]; + } else if (format === MangaFormat.PDF) { + return ['library', libraryId, 'series', seriesId, 'pdf', chapterId]; + } else { + return ['library', libraryId, 'series', seriesId, 'manga', chapterId]; + } + } + + downloadPdf(chapterId: number) { + return this.baseUrl + 'reader/pdf?chapterId=' + chapterId; + } + bookmark(seriesId: number, volumeId: number, chapterId: number, page: number) { return this.httpClient.post(this.baseUrl + 'reader/bookmark', {seriesId, volumeId, chapterId, page}); } @@ -51,7 +71,7 @@ export class ReaderService { /** * Used exclusively for reading multiple bookmarks from a series - * @param seriesId + * @param seriesId */ getBookmarkInfo(seriesId: number) { return this.httpClient.get(this.baseUrl + 'reader/bookmark-info?seriesId=' + seriesId); @@ -100,7 +120,7 @@ export class ReaderService { markVolumeUnread(seriesId: number, volumeId: number) { return this.httpClient.post(this.baseUrl + 'reader/mark-volume-unread', {seriesId, volumeId}); } - + getNextChapter(seriesId: number, volumeId: number, currentChapterId: number, readingListId: number = -1) { if (readingListId > 0) { @@ -150,7 +170,7 @@ export class ReaderService { /** * Parses out the page number from a Image src url * @param imageSrc Src attribute of Image - * @returns + * @returns */ imageUrlToPageNum(imageSrc: string) { if (imageSrc === undefined || imageSrc === '') { return -1; } @@ -192,7 +212,7 @@ export class ReaderService { } enterFullscreen(el: Element, callback?: VoidFunction) { - if (!document.fullscreenElement) { + if (!document.fullscreenElement) { if (el.requestFullscreen) { el.requestFullscreen().then(() => { if (callback) { @@ -214,7 +234,7 @@ export class ReaderService { } /** - * + * * @returns If document is in fullscreen mode */ checkFullscreenMode() { diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index 7c50a17ba..a11a842a5 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -68,6 +68,10 @@ const routes: Routes = [ path: ':libraryId/series/:seriesId/book', loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule) }, + { + path: ':libraryId/series/:seriesId/pdf', + loadChildren: () => import('../app/pdf-reader/pdf-reader.module').then(m => m.PdfReaderModule) + }, ] }, {path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)}, diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index c38fbcabb..1274cc3ae 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -7,7 +7,7 @@ import { catchError, debounceTime, take, takeUntil } from 'rxjs/operators'; import { Chapter } from 'src/app/_models/chapter'; import { AccountService } from 'src/app/_services/account.service'; import { NavService } from 'src/app/_services/nav.service'; -import { ReaderService } from 'src/app/_services/reader.service'; +import { CHAPTER_ID_DOESNT_EXIST, CHAPTER_ID_NOT_FETCHED, ReaderService } from 'src/app/_services/reader.service'; import { SeriesService } from 'src/app/_services/series.service'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { BookService } from '../book.service'; @@ -40,8 +40,6 @@ interface HistoryPoint { } const TOP_OFFSET = -50 * 1.5; // px the sticky header takes up // TODO: Do I need this or can I change it with new fixed top height -const CHAPTER_ID_NOT_FETCHED = -2; -const CHAPTER_ID_DOESNT_EXIST = -1; /** * Styles that should be applied on the top level book-content tag @@ -515,7 +513,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) { // Redirect to the manga reader. const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); - this.router.navigate(['library', info.libraryId, 'series', info.seriesId, 'manga', this.chapterId], {queryParams: params}); + this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params}); return; } diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts index 6c9b86133..451ec0fce 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts @@ -222,10 +222,6 @@ export class CardDetailsModalComponent implements OnInit { return; } - if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]); - } else { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]); - } + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, this.chapter.id, chapter.files[0].format)); } } diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index c83f32f27..6b5da3c59 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -219,11 +219,7 @@ export class CardDetailDrawerComponent implements OnInit { } const params = this.readerService.getQueryParamsObject(incognito, false); - if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id], {queryParams: params}); - } else { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id], {queryParams: params}); - } + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: params}); this.close(); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index 498f2144f..96654b5f1 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -102,7 +102,7 @@ -
+
+
@@ -47,7 +51,10 @@ title="Previous Page" aria-hidden="true"> -
+
@@ -56,12 +63,12 @@
+ class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"> @@ -115,13 +122,13 @@
@@ -138,7 +145,7 @@
 
-
+