diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 9081cef37..638ef5e6d 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -468,18 +468,15 @@ namespace API.Controllers _unitOfWork.UserRepository.Update(user); } - - if (await _unitOfWork.CommitAsync()) - { - return Ok(); - } + await _unitOfWork.CommitAsync(); } catch (Exception) { await _unitOfWork.RollbackAsync(); + return BadRequest("Could not save bookmark"); } - return BadRequest("Could not save bookmark"); + return Ok(); } /// diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 77b1609e2..a53521b19 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -169,7 +169,7 @@ namespace API.Controllers _logger.LogInformation("Server Settings updated"); - _taskScheduler.ScheduleTasks(); + await _taskScheduler.ScheduleTasks(); return Ok(updateSettingsDto); } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 03dbeaa5e..c36c9d146 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -18,4 +18,4 @@ namespace API.DTOs public ReadingDirection BookReaderReadingDirection { get; set; } public bool SiteDarkMode { get; set; } } -} \ No newline at end of file +} diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs b/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs new file mode 100644 index 000000000..b649b12b6 --- /dev/null +++ b/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs @@ -0,0 +1,1317 @@ +// +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("20211227180752_FullscreenPref")] + partial class FullscreenPref + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); + + 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("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("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("FullscreenMode") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SiteDarkMode") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + 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("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("GenreId") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + 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("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GenreId"); + + 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("Language") + .HasColumnType("TEXT"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + 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("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format") + .IsUnique(); + + 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.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("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.Navigation("AppUser"); + }); + + 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.Genre", null) + .WithMany("Chapters") + .HasForeignKey("GenreId"); + + 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.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("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.Genre", b => + { + b.Navigation("Chapters"); + }); + + 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("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.cs b/API/Data/Migrations/20211227180752_FullscreenPref.cs new file mode 100644 index 000000000..ab6cbc8a8 --- /dev/null +++ b/API/Data/Migrations/20211227180752_FullscreenPref.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class FullscreenPref : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FullscreenMode", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FullscreenMode", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 963a4f815..7666a599a 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -183,6 +183,9 @@ namespace API.Data.Migrations b.Property("BookReaderTapToPaginate") .HasColumnType("INTEGER"); + b.Property("FullscreenMode") + .HasColumnType("INTEGER"); + b.Property("PageSplitOption") .HasColumnType("INTEGER"); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index e124f4709..1537cd27e 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -514,7 +514,7 @@ public class SeriesRepository : ISeriesRepository var retSeries = query.Where(s => s.AppUserId == userId && s.PagesRead > 0 && s.PagesRead < s.Series.Pages) - .OrderByDescending(s => s.LastModified) + .OrderByDescending(s => s.LastModified) // TODO: This needs to be Chapter Created (Max) .ThenByDescending(s => s.Series.LastModified) .Select(s => s.Series) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 23bb8df25..dd2c83b92 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -4,10 +4,19 @@ namespace API.Entities.Enums { public enum LibraryType { + /// + /// Uses Manga regex for filename parsing + /// [Description("Manga")] Manga = 0, + /// + /// Uses Comic regex for filename parsing + /// [Description("Comic")] Comic = 1, + /// + /// Uses Manga regex for filename parsing also uses epub metadata + /// [Description("Book")] Book = 2, } diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 7e44dbbee..664afb7f7 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -1,3 +1,4 @@ + import { PageSplitOption } from './page-split-option'; import { READER_MODE } from './reader-mode'; import { ReadingDirection } from './reading-direction'; diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 8281ab9e9..c192db1fc 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -193,4 +193,48 @@ export class ReaderService { } return params; } + + enterFullscreen(el: Element, callback?: VoidFunction) { + if (!document.fullscreenElement) { + if (el.requestFullscreen) { + el.requestFullscreen().then(() => { + if (callback) { + callback(); + } + }); + } + // else if (el.mozRequestFullScreen) { + // el.mozRequestFullScreen(); + // } else if (el.webkitRequestFullscreen) { + // el.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + // } else if (el.msRequestFullscreen) { + // el.msRequestFullscreen(); + // } + } + } + + exitFullscreen(callback?: VoidFunction) { + if (document.exitFullscreen && this.checkFullscreenMode()) { + document.exitFullscreen().then(() => { + if (callback) { + callback(); + } + }); + } + // else if (document.msExitFullscreen) { + // document.msExitFullscreen(); + // } else if (document.mozCancelFullScreen) { + // document.mozCancelFullScreen(); + // } else if (document.webkitExitFullscreen) { + // document.webkitExitFullscreen(); + // } + } + + /** + * + * @returns If document is in fullscreen mode + */ + checkFullscreenMode() { + return document.fullscreenElement != null; + } } diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html index e12c8d802..f5da74301 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html @@ -1,4 +1,4 @@ -
+
Skip to main content @@ -57,6 +57,17 @@ The ability to click the sides of the page to page left and right
+
+ + Put reader in fullscreen mode + + + + +
diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index e5019010f..c3fae3c56 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -111,6 +111,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef; @ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef; @ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef; + @ViewChild('reader', {static: true}) reader!: ElementRef; /** * Next Chapter Id. This is not garunteed to be a valid ChapterId. Prefetched on page load (non-blocking). @@ -185,6 +186,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ originalBodyColor: string | undefined; + /** + * If the web browser is in fullscreen mode + */ + isFullscreen: boolean = false; + darkModeStyles = ` *:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) { color: #dcdcdc !important; @@ -365,6 +371,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.clickToPaginateVisualOverlayTimeout2 = undefined; } + this.readerService.exitFullscreen(); + this.onDestroy.next(); this.onDestroy.complete(); } @@ -1014,4 +1022,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); } + toggleFullscreen() { + this.isFullscreen = this.readerService.checkFullscreenMode(); + if (this.isFullscreen) { + this.readerService.exitFullscreen(() => { + this.isFullscreen = false; + }); + } else { + this.readerService.enterFullscreen(this.reader.nativeElement, () => { + this.isFullscreen = true; + }); + } + } + } diff --git a/UI/Web/src/app/manga-reader/_models/reader-enums.ts b/UI/Web/src/app/manga-reader/_models/reader-enums.ts index 9738edf9f..37ab08054 100644 --- a/UI/Web/src/app/manga-reader/_models/reader-enums.ts +++ b/UI/Web/src/app/manga-reader/_models/reader-enums.ts @@ -15,8 +15,3 @@ export enum PAGING_DIRECTION { BACKWARDS = -1, } -export enum COLOR_FILTER { - NONE = '', - SEPIA = 'filter-sepia', - DARK = 'filter-dark' -} diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index 0353d29aa..346ba56ec 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -1,4 +1,4 @@ -
+
-
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.scss b/UI/Web/src/app/manga-reader/manga-reader.component.scss index 6caec0857..8357a363b 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.scss +++ b/UI/Web/src/app/manga-reader/manga-reader.component.scss @@ -6,8 +6,6 @@ $side-width: 25%; $dash-width: 3px; $pointer-offset: 5px; -$secondary-color: #CCC; - @media(min-width: 600px) { .overlay .left .i { @@ -18,7 +16,6 @@ $secondary-color: #CCC; } } - .btn-icon { color: white; } @@ -35,6 +32,10 @@ canvas { } } + + + + .loading { position: absolute; left: 48%; @@ -48,14 +49,6 @@ canvas { white-space: nowrap; } -.filter-dark { - filter: brightness(0.65); -} - -.filter-sepia { - filter: sepia(80%) hue-rotate(349deg) saturate(200%) brightness(0.65); -} - .bottom-menu { padding: 20px 20px; } @@ -63,6 +56,7 @@ canvas { .overlay { background-color: rgba(0,0,0,0.5); + backdrop-filter: blur(10px); color: white; } @@ -226,15 +220,28 @@ canvas { width: 100%; } - .highlight { - background-color: rgba(65, 225, 100, 0.5) !important; - animation: fadein .5s both; - } - .highlight-2 { - background-color: rgba(65, 105, 225, 0.5) !important; - animation: fadein .5s both; + .pagination-area { + display: flex; + align-items: center; + justify-content: center; + + i { + color: white; + font-size: 42px; + } } +.highlight { + background-color: rgba(65, 225, 100, 0.5) !important; + animation: fadein .5s both; + backdrop-filter: blur(10px); +} +.highlight-2 { + background-color: rgba(65, 105, 225, 0.5) !important; + animation: fadein .5s both; + backdrop-filter: blur(10px); +} + .bookmark-effect { animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1); diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 5697e6e19..602963818 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -1,5 +1,5 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; -import { Location } from '@angular/common'; +import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; +import { DOCUMENT, Location } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { take, takeUntil } from 'rxjs/operators'; import { User } from '../_models/user'; @@ -19,7 +19,7 @@ import { Stack } from '../shared/data-structures/stack'; import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { ChapterInfo } from './_models/chapter-info'; -import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums'; +import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums'; import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences'; import { READER_MODE } from '../_models/preferences/reader-mode'; import { MangaFormat } from '../_models/manga-format'; @@ -32,7 +32,7 @@ const CHAPTER_ID_NOT_FETCHED = -2; const CHAPTER_ID_DOESNT_EXIST = -1; const ANIMATION_SPEED = 200; -const OVERLAY_AUTO_CLOSE_TIME = 6000; +const OVERLAY_AUTO_CLOSE_TIME = 3000; const CLICK_OVERLAY_TIMEOUT = 3000; @@ -79,7 +79,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { incognitoMode: boolean = false; /** - * If this is true, chapters will be fetched in the order of a reading list, rather than natural series order. + * If this is true, chapters will be fetched in the order of a reading list, rather than natural series order. */ readingListMode: boolean = false; /** @@ -99,14 +99,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { pageSplitOption = PageSplitOption.FitSplit; currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; - colorMode: COLOR_FILTER = COLOR_FILTER.NONE; + isFullscreen: boolean = false; autoCloseMenu: boolean = true; readerMode: READER_MODE = READER_MODE.MANGA_LR; pageSplitOptions = pageSplitOptions; - - isLoading = true; + isLoading = true; + + @ViewChild('reader') reader!: ElementRef; @ViewChild('content') canvas: ElementRef | undefined; private ctx!: CanvasRenderingContext2D; private canvasImage = new Image(); @@ -219,21 +220,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private readonly onDestroy = new Subject(); - + getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum); - + get pageBookmarked() { return this.bookmarks.hasOwnProperty(this.pageNum); } - + get splitIconClass() { if (this.isSplitLeftToRight()) { return 'left-side'; } else if (this.isNoSplit()) { - return 'none'; + return 'none'; } return 'right-side'; } @@ -249,17 +250,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - get colorOptionName() { - switch(this.colorMode) { - case COLOR_FILTER.NONE: - return 'None'; - case COLOR_FILTER.DARK: - return 'Dark'; - case COLOR_FILTER.SEPIA: - return 'Sepia'; - } - } - get READER_MODE(): typeof READER_MODE { return READER_MODE; } @@ -274,10 +264,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, public readerService: ReaderService, private location: Location, - private formBuilder: FormBuilder, private navService: NavService, + private formBuilder: FormBuilder, private navService: NavService, private toastr: ToastrService, private memberService: MemberService, - private libraryService: LibraryService, private utilityService: UtilityService, - private renderer: Renderer2) { + private libraryService: LibraryService, private utilityService: UtilityService, + private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) { this.navService.hideNavBar(); } @@ -295,13 +285,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; - + const readingListId = this.route.snapshot.queryParamMap.get('readingListId'); if (readingListId != null) { this.readingListMode = true; this.readingListId = parseInt(readingListId, 10); } - + this.continuousChaptersStack.push(this.chapterId); @@ -325,10 +315,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.updateForm(); + this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => { this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; const needsSplitting = this.isCoverImage(); - // If we need to split on a menu change, then we need to re-render. + // If we need to split on a menu change, then we need to re-render. if (needsSplitting) { this.loadPage(); } @@ -341,7 +332,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } }); } else { - // If no user, we can't render + // If no user, we can't render this.router.navigateByUrl('/login'); } }); @@ -365,6 +356,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.onDestroy.complete(); this.goToPageEvent.complete(); this.showBookmarkEffectEvent.complete(); + this.readerService.exitFullscreen(); } @HostListener('window:keyup', ['$event']) @@ -407,6 +399,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + clickOverlayClass(side: 'right' | 'left') { + if (!this.showClickOverlay) { + return ''; + } + + if (this.readingDirection === ReadingDirection.LeftToRight) { + return side === 'right' ? 'highlight' : 'highlight-2'; + } + return side === 'right' ? 'highlight-2' : 'highlight'; + } + init() { this.nextChapterId = CHAPTER_ID_NOT_FETCHED; this.prevChapterId = CHAPTER_ID_NOT_FETCHED; @@ -422,7 +425,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }).pipe(take(1)).subscribe(results => { if (this.readingListMode && results.chapterInfo.seriesFormat === MangaFormat.EPUB) { - // Redirect to the book reader. + // Redirect to the book reader. const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); this.router.navigate(['library', results.chapterInfo.libraryId, 'series', results.chapterInfo.seriesId, 'book', this.chapterId], {queryParams: params}); return; @@ -435,8 +438,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { page = this.maxPages; } this.setPageNum(page); - - + + // Due to change detection rules in Angular, we need to re-create the options object to apply the change const newOptions: Options = Object.assign({}, this.pageOptions); @@ -448,7 +451,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.updateTitle(results.chapterInfo, type); }); - + // From bookmarks, create map of pages to make lookup time O(1) this.bookmarks = {}; @@ -562,7 +565,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { getFittingIcon() { const value = this.getFit(); - + switch(value) { case FITTING_OPTION.HEIGHT: return 'fa-arrows-alt-v'; @@ -608,10 +611,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }, OVERLAY_AUTO_CLOSE_TIME); } - + toggleMenu() { this.menuOpen = !this.menuOpen; - + if (this.menuTimeout) { clearTimeout(this.menuTimeout); } @@ -629,8 +632,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } /** - * + * * @returns If the current model reflects no split of fit split + * @remarks Fit to Screen falls under no split */ isNoSplit() { const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10); @@ -717,7 +721,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.readerMode !== READER_MODE.WEBTOON) { this.loadPage(); - } + } } prevPage(event?: any) { @@ -745,7 +749,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.readerMode !== READER_MODE.WEBTOON) { this.loadPage(); - } + } } loadNextChapter() { @@ -789,7 +793,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { loadChapter(chapterId: number, direction: 'Next' | 'Prev') { if (chapterId >= 0) { this.chapterId = chapterId; - this.continuousChaptersStack.push(chapterId); + this.continuousChaptersStack.push(chapterId); // Load chapter Id onto route but don't reload const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); window.history.replaceState({}, '', newRoute); @@ -804,14 +808,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } else { this.nextPageDisabled = true; } - + } } /** * There are some hard limits on the size of canvas' that we must cap at. https://github.com/jhildenbiddle/canvas-size#test-results * For Safari, it's 16,777,216, so we cap at 4096x4096 when this happens. The drawImage in render will perform bi-cubic scaling for us. - * @returns If we should continue to the render loop + * @returns If we should continue to the render loop */ setCanvasSize() { if (this.ctx && this.canvas) { @@ -832,9 +836,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (needsScaling) { this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; - } else if (this.isCoverImage()) { - //this.canvas.nativeElement.width = this.canvasImage.width / 2; - //this.canvas.nativeElement.height = this.canvasImage.height; } else { this.canvas.nativeElement.width = this.canvasImage.width; this.canvas.nativeElement.height = this.canvasImage.height; @@ -877,21 +878,26 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { let newWidth = windowWidth; let newHeight = newWidth / ratio; if (newHeight > windowHeight) { - newHeight = windowHeight; + newHeight = windowHeight; newWidth = newHeight * ratio; } // Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit if (windowWidth > newWidth) { - this.ctx.drawImage(this.canvasImage, 0, 0); + //console.log('Using raw draw'); + this.setCanvasSize(); + this.ctx.drawImage(this.canvasImage, 0, 0); } else { + //console.log('Using scaled draw'); + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight); } } else { + //console.log('Normal Render') this.ctx.drawImage(this.canvasImage, 0, 0); } } - + // Reset scroll on non HEIGHT Fits if (this.getFit() !== FITTING_OPTION.HEIGHT) { window.scrollTo(0, 0); @@ -908,13 +914,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; - + const needsSplitting = this.isCoverImage(); let newScale = this.generalSettingsForm.get('fittingOption')?.value; const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1)); const heightRatio = windowHeight / (this.canvasImage.height); - // Given that we now have image dimensions, assuming this isn't a split image, + // Given that we now have image dimensions, assuming this isn't a split image, // Try to reset one time based on who's dimension (width/height) is smaller if (widthRatio < heightRatio) { newScale = FITTING_OPTION.WIDTH; @@ -984,19 +990,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - clickOverlayClass(side: 'right' | 'left') { - if (!this.showClickOverlay) { - return ''; - } - - if (this.readingDirection === ReadingDirection.LeftToRight) { - return side === 'right' ? 'highlight' : 'highlight-2'; - } - return side === 'right' ? 'highlight-2' : 'highlight'; - } sliderDragUpdate(context: ChangeContext) { - // This will update the value for value except when in webtoon due to how the webtoon reader + // This will update the value for value except when in webtoon due to how the webtoon reader // responds to page changes if (this.readerMode !== READER_MODE.WEBTOON) { this.setPageNum(context.value); @@ -1005,7 +1001,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { sliderPageUpdate(context: ChangeContext) { const page = context.value; - + if (page > this.pageNum) { this.pagingDirection = PAGING_DIRECTION.FORWARD; } else { @@ -1049,7 +1045,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { goToPage(pageNum: number) { let page = pageNum; - + if (page === undefined || this.pageNum === page) { return; } if (page > this.maxPages) { @@ -1079,20 +1075,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return goToPageNum; } - toggleColorMode() { - switch(this.colorMode) { - case COLOR_FILTER.NONE: - this.colorMode = COLOR_FILTER.DARK; - break; - case COLOR_FILTER.DARK: - this.colorMode = COLOR_FILTER.SEPIA; - break; - case COLOR_FILTER.SEPIA: - this.colorMode = COLOR_FILTER.NONE; - break; + toggleFullscreen() { + this.isFullscreen = this.readerService.checkFullscreenMode(); + if (this.isFullscreen) { + this.readerService.exitFullscreen(() => { + this.isFullscreen = false; + this.firstPageRendered = false; + this.render(); + }); + } else { + this.readerService.enterFullscreen(this.reader.nativeElement, () => { + this.isFullscreen = true; + this.firstPageRendered = false; + this.render(); + }); } } + toggleReaderMode() { switch(this.readerMode) { case READER_MODE.MANGA_LR: @@ -1163,4 +1163,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.toastr.info('Incognito mode is off. Progress will now start being tracked.'); this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); } + + getWindowDimensions() { + const windowWidth = window.innerWidth + || document.documentElement.clientWidth + || document.body.clientWidth; + const windowHeight = window.innerHeight + || document.documentElement.clientHeight + || document.body.clientHeight; + return [windowWidth, windowHeight]; + } } 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 7763ddb55..a25ea961f 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 @@ -2,7 +2,7 @@

User Dashboard


-

Books

+

Book Reader

@@ -166,53 +166,8 @@ - - - -
- - -
-
- - -

Change your Password

- - -
- - -
-
- This field is required -
-
-
-
- - -
-
- Passwords must match -
-
- This field is required -
-
-
-
- - -
- -
- -

Authentication is disabled on this server. A password is not required to authenticate.

-
-
-
+ + + +

Change your Password

+ +
+
+ + +
+
+ This field is required +
+
+
+
+ + +
+
+ Passwords must match +
+
+ This field is required +
+
+
+
+ + +
+
+
+ +

Authentication is disabled on this server. A password is not required to authenticate.

+
+
+ +

All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.

+ + + +
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 ba4c17865..e7947886b 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 @@ -55,6 +55,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { tabs: Array<{title: string, fragment: string}> = [ {title: 'Preferences', fragment: ''}, {title: 'Bookmarks', fragment: 'bookmarks'}, + {title: 'Password', fragment: 'password'}, + {title: '3rd Party Clients', fragment: 'clients'}, ]; active = this.tabs[0]; opdsEnabled: boolean = false; diff --git a/UI/Web/src/app/user-settings/user-settings.module.ts b/UI/Web/src/app/user-settings/user-settings.module.ts index 5be4cfab8..5b837aba0 100644 --- a/UI/Web/src/app/user-settings/user-settings.module.ts +++ b/UI/Web/src/app/user-settings/user-settings.module.ts @@ -7,6 +7,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NgxSliderModule } from '@angular-slider/ngx-slider'; import { UserSettingsRoutingModule } from './user-settings-routing.module'; import { ApiKeyComponent } from './api-key/api-key.component'; +import { SharedModule } from '../shared/shared.module'; @@ -24,6 +25,7 @@ import { ApiKeyComponent } from './api-key/api-key.component'; NgbTooltipModule, NgxSliderModule, UserSettingsRoutingModule, + SharedModule // SentenceCase pipe ] }) export class UserSettingsModule { }