From f701f8e5999da4e7ce8f8e1b4c7a968953426a92 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Fri, 13 May 2022 19:30:37 -0500 Subject: [PATCH] Book Reader Bugfixes (#1254) * Fixed image scoping breaking and books not being able to load images * Cleaned up a lot of variables and added more jsdoc. After shifting the margin, we try to recover the column layout width,height, and scroll posisiton. * Tap to paginate is working on first load now * On resize, rescroll to attempt to avoid breakage * Fixed transparent background for action bar on white theme * Moved some lists to immutable arrays * Actually fixed backgournd now * Fixed some settings not updating in book reader on load * Put some code in place to test out opening menu with clicking on the document * Fixed settings not propagating to the reader * Fixing 2 column when loading annd ios mobile * Fixed an issue where paging to prev page would sometimes skip the first page. * Fixing previous page skipping first page of chapter * removing console logs * Save progress when we page * Click on document to show the side nav * Removed columns auto because it could render more columns than applicable. Don't explicitly call saveProgress on prev page, as we already do in another call. Adjusted the logic to calculate windowHeight and width to be the same throughout the reader. * Setting select fix and settings polish * Fixed awkward tooltip wording * Added a message for when there is nothing to show on recommended tab * Removed bug marker, there was no bug after all * Fixing book title truncation in action bar * When counting volumes or chapters that have range, count the max part of the range for publication status. * Fixing TOC rendering issue * Styling fixes - Fixed an issue where the image height in the book reader was the column height plus padding so it was breaking pagination calc. - Centered book reader setting pills - Made inactive setting pill into a ghost button - Fixed spacing across the reader settings drawer * Added a bit of code to allow us to disable buttons before we click for next chapter load * Removed titles from action bars * The next page button will now show as the primary color to indicate to the user what the next forward page is. * Added a view series to bookmark page and removed actions from header since it didn't work * Fixed a bug where pagination wasn't mutating url state * Lots of changes, code is kinda working. Added Immersive Mode, but didn't generate migration. Added concept of virtual pages with ability to see them. Math is still slightly off. Cleaned up prefetching code so we do it much earlier. Added some code that doesn't work to disable buttons with virtual paging included. * When turning immersive mode on, force tap to paginate * Refactored out the book reader state as it wasn't very beneficial * Fixed total virtual page calculation * Next/prev page seems to be working pretty well * Applied Robbie's virtual page logic and fixed a bug in prev page code * Changed the next page to use same virtual page logic * Getting back and forward working...somehow. * removing redundant code * Fixing book title overflow from new action bar changes * Polishing pagination styles * Changing chapter to section * Fixing up other book reader themes * Fixed the login header being off-center * Fixing styling to follow approach * Refactored the pagination buttons to properly call next/prev page based on reading direction * Drawer pagination buttons now respect when there is no chapters (prev/next) * Everything except disabling buttons when on last possible page working * Added Book Reader immersive mode migration * Disable next/prev buttons for continuous reading before we request next/prev chapter if there is no chapter. * Show a tooltip for the title * Fixed unit test Co-authored-by: Robbie Davis --- API.Tests/Helpers/EntityFactory.cs | 4 +- API.Tests/Parser/ParserTest.cs | 4 +- API/Controllers/UsersController.cs | 1 + API/DTOs/UserPreferencesDto.cs | 5 + API/Data/DbFactory.cs | 4 +- ...234708_BookReaderImmersiveMode.Designer.cs | 1526 +++++++++++++++++ .../20220513234708_BookReaderImmersiveMode.cs | 26 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Data/Seed.cs | 100 +- API/Entities/AppUserPreferences.cs | 5 + API/Parser/Parser.cs | 38 +- API/Services/BookService.cs | 41 +- API/Services/ReaderService.cs | 2 +- API/Services/SeriesService.cs | 2 +- API/Services/Tasks/ScannerService.cs | 12 +- .../app/_models/preferences/preferences.ts | 1 + .../app/_services/action-factory.service.ts | 12 +- .../manage-settings.component.html | 2 +- .../app/all-series/all-series.component.ts | 2 +- UI/Web/src/app/app.component.ts | 3 +- .../book-reader/_models/book-black-theme.ts | 3 + .../book-reader/_models/book-dark-theme.ts | 13 + .../book-reader/_models/book-white-theme.ts | 14 +- .../book-reader/book-reader.component.html | 82 +- .../book-reader/book-reader.component.scss | 69 +- .../book-reader/book-reader.component.ts | 384 ++++- .../reader-settings.component.html | 37 +- .../reader-settings.component.scss | 44 +- .../reader-settings.component.ts | 30 +- .../table-of-contents.component.html | 2 +- .../table-of-contents.component.scss | 2 +- .../table-of-contents.component.ts | 2 +- .../bookmarks/bookmarks.component.html | 3 +- .../bookmark/bookmarks/bookmarks.component.ts | 3 + .../cards/card-item/card-item.component.ts | 3 +- .../collection-detail.component.ts | 2 +- .../library-detail.component.ts | 2 +- .../library-recommended.component.html | 4 +- .../library-recommended.component.ts | 30 +- .../reading-lists/reading-lists.component.ts | 6 +- .../splash-container.component.scss | 2 +- .../_services/filter-utilities.service.ts | 8 +- .../user-preferences.component.html | 17 +- .../user-preferences.component.ts | 19 +- .../{progress.scss => _progress.scss} | 0 45 files changed, 2284 insertions(+), 290 deletions(-) create mode 100644 API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs create mode 100644 API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs rename UI/Web/src/theme/components/{progress.scss => _progress.scss} (100%) diff --git a/API.Tests/Helpers/EntityFactory.cs b/API.Tests/Helpers/EntityFactory.cs index e98cd5730..3632ff9a0 100644 --- a/API.Tests/Helpers/EntityFactory.cs +++ b/API.Tests/Helpers/EntityFactory.cs @@ -31,7 +31,7 @@ namespace API.Tests.Helpers return new Volume() { Name = volumeNumber, - Number = (int) API.Parser.Parser.MinimumNumberFromRange(volumeNumber), + Number = (int) API.Parser.Parser.MinNumberFromRange(volumeNumber), Pages = pages, Chapters = chaps }; @@ -43,7 +43,7 @@ namespace API.Tests.Helpers { IsSpecial = isSpecial, Range = range, - Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty, + Number = API.Parser.Parser.MinNumberFromRange(range) + string.Empty, Files = files ?? new List(), Pages = pageCount, diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index d5dd233de..4ae75d91b 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -140,7 +140,7 @@ namespace API.Tests.Parser [InlineData("40.1_a", 0)] public void MinimumNumberFromRangeTest(string input, float expected) { - Assert.Equal(expected, MinimumNumberFromRange(input)); + Assert.Equal(expected, MinNumberFromRange(input)); } [Theory] @@ -153,7 +153,7 @@ namespace API.Tests.Parser [InlineData("40.1_a", 0)] public void MaximumNumberFromRangeTest(string input, float expected) { - Assert.Equal(expected, MaximumNumberFromRange(input)); + Assert.Equal(expected, MaxNumberFromRange(input)); } [Theory] diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index a896348dc..97dae76a4 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -88,6 +88,7 @@ namespace API.Controllers preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName; existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode; + existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id); // TODO: Remove this code - this overrides layout mode to be single until the mode is released diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 95833fa81..4fc2f6904 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -77,5 +77,10 @@ namespace API.DTOs public SiteTheme Theme { get; set; } public string BookReaderThemeName { get; set; } public BookPageLayoutMode BookReaderLayoutMode { get; set; } + /// + /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. + /// + /// Defaults to false + public bool BookReaderImmersiveMode { get; set; } = false; } } diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs index 41857a455..ad97958da 100644 --- a/API/Data/DbFactory.cs +++ b/API/Data/DbFactory.cs @@ -35,7 +35,7 @@ namespace API.Data return new Volume() { Name = volumeNumber, - Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber), + Number = (int) Parser.Parser.MinNumberFromRange(volumeNumber), Chapters = new List() }; } @@ -46,7 +46,7 @@ namespace API.Data var specialTitle = specialTreatment ? info.Filename : info.Chapters; return new Chapter() { - Number = specialTreatment ? "0" : Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty, + Number = specialTreatment ? "0" : Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty, Range = specialTreatment ? info.Filename : info.Chapters, Title = (specialTreatment && info.Format == MangaFormat.Epub) ? info.Title diff --git a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs b/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs new file mode 100644 index 000000000..26c9a1397 --- /dev/null +++ b/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs @@ -0,0 +1,1526 @@ +// +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("20220513234708_BookReaderImmersiveMode")] + partial class BookReaderImmersiveMode + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); + + 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("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("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PageLayoutMode") + .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("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("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.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("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("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("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.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("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .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/20220513234708_BookReaderImmersiveMode.cs b/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs new file mode 100644 index 000000000..f194a3b87 --- /dev/null +++ b/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class BookReaderImmersiveMode : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BookReaderImmersiveMode", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BookReaderImmersiveMode", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 1c03ac40b..a8b5527d5 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -176,6 +176,9 @@ namespace API.Data.Migrations b.Property("BookReaderFontSize") .HasColumnType("INTEGER"); + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + b.Property("BookReaderLineSpacing") .HasColumnType("INTEGER"); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 9b7dacc2a..63e6cbdd5 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; @@ -22,35 +21,36 @@ namespace API.Data /// /// Generated on Startup. Seed.SeedSettings must run before /// - public static IList DefaultSettings; + public static ImmutableArray DefaultSettings; - public static readonly IList DefaultThemes = new List - { - new() + public static readonly ImmutableArray DefaultThemes = ImmutableArray.Create( + new List { - Name = "Dark", - NormalizedName = Parser.Parser.Normalize("Dark"), - Provider = ThemeProvider.System, - FileName = "dark.scss", - IsDefault = true, - }, - new() - { - Name = "Light", - NormalizedName = Parser.Parser.Normalize("Light"), - Provider = ThemeProvider.System, - FileName = "light.scss", - IsDefault = false, - }, - new() - { - Name = "E-Ink", - NormalizedName = Parser.Parser.Normalize("E-Ink"), - Provider = ThemeProvider.System, - FileName = "e-ink.scss", - IsDefault = false, - }, - }; + new() + { + Name = "Dark", + NormalizedName = Parser.Parser.Normalize("Dark"), + Provider = ThemeProvider.System, + FileName = "dark.scss", + IsDefault = true, + }, + new() + { + Name = "Light", + NormalizedName = Parser.Parser.Normalize("Light"), + Provider = ThemeProvider.System, + FileName = "light.scss", + IsDefault = false, + }, + new() + { + Name = "E-Ink", + NormalizedName = Parser.Parser.Normalize("E-Ink"), + Provider = ThemeProvider.System, + FileName = "e-ink.scss", + IsDefault = false, + }, + }.ToArray()); public static async Task SeedRoles(RoleManager roleManager) { @@ -91,24 +91,32 @@ namespace API.Data public static async Task SeedSettings(DataContext context, IDirectoryService directoryService) { await context.Database.EnsureCreatedAsync(); - - DefaultSettings = new List() + DefaultSettings = ImmutableArray.Create(new List() { - new () {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, - new () {Key = ServerSettingKey.TaskScan, Value = "daily"}, - new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, // Not used from DB, but DB is sync with appSettings.json - new () {Key = ServerSettingKey.TaskBackup, Value = "daily"}, - new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)}, - new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json - new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, - new () {Key = ServerSettingKey.EnableOpds, Value = "false"}, - new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, - new () {Key = ServerSettingKey.BaseUrl, Value = "/"}, - new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()}, - new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()}, - new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory}, - new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl}, - }; + new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, + new() {Key = ServerSettingKey.TaskScan, Value = "daily"}, + new() + { + Key = ServerSettingKey.LoggingLevel, Value = "Information" + }, // Not used from DB, but DB is sync with appSettings.json + new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, + new() + { + Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory) + }, + new() + { + Key = ServerSettingKey.Port, Value = "5000" + }, // Not used from DB, but DB is sync with appSettings.json + new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, + new() {Key = ServerSettingKey.EnableOpds, Value = "false"}, + new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, + new() {Key = ServerSettingKey.BaseUrl, Value = "/"}, + new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()}, + new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()}, + new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory}, + new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl}, + }.ToArray()); foreach (var defaultSetting in DefaultSettings) { diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 6caa18b79..bd68bc5ef 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -82,6 +82,11 @@ namespace API.Entities /// /// Defaults to Default public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default; + /// + /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. + /// + /// Defaults to false + public bool BookReaderImmersiveMode { get; set; } = false; public AppUser AppUser { get; set; } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index c17ecc716..3edcf5d7c 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -926,25 +926,7 @@ namespace API.Parser } - public static float MaximumNumberFromRange(string range) - { - try - { - if (!Regex.IsMatch(range, @"^[\d-.]+$")) - { - return (float) 0.0; - } - - var tokens = range.Replace("_", string.Empty).Split("-"); - return tokens.Max(float.Parse); - } - catch - { - return (float) 0.0; - } - } - - public static float MinimumNumberFromRange(string range) + public static float MinNumberFromRange(string range) { try { @@ -962,6 +944,24 @@ namespace API.Parser } } + public static float MaxNumberFromRange(string range) + { + try + { + if (!Regex.IsMatch(range, @"^[\d-.]+$")) + { + return (float) 0.0; + } + + var tokens = range.Replace("_", string.Empty).Split("-"); + return tokens.Max(float.Parse); + } + catch + { + return (float) 0.0; + } + } + public static string Normalize(string name) { return NormalizeRegex.Replace(name, string.Empty).ToLower(); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 4ccea99b4..fedd2ddb9 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -156,8 +156,7 @@ namespace API.Services public async Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book) { - // @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be - // Scoped + // @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty; var importBuilder = new StringBuilder(); foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) @@ -246,13 +245,13 @@ namespace API.Services private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase) { - var images = doc.DocumentNode.SelectNodes("//img"); + var images = doc.DocumentNode.SelectNodes("//img") + ?? doc.DocumentNode.SelectNodes("//image"); + if (images == null) return; foreach (var image in images) { - if (image.Name != "img") continue; - string key = null; if (image.Attributes["src"] != null) { @@ -283,23 +282,22 @@ namespace API.Services /// private static string GetKeyForImage(EpubBookRef book, string imageFile) { - if (!book.Content.Images.ContainsKey(imageFile)) + if (book.Content.Images.ContainsKey(imageFile)) return imageFile; + + var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile)); + if (correctedKey != null) { - var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile)); + imageFile = correctedKey; + } + else if (imageFile.StartsWith("..")) + { + // There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg + correctedKey = + book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty))); if (correctedKey != null) { imageFile = correctedKey; } - else if (imageFile.StartsWith("..")) - { - // There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg - correctedKey = - book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty))); - if (correctedKey != null) - { - imageFile = correctedKey; - } - } } return imageFile; @@ -321,12 +319,11 @@ namespace API.Services private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary mappings) { var anchors = doc.DocumentNode.SelectNodes("//a"); - if (anchors != null) + if (anchors == null) return; + + foreach (var anchor in anchors) { - foreach (var anchor in anchors) - { - BookService.UpdateLinks(anchor, mappings, page); - } + UpdateLinks(anchor, mappings, page); } } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index b4402558a..8e3f5c47d 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -475,7 +475,7 @@ public class ReaderService : IReaderService { var chapters = volume.Chapters .OrderBy(c => float.Parse(c.Number)) - .Where(c => !c.IsSpecial && Parser.Parser.MaximumNumberFromRange(c.Range) <= chapterNumber); + .Where(c => !c.IsSpecial && Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber); MarkChaptersAsRead(user, volume.SeriesId, chapters); } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 4dec796c2..ada58dc19 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -456,7 +456,7 @@ public class SeriesService : ISeriesService var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) - .OrderBy(v => Parser.Parser.MinimumNumberFromRange(v.Name)) + .OrderBy(v => Parser.Parser.MinNumberFromRange(v.Name)) .ToList(); var chapters = volumes.SelectMany(v => v.Chapters).ToList(); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index a9a96b71f..0bd8b458b 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -124,7 +124,9 @@ public class ScannerService : IScannerService var path = Directory.GetParent(existingFolder)?.FullName; if (!folderPaths.Contains(path) || !folderPaths.Any(p => p.Contains(path ?? string.Empty))) { - _logger.LogInformation("[ScanService] Aborted: {SeriesName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library", series.OriginalName); + _logger.LogCritical("[ScanService] Aborted: {SeriesName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library", series.OriginalName); + await _eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent($"Scan of {series.Name} aborted", $"{series.OriginalName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library")); return; } if (!string.IsNullOrEmpty(path)) @@ -597,8 +599,8 @@ public class ScannerService : IScannerService // To not have to rely completely on ComicInfo, try to parse out if the series is complete by checking parsed filenames as well. if (series.Metadata.MaxCount != series.Metadata.TotalCount) { - var maxVolume = series.Volumes.Max(v => v.Number); - var maxChapter = chapters.Max(c => (int) float.Parse(c.Number)); + var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name)); + var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range)); if (maxVolume == series.Metadata.TotalCount) series.Metadata.MaxCount = maxVolume; else if (maxChapter == series.Metadata.TotalCount) series.Metadata.MaxCount = maxChapter; } @@ -863,7 +865,7 @@ public class ScannerService : IScannerService // Add files var specialTreatment = info.IsSpecialInfo(); AddOrUpdateFileForChapter(chapter, info); - chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty; + chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty; chapter.Range = specialTreatment ? info.Filename : info.Chapters; } @@ -910,7 +912,7 @@ public class ScannerService : IScannerService private void UpdateChapterFromComicInfo(Chapter chapter, ICollection allPeople, ICollection allTags, ICollection allGenres, ComicInfo? info) { - var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault(); + var firstFile = chapter.Files.MinBy(x => x.Chapter); if (firstFile == null || _cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, firstFile)) return; diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 065eb577b..874dd09a9 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -27,6 +27,7 @@ export interface Preferences { bookReaderReadingDirection: ReadingDirection; bookReaderThemeName: string; bookReaderLayoutMode: BookPageLayoutMode; + bookReaderImmersiveMode: boolean; // Global theme: SiteTheme; diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 52c208859..b67a162bd 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -27,7 +27,11 @@ export enum Action { /** * Essentially a download, but handled differently. Needed so card bubbles it up for handling */ - DownloadBookmark = 12 + DownloadBookmark = 12, + /** + * Open Series detail page for said series + */ + ViewSeries = 13 } export interface ActionItem { @@ -305,6 +309,12 @@ export class ActionFactoryService { ]; this.bookmarkActions = [ + { + action: Action.ViewSeries, + title: 'View Series', + callback: this.dummyCallback, + requiresAdmin: false + }, { action: Action.DownloadBookmark, title: 'Download', diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 480857868..be06070a1 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -80,7 +80,7 @@

Reoccuring Tasks

  - How often Kavita will scan and refresh metatdata around manga files. + How often Kavita will scan and refresh metadata around manga files. How often Kavita will scan and refresh metatdata around manga files. +
-
- +
+ + This will hide the menu behind a click on the reader document and turn tap to paginate on + + + +
+ + +
+
+ +
+ Put reader in fullscreen mode @@ -91,7 +106,11 @@
- + + Default: Mirrors epub file (usually one long scrolling page per chapter).
1 Column: Creates a single virtual page at a time.
2 Column: Creates two virtual pages at a time laid out side-by-side.
+ + +
@@ -120,7 +139,7 @@
- diff --git a/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.scss b/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.scss index d9348c704..ed21da1eb 100644 --- a/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.scss +++ b/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.scss @@ -1,9 +1,43 @@ -.dot { - height: 25px; - width: 25px; - border-radius: 50%; +.controls { + margin: 0.25rem 0 0.25rem; + + .form-label { + margin: 0; + } + + .btn.btn-icon { + display: flex; + width: 50%; + justify-content: center; + align-items: center; + &.color { + display: unset; + width: auto; + + .dot { + height: 25px; + width: 25px; + border-radius: 50%; + margin: 0 auto; + } + } + } + + .form-check.form-switch { + width: 50%; + display: flex; + justify-content: center; + + input { + margin-right: 0.25rem; + } + } } .active { border: 1px solid var(--primary-color); -} \ No newline at end of file +} + +::ng-deep .accordion-body { + padding: 0.25rem 1rem 1rem !important; +} diff --git a/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.ts b/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.ts index e3ec33ac4..d941f4fd0 100644 --- a/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.ts +++ b/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from '@angular/common'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Subject, take, takeUntil } from 'rxjs'; import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode'; @@ -63,7 +63,6 @@ const mobileBreakpointMarginOverride = 700; styleUrls: ['./reader-settings.component.scss'] }) export class ReaderSettingsComponent implements OnInit, OnDestroy { - /** * Outputs when clickToPaginate is changed */ @@ -88,6 +87,10 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { * Outputs when reading direction is changed */ @Output() readingDirection: EventEmitter = new EventEmitter(); + /** + * Outputs when immersive mode is changed + */ + @Output() immersiveMode: EventEmitter = new EventEmitter(); user!: User; /** @@ -127,7 +130,8 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { - constructor(private bookService: BookService, private accountService: AccountService, @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {} + constructor(private bookService: BookService, private accountService: AccountService, + @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {} ngOnInit(): void { @@ -153,10 +157,9 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { if (this.user.preferences.bookReaderReadingDirection === undefined) { this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight; } - - this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection; + this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(fontName => { const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family; @@ -180,7 +183,6 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { this.clickToPaginateChanged.emit(value); }); - this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, [])); this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { this.pageStyles['line-height'] = value + '%'; @@ -196,11 +198,25 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, [])); this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => { - console.log(layoutMode); this.layoutModeUpdate.emit(layoutMode); }); + this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, [])); + this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((immersiveMode: boolean) => { + if (immersiveMode) { + this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true); + } + this.immersiveMode.emit(immersiveMode); + }); + + this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme); + + // Emit first time so book reader gets the setting + this.readingDirection.emit(this.readingDirectionModel); + this.clickToPaginateChanged.emit(this.user.preferences.bookReaderTapToPaginate); + this.layoutModeUpdate.emit(this.user.preferences.bookReaderLayoutMode); + this.resetSettings(); } else { this.resetSettings(); diff --git a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.html b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.html index 174e8cf8c..6d43689df 100644 --- a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.html +++ b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.html @@ -1,5 +1,5 @@
-

Table of Contents

+
This book does not have Table of Contents set in the metadata or a toc file
diff --git a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.scss b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.scss index 6ae729a59..e556f0e78 100644 --- a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.scss +++ b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.scss @@ -7,5 +7,5 @@ } .chapter-title { - padding-inline-start: 0px + padding-inline-start: 1rem; } \ No newline at end of file diff --git a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.ts b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.ts index 101a09191..709e4d645 100644 --- a/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.ts +++ b/UI/Web/src/app/book-reader/table-of-contents/table-of-contents.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Subject } from 'rxjs'; import { BookChapterItem } from '../_models/book-chapter-item'; diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html index f08659b1d..42151ba2b 100644 --- a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html @@ -1,9 +1,8 @@

- Bookmarks

-
{{series?.length}} Series
+
{{series?.length}} Series
| null = null; downloadInProgress: boolean = false; - /** * Handles touch events for selection on mobile devices */ prevTouchTime: number = 0; /** - * Handles touch events for selection on mobile devices to ensure you are touch scrolling + * Handles touch events for selection on mobile devices to ensure you aren't touch scrolling */ prevOffset: number = 0; diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index 83ee8a4c9..b2f779d2a 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -158,7 +158,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { } onPageChange(pagination: Pagination) { - this.filterUtilityService.updateUrlFromPagination(this.seriesPagination); + this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined); this.loadPage(); } diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index fe19afd62..562087000 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -183,7 +183,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { } onPageChange(pagination: Pagination) { - this.filterUtilityService.updateUrlFromPagination(this.pagination); + this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined); this.loadPage(); } diff --git a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html index 1fb9f4573..26dcb5090 100644 --- a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html +++ b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.html @@ -1,6 +1,8 @@ - + +

Nothing to show here. Add some metadata to your library, read something or rate something.

+
diff --git a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts index 61576f72a..d06493b37 100644 --- a/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts +++ b/UI/Web/src/app/library-detail/library-recommended/library-recommended.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { map, Observable, shareReplay } from 'rxjs'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs'; import { Genre } from 'src/app/_models/genre'; import { Series } from 'src/app/_models/series'; import { MetadataService } from 'src/app/_services/metadata.service'; @@ -11,7 +11,7 @@ import { SeriesService } from 'src/app/_services/series.service'; templateUrl: './library-recommended.component.html', styleUrls: ['./library-recommended.component.scss'] }) -export class LibraryRecommendedComponent implements OnInit { +export class LibraryRecommendedComponent implements OnInit, OnDestroy { @Input() libraryId: number = 0; @@ -24,30 +24,40 @@ export class LibraryRecommendedComponent implements OnInit { genre: string = ''; genre$!: Observable; + all$!: Observable; + noData: boolean = true; + + private onDestroy: Subject = new Subject(); constructor(private recommendationService: RecommendationService, private seriesService: SeriesService, private metadataService: MetadataService) { } ngOnInit(): void { this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId) - .pipe(map(p => p.result), shareReplay()); + .pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId) - .pipe(map(p => p.result), shareReplay()); + .pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); this.rediscover$ = this.recommendationService.getRediscover(this.libraryId) - .pipe(map(p => p.result), shareReplay()); + .pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); this.onDeck$ = this.seriesService.getOnDeck(this.libraryId) - .pipe(map(p => p.result), shareReplay()); + .pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); - this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay()); + this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(takeUntil(this.onDestroy), map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay()); this.genre$.subscribe(genre => { - this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(map(p => p.result), shareReplay()); + this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); }); + this.all$ = merge(this.quickReads$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntil(this.onDestroy)); + this.all$.subscribe(() => this.noData = false); - + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); } diff --git a/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.ts index 9c6cc149c..7771b7a9c 100644 --- a/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.ts +++ b/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { take } from 'rxjs/operators'; +import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { PaginatedResult, Pagination } from 'src/app/_models/pagination'; import { ReadingList } from 'src/app/_models/reading-list'; import { AccountService } from 'src/app/_services/account.service'; @@ -24,7 +25,8 @@ export class ReadingListsComponent implements OnInit { isAdmin: boolean = false; constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, - private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService) { } + private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService, + private filterUtilityService: FilterUtilitiesService) { } ngOnInit(): void { this.loadPage(); @@ -84,7 +86,7 @@ export class ReadingListsComponent implements OnInit { } onPageChange(pagination: Pagination) { - window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage); + this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);; this.loadPage(); } diff --git a/UI/Web/src/app/registration/splash-container/splash-container.component.scss b/UI/Web/src/app/registration/splash-container/splash-container.component.scss index caba64f6c..e710a7c34 100644 --- a/UI/Web/src/app/registration/splash-container/splash-container.component.scss +++ b/UI/Web/src/app/registration/splash-container/splash-container.component.scss @@ -45,7 +45,7 @@ font-weight: bold; display: inline-block; vertical-align: middle; - width: 280px; + width: 100%; } .card-text { diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index 77dd2910b..5460c7fe0 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -49,11 +49,11 @@ export class FilterUtilitiesService { * @param pagination * @param filter */ - updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter) { - let params = '?page=' + pagination.currentPage; + updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) { + const params = '?page=' + pagination.currentPage; - const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter); - window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination)); + const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter); + window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination)); } /** diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index db26de166..1b1d54059 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -87,7 +87,7 @@
- +
@@ -95,7 +95,7 @@
- +
@@ -123,13 +123,24 @@
- +   Should the sides of the book reader screen allow tapping on it to move to prev/next page Should the sides of the book reader screen allow tapping on it to move to prev/next page
+
+ +
+
+ +   + This will hide the menu behind a click on the reader document and turn tap to paginate on + This will hide the menu behind a click on the reader document and turn tap to paginate on +
+
+
diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 57f351612..2180d783d 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; -import { take } from 'rxjs/operators'; +import { take, takeUntil } from 'rxjs/operators'; import { Title } from '@angular/platform-browser'; import { BookService } from 'src/app/book-reader/book.service'; import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes } from 'src/app/_models/preferences/preferences'; @@ -11,7 +11,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { SettingsService } from 'src/app/admin/settings.service'; import { bookColorThemes } from 'src/app/book-reader/reader-settings/reader-settings.component'; import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode'; -import { forkJoin } from 'rxjs'; +import { forkJoin, Subject } from 'rxjs'; enum AccordionPanelID { ImageReader = 'image-reader', @@ -55,6 +55,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { opdsEnabled: boolean = false; makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)}; + private onDestroy = new Subject(); + get AccordionPanelID() { return AccordionPanelID; } @@ -114,6 +116,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, [])); this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, [])); this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, [])); + this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, [])); this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, [])); }); @@ -125,10 +128,18 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { const values = this.passwordChangeForm.value; this.passwordsMatch = values.password === values.confirmPassword; })); + + this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(mode => { + if (mode) { + this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true); + } + }); } ngOnDestroy() { this.obserableHandles.forEach(o => o.unsubscribe()); + this.onDestroy.next(); + this.onDestroy.complete(); } public get password() { return this.passwordChangeForm.get('password'); } @@ -152,6 +163,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.user.preferences.bookReaderLayoutMode); this.settingsForm.get('bookReaderThemeName')?.setValue(this.user.preferences.bookReaderThemeName); this.settingsForm.get('theme')?.setValue(this.user.preferences.theme); + this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.user.preferences.bookReaderImmersiveMode); } resetPasswordForm() { @@ -180,7 +192,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10), bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10), bookReaderThemeName: modelSettings.bookReaderThemeName, - theme: modelSettings.theme + theme: modelSettings.theme, + bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode }; this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { diff --git a/UI/Web/src/theme/components/progress.scss b/UI/Web/src/theme/components/_progress.scss similarity index 100% rename from UI/Web/src/theme/components/progress.scss rename to UI/Web/src/theme/components/_progress.scss