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