diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 413a47cb0..b1f8d98b9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -99,6 +99,7 @@ public class UsersController : BaseApiController existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate; existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; + existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle; existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName; existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode; existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 41dbccf60..ae8a71f66 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -93,6 +93,12 @@ public class UserPreferencesDto [Required] public ReadingDirection BookReaderReadingDirection { get; set; } + /// + /// Book Reader Option: What writing style should be used, horizontal or vertical. + /// + [Required] + public WritingStyle BookReaderWritingStyle { get; set; } + /// /// UI Site Global Setting: The UI theme the user should use. /// diff --git a/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs b/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs new file mode 100644 index 000000000..37cc255ae --- /dev/null +++ b/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs @@ -0,0 +1,1854 @@ +// +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("20230304202540_BookWritingStylePref")] + partial class BookWritingStylePref + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .HasColumnType("INTEGER"); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .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("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + 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("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .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("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInDashboard") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInSearch") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .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("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Devices"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("UserProgress"); + }); + + 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/20230304202540_BookWritingStylePref.cs b/API/Data/Migrations/20230304202540_BookWritingStylePref.cs new file mode 100644 index 000000000..fd6703060 --- /dev/null +++ b/API/Data/Migrations/20230304202540_BookWritingStylePref.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class BookWritingStylePref : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BookReaderWritingStyle", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BookReaderWritingStyle", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 980463794..bffad6524 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -221,6 +221,9 @@ namespace API.Data.Migrations b.Property("BookReaderReadingDirection") .HasColumnType("INTEGER"); + b.Property("BookReaderWritingStyle") + .HasColumnType("INTEGER"); + b.Property("BookReaderTapToPaginate") .HasColumnType("INTEGER"); diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 828c19f72..835ac2cda 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -76,6 +76,10 @@ public class AppUserPreferences /// public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; + /// + /// Book Reader Option: Defines the writing styles vertical/horizontal + /// + public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal; /// /// UI Site Global Setting: The UI theme the user should use. /// diff --git a/API/Entities/Enums/WritingStyle.cs b/API/Entities/Enums/WritingStyle.cs new file mode 100644 index 000000000..b2e086599 --- /dev/null +++ b/API/Entities/Enums/WritingStyle.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; + +namespace API.Entities.Enums; + +/// +/// Represents the writing styles for the book-reader +/// +public enum WritingStyle +{ + /// + /// Vertical writing style for the book-reader + /// + [Description ("Vertical")] + Vertical = 0, + /// + /// Horizontal writing style for the book-reader + /// + [Description ("Horizontal")] + Horizontal = 1 +} diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 17b30f672..b80f946f4 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -7,6 +7,7 @@ import { ReaderMode } from './reader-mode'; import { ReadingDirection } from './reading-direction'; import { ScalingOption } from './scaling-option'; import { SiteTheme } from './site-theme'; +import {WritingStyle} from "./writing-style"; export interface Preferences { // Manga Reader @@ -28,6 +29,7 @@ export interface Preferences { bookReaderFontFamily: string; bookReaderTapToPaginate: boolean; bookReaderReadingDirection: ReadingDirection; + bookReaderWritingStyle: WritingStyle; bookReaderThemeName: string; bookReaderLayoutMode: BookPageLayoutMode; bookReaderImmersiveMode: boolean; @@ -41,6 +43,7 @@ export interface Preferences { } export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}]; +export const bookWritingStyles = [{text: 'Horizontal', value: WritingStyle.Horizontal}, {text: 'Vertical', value: WritingStyle.Vertical}]; export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}]; export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}]; export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}]; diff --git a/UI/Web/src/app/_models/preferences/writing-style.ts b/UI/Web/src/app/_models/preferences/writing-style.ts new file mode 100644 index 000000000..e4c031fef --- /dev/null +++ b/UI/Web/src/app/_models/preferences/writing-style.ts @@ -0,0 +1,7 @@ +/* + * Mode the user is reading the book in. Not applicable with ReaderMode.Webtoon + */ +export enum WritingStyle{ + Vertical = 0, + Horizontal = 1 +} diff --git a/UI/Web/src/app/_services/scroll.service.ts b/UI/Web/src/app/_services/scroll.service.ts index 7e786f7ed..3a0837962 100644 --- a/UI/Web/src/app/_services/scroll.service.ts +++ b/UI/Web/src/app/_services/scroll.service.ts @@ -24,22 +24,31 @@ export class ScrollService { } get scrollPosition() { - return (window.pageYOffset - || document.documentElement.scrollTop + return (window.pageYOffset + || document.documentElement.scrollTop || document.body.scrollTop || 0); } - scrollTo(top: number, el: Element | Window = window) { + /* + * When in the scroll vertical position the scroll in the horizontal position is needed + */ + get scrollPositionX() { + return (window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft || 0); + } + + scrollTo(top: number, el: Element | Window = window, behavior: 'auto' | 'smooth' = 'smooth') { el.scroll({ top: top, - behavior: 'smooth' + behavior: behavior }); } - - scrollToX(left: number, el: Element | Window = window) { + + scrollToX(left: number, el: Element | Window = window, behavior: 'auto' | 'smooth' = 'auto') { el.scroll({ left: left, - behavior: 'auto' + behavior: behavior }); } diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index d894c5bd8..4a61ac405 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -1,4 +1,4 @@ -
+
Skip to main content @@ -52,6 +52,7 @@ (styleUpdate)="updateReaderStyles($event)" (clickToPaginateChanged)="showPaginationOverlay($event)" (fullscreen)="toggleFullscreen()" + (bookReaderWritingStyle)="updateWritingStyle($event)" (layoutModeUpdate)="updateLayoutMode($event)" (readingDirection)="updateReadingDirection($event)" (immersiveMode)="updateImmersiveMode($event)" @@ -72,26 +73,25 @@
-
- +
+
-
+
-
- -
+
-
diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss index 3b2d1184b..0183f0b3b 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss @@ -87,6 +87,7 @@ $action-bar-height: 38px; .fixed-top { z-index: 1022; + direction: ltr; } .dark-mode .overlay { @@ -95,7 +96,7 @@ $action-bar-height: 38px; .action-bar { - background-color: var(--br-actionbar-bg-color); + background-color: var(--br-actionbar-bg-color); overflow: hidden; box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%); max-height: $action-bar-height; @@ -109,13 +110,13 @@ $action-bar-height: 38px; -webkit-box-orient: vertical; overflow: hidden; } - + @media(max-width: 875px) { .book-title { display: none; } - } - + } + .book-title { margin-top: 10px; text-align: center; @@ -142,6 +143,10 @@ $action-bar-height: 38px; &.column-layout-2 { height: calc(var(--vh) * 100); } + + &.writing-style-vertical { + direction: rtl; + } } .reading-section { @@ -151,6 +156,7 @@ $action-bar-height: 38px; padding-top: $action-bar-height; padding-bottom: $action-bar-height; position: relative; + direction: ltr; //background-color: green !important; @@ -167,12 +173,18 @@ $action-bar-height: 38px; //padding-top: 0px; //padding-bottom: 0px; } + + &.writing-style-vertical { + writing-mode: vertical-rl; + height: 100%; + } } .book-container { position: relative; height: 100%; + //background-color: purple !important; &.column-layout-1 { @@ -182,6 +194,12 @@ $action-bar-height: 38px; &.column-layout-2 { height: calc((var(--vh, 1vh) * 100) - $action-bar-height); } + + &.writing-style-vertical { + // Fixes an issue where chrome will cut of margins, doesn't seem to affect other browsers + overflow: auto; + } + } .book-content { @@ -192,10 +210,22 @@ $action-bar-height: 38px; &.column-layout-1 { height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2 + + &.writing-style-vertical { + height: auto; + padding: 0 10px 0 0; + margin: 20px 0; + } } &.column-layout-2 { height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2 + + &.writing-style-vertical { + height: auto; + padding: 0 10px 0 0; + margin: 20px 0; + } } // &.immersive { @@ -228,6 +258,8 @@ $action-bar-height: 38px; position: fixed; width: 100%; bottom: 0px; + left: 0px; + writing-mode: horizontal-tb; } @@ -242,7 +274,7 @@ $action-bar-height: 38px; overflow-wrap: break-word; } - + } .column-layout-2 { @@ -254,7 +286,7 @@ $action-bar-height: 38px; overflow-wrap: break-word; } - + } @@ -293,7 +325,7 @@ $action-bar-height: 38px; position: absolute; right: 0px; top: $action-bar-height; - width: 20%; + width: 20vw; z-index: 3; cursor: pointer; background: transparent; @@ -330,7 +362,7 @@ $action-bar-height: 38px; position: absolute; left: 0px; top: $action-bar-height; - width: 20%; + width: 20vw; background: transparent; border-color: transparent; border: none !important; @@ -342,6 +374,7 @@ $action-bar-height: 38px; &.immersive { top: 0px; } + } @@ -360,7 +393,7 @@ $action-bar-height: 38px; .btn { &.btn-secondary { - color: var(--br-actionbar-button-text-color); + color: var(--br-actionbar-button-text-color); border-color: transparent; background-color: unset; @@ -368,7 +401,7 @@ $action-bar-height: 38px; border-color: var(--br-actionbar-button-hover-border-color); } } - + &.btn-outline-secondary { border-color: transparent; background-color: unset; @@ -380,7 +413,7 @@ $action-bar-height: 38px; span { background-color: unset; - color: var(--br-actionbar-button-text-color); + color: var(--br-actionbar-button-text-color); } i { diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index ac2086c7f..4d5465c29 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -17,6 +17,7 @@ import { animate, state, style, transition, trigger } from '@angular/animations' import { Stack } from 'src/app/shared/data-structures/stack'; import { MemberService } from 'src/app/_services/member.service'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; +import {WritingStyle} from "../../../_models/preferences/writing-style"; import { MangaFormat } from 'src/app/_models/manga-format'; import { LibraryService } from 'src/app/_services/library.service'; import { LibraryType } from 'src/app/_models/library'; @@ -42,11 +43,12 @@ interface HistoryPoint { /** * XPath to scroll to */ - scrollPart: string; + scrollPart: string; } const TOP_OFFSET = -50 * 1.5; // px the sticky header takes up // TODO: Do I need this or can I change it with new fixed top height +const COLUMN_GAP = 20; // px /** * Styles that should be applied on the top level book-content tag */ @@ -201,6 +203,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { * Used for showing/hiding bottom action bar. Calculates if there is enough scroll to show it. * Will hide if all content in book is absolute positioned */ + + horizontalScrollbarNeeded = false; scrollbarNeeded = false; readingDirection: ReadingDirection = ReadingDirection.LeftToRight; clickToPaginate = false; @@ -250,6 +254,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; + writingStyle: WritingStyle = WritingStyle.Horizontal; + private readonly onDestroy = new Subject(); @ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef; @@ -346,26 +352,39 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } get ColumnWidth() { + const base = this.writingStyle === WritingStyle.Vertical ? this.windowHeight : this.windowWidth; switch (this.layoutMode) { case BookPageLayoutMode.Default: return 'unset'; case BookPageLayoutMode.Column1: - return (this.windowWidth /2) + 'px'; + return (base / 2) + 'px'; case BookPageLayoutMode.Column2: - return ((this.windowWidth / 4)) + 'px'; + return (base / 4) + 'px'; + default: + return 'unset'; } } get ColumnHeight() { - if (this.layoutMode !== BookPageLayoutMode.Default) { + if (this.layoutMode !== BookPageLayoutMode.Default || this.writingStyle === WritingStyle.Vertical) { // Take the height after page loads, subtract the top/bottom bar const height = this.windowHeight - (this.topOffset * 2); this.document.documentElement.style.setProperty('--book-reader-content-max-height', `${height}px`); return height + 'px'; } + return 'unset'; } + get VerticalBookContentWidth() { + if (this.layoutMode !== BookPageLayoutMode.Default && this.writingStyle !== WritingStyle.Horizontal ) { + const width = this.getVerticalPageWidth() + this.document.documentElement.style.setProperty('--book-reader-content-max-width', `${width}px`); + return width + 'px'; + } + return ''; + } + get ColumnLayout() { switch (this.layoutMode) { case BookPageLayoutMode.Default: @@ -377,6 +396,22 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + get WritingStyle() { + switch (this.writingStyle) { + case WritingStyle.Horizontal: + return ''; + case WritingStyle.Vertical: + return 'writing-style-vertical'; + } + } + + get PageWidthForPagination() { + if (this.layoutMode === BookPageLayoutMode.Default && this.writingStyle === WritingStyle.Vertical && this.horizontalScrollbarNeeded) { + return 'unset'; + } + return '100%' + } + get PageHeightForPagination() { if (this.layoutMode === BookPageLayoutMode.Default) { // if the book content is less than the height of the container, override and return height of container for pagination area @@ -531,7 +566,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params}); return; } - + this.bookTitle = info.bookTitle; this.cdRef.markForCheck(); @@ -599,7 +634,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { onResize(){ // Update the window Height this.updateWidthAndHeightCalcs(); - const resumeElement = this.getFirstVisibleElementXPath(); if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) { this.scrollTo(resumeElement); // This works pretty well, but not perfect @@ -625,6 +659,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + onWheel(event: WheelEvent) { + // This allows the user to scroll the page horizontally without holding shift + if (this.layoutMode !== BookPageLayoutMode.Default || this.writingStyle !== WritingStyle.Vertical) { + return; + } + if (event.deltaY !== 0) { + event.preventDefault() + this.scrollService.scrollToX( event.deltaY + this.reader.nativeElement.scrollLeft, this.reader.nativeElement); + } +} + closeReader() { this.readerService.closeReader(this.readingListMode, this.readingListId); } @@ -674,7 +719,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.isLoading = false; this.cdRef.markForCheck(); return; - } + } if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId && !this.prevChapterPrefetched) { this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => { @@ -790,7 +835,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => { this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage this.cdRef.markForCheck(); - + setTimeout(() => { this.addLinkClickHandlers(); this.updateReaderStyles(this.pageStyles); @@ -817,19 +862,33 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ updateImagesWithHeight() { const images = this.readingSectionElemRef?.nativeElement.querySelectorAll('img') || []; + let maxHeight: number | undefined; - if (this.layoutMode !== BookPageLayoutMode.Default) { - const height = (parseInt(this.ColumnHeight.replace('px', ''), 10) - (this.topOffset * 2)) + 'px'; - Array.from(images).forEach(img => { - this.renderer.setStyle(img, 'max-height', height); - }); + if (this.layoutMode !== BookPageLayoutMode.Default && this.writingStyle !== WritingStyle.Vertical) { + maxHeight = (parseInt(this.ColumnHeight.replace('px', ''), 10) - (this.topOffset * 2)); + } else if (this.layoutMode !== BookPageLayoutMode.Column2 && this.writingStyle === WritingStyle.Vertical) { + maxHeight = this.getPageHeight() - COLUMN_GAP; + } else if (this.layoutMode === BookPageLayoutMode.Column2 && this.writingStyle === WritingStyle.Vertical) { + maxHeight = this.getPageHeight() / 2 - COLUMN_GAP; } else { - Array.from(images).forEach(img => { - this.renderer.removeStyle(img, 'max-height'); - }); + maxHeight = undefined; } + Array.from(images).forEach(img => { + if (maxHeight === undefined) { + this.renderer.removeStyle(img, 'max-height'); + } else if (this.writingStyle === WritingStyle.Horizontal) { + this.renderer.setStyle(img, 'max-height', `${maxHeight}px`); + } else { + const aspectRatio = img.width / img.height; + const pageWidth = this.getVerticalPageWidth() + const maxImgHeight = Math.min(maxHeight, pageWidth / aspectRatio); + this.renderer.setStyle(img, 'max-height', `${maxImgHeight}px`); + } + }); + } + setupPage(part?: string | undefined, scrollTop?: number | undefined) { this.isLoading = false; this.cdRef.markForCheck(); @@ -846,11 +905,20 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.scrollTo(part); } else if (scrollTop !== undefined && scrollTop !== 0) { this.scrollService.scrollTo(scrollTop, this.reader.nativeElement); + } else if ((this.writingStyle === WritingStyle.Vertical) && (this.layoutMode === BookPageLayoutMode.Default)) { + setTimeout(()=> this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.clientWidth, this.reader.nativeElement)); } else { if (this.layoutMode === BookPageLayoutMode.Default) { this.scrollService.scrollTo(0, this.reader.nativeElement); - } else { + } else if (this.writingStyle === WritingStyle.Vertical) { + if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) { + setTimeout(() => this.scrollService.scrollTo(this.bookContentElemRef.nativeElement.scrollHeight, this.bookContentElemRef.nativeElement)); + } else { + setTimeout(() => this.scrollService.scrollTo(0, this.bookContentElemRef.nativeElement)); + } + } + else { this.reader.nativeElement.children // We need to check if we are paging back, because we need to adjust the scroll if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) { @@ -858,7 +926,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } else { setTimeout(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement)); } - } + } } // we need to click the document before arrow keys will scroll down. @@ -909,7 +977,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { /** * Given a direction, calls the next or prev page method - * @param direction Direction to move + * @param direction Direction to move */ movePage(direction: PAGING_DIRECTION) { if (direction === PAGING_DIRECTION.BACKWARDS) { @@ -931,7 +999,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (currentVirtualPage > 1) { // -2 apparently goes back 1 virtual page... - this.scrollService.scrollToX((currentVirtualPage - 2) * pageWidth, this.bookContentElemRef.nativeElement); + if (this.writingStyle === WritingStyle.Vertical) { + this.scrollService.scrollTo((currentVirtualPage - 2) * pageWidth, this.bookContentElemRef.nativeElement, "auto"); + } else { + this.scrollService.scrollToX((currentVirtualPage - 2) * pageWidth, this.bookContentElemRef.nativeElement); + } this.handleScrollEvent(); return; } @@ -956,14 +1028,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } this.pagingDirection = PAGING_DIRECTION.FORWARD; - // We need to handle virtual paging before we increment the actual page if (this.layoutMode !== BookPageLayoutMode.Default) { const [currentVirtualPage, totalVirtualPages, pageWidth] = this.getVirtualPage(); if (currentVirtualPage < totalVirtualPages) { // +0 apparently goes forward 1 virtual page... - this.scrollService.scrollToX((currentVirtualPage) * pageWidth, this.bookContentElemRef.nativeElement); + if (this.writingStyle === WritingStyle.Vertical) { + this.scrollService.scrollTo( (currentVirtualPage) * pageWidth, this.bookContentElemRef.nativeElement, 'auto'); + } else { + this.scrollService.scrollToX((currentVirtualPage) * pageWidth, this.bookContentElemRef.nativeElement); + } this.handleScrollEvent(); return; } @@ -985,47 +1060,75 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } /** - * + * * @returns Total Page width (excluding margin) */ getPageWidth() { if (this.readingSectionElemRef == null) return 0; - const margin = (this.readingSectionElemRef.nativeElement.clientWidth * (parseInt(this.pageStyles['margin-left'], 10) / 100)) * 2; - const columnGap = 20; - return this.readingSectionElemRef.nativeElement.clientWidth - margin + columnGap; + + return this.readingSectionElemRef.nativeElement.clientWidth - margin + COLUMN_GAP; + } + + getPageHeight() { + if (this.readingSectionElemRef == null) return 0; + const height = (parseInt(this.ColumnHeight.replace('px', ''), 10)); + + return height - COLUMN_GAP; + } + + getVerticalPageWidth() { + const margin = (window.innerWidth * (parseInt(this.pageStyles['margin-left'], 10) / 100)) * 2; + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + return windowWidth - margin; } /** * currentVirtualPage starts at 1 - * @returns + * @returns */ getVirtualPage() { - if (this.bookContentElemRef === undefined || this.readingSectionElemRef === undefined) return [1, 1, 0]; + if (!this.bookContentElemRef || !this.readingSectionElemRef) return [1, 1, 0]; - const scrollOffset = this.bookContentElemRef.nativeElement.scrollLeft; - const totalScroll = this.bookContentElemRef.nativeElement.scrollWidth; - const pageWidth = this.getPageWidth(); - const delta = totalScroll - scrollOffset; - - const totalVirtualPages = Math.max(1, Math.round((totalScroll) / pageWidth)); + const [scrollOffset, totalScroll] = this.getScrollOffsetAndTotalScroll(); + const pageSize = this.getPageSize(); + const totalVirtualPages = Math.max(1, Math.round(totalScroll / pageSize)); + const delta = scrollOffset - totalScroll; let currentVirtualPage = 1; - // If first virtual page, i.e. totalScroll and delta are the same value - if (totalScroll - delta === 0) { + //If first virtual page, i.e. totalScroll and delta are the same value + if (totalScroll === delta) { currentVirtualPage = 1; - // If second virtual page - } else if (totalScroll - delta === pageWidth) { + // If second virtual page + } else if (totalScroll - delta === pageSize) { currentVirtualPage = 2; - - // Otherwise do math to get correct page. i.e. scrollOffset + pageWidth (this accounts for first page offset) + // Otherwise do math to get correct page. i.e. scroll + pageHeight/pageWidth (this accounts for first page offset) } else { - currentVirtualPage = Math.min(Math.max(1, Math.round((scrollOffset + pageWidth) / pageWidth)), totalVirtualPages); - } + currentVirtualPage = Math.min(Math.max(1, Math.round((scrollOffset + pageSize) / pageSize)), totalVirtualPages); + } + + return [currentVirtualPage, totalVirtualPages, pageSize]; - return [currentVirtualPage, totalVirtualPages, pageWidth]; } + private getScrollOffsetAndTotalScroll() { + const { nativeElement: bookContent } = this.bookContentElemRef; + const scrollOffset = this.writingStyle === WritingStyle.Vertical + ? bookContent.scrollTop + : bookContent.scrollLeft; + const totalScroll = this.writingStyle === WritingStyle.Vertical + ? bookContent.scrollHeight + : bookContent.scrollWidth; + return [scrollOffset, totalScroll]; + } + + private getPageSize() { + return this.writingStyle === WritingStyle.Vertical + ? this.getPageHeight() + : this.getPageWidth(); + } + + getFirstVisibleElementXPath() { let resumeElement: string | null = null; if (this.bookContentElemRef === null) return null; @@ -1059,7 +1162,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { // Before we apply styles, let's get an element on the screen so we can scroll to it after any shifts const resumeElement: string | null | undefined = this.getFirstVisibleElementXPath(); - + // Needs to update the image size when reading mode is vertically + if (this.writingStyle === WritingStyle.Vertical) { + this.updateImagesWithHeight(); + } // Line Height must be placed on each element in the page // Apply page level overrides @@ -1097,7 +1203,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { /** * Applies styles and classes that control theme - * @param theme + * @param theme */ updateColorTheme(theme: BookTheme) { // Remove all themes @@ -1121,6 +1227,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { // Recalculate if bottom action bar is needed this.scrollbarNeeded = this.bookContentElemRef?.nativeElement?.clientHeight > this.reader?.nativeElement?.clientHeight; + this.horizontalScrollbarNeeded = this.bookContentElemRef?.nativeElement?.clientWidth > this.reader?.nativeElement?.clientWidth; this.cdRef.markForCheck(); } @@ -1148,7 +1255,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (element === null) return; - if (this.layoutMode === BookPageLayoutMode.Default) { + if(this.layoutMode === BookPageLayoutMode.Default && this.writingStyle === WritingStyle.Vertical ) { + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + const scrollLeft = element.getBoundingClientRect().left + window.pageXOffset - (windowWidth - element.getBoundingClientRect().width); + setTimeout(() => this.scrollService.scrollToX(scrollLeft, this.reader.nativeElement, 'smooth'), 10); + } + else if ((this.layoutMode === BookPageLayoutMode.Default) && (this.writingStyle === WritingStyle.Horizontal)) { const fromTopOffset = element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET; // We need to use a delay as webkit browsers (aka apple devices) don't always have the document rendered by this point setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10); @@ -1210,7 +1322,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.isFullscreen = true; this.cdRef.markForCheck(); // HACK: This is a bug with how browsers change the background color for fullscreen mode - this.renderer.setStyle(this.reader.nativeElement, 'background', this.themeService.getCssVariable('--bs-body-color')); + this.renderer.setStyle(this.reader.nativeElement, 'background', this.themeService.getCssVariable('--bs-body-color')); if (!this.darkMode) { this.renderer.setStyle(this.reader.nativeElement, 'background', 'white'); } @@ -1218,11 +1330,29 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + updateWritingStyle(writingStyle: WritingStyle) { + this.writingStyle = writingStyle; + if (this.layoutMode !== BookPageLayoutMode.Default) { + const lastSelector = this.lastSeenScrollPartPath; + setTimeout(() => { + this.scrollTo(lastSelector); + this.updateLayoutMode(this.layoutMode); + }); + } else if (this.bookContentElemRef !== undefined) { + const resumeElement = this.getFirstVisibleElementXPath(); + if (resumeElement) { + setTimeout(() => { + this.scrollTo(resumeElement); + }); + } + } + this.cdRef.markForCheck(); + } + updateLayoutMode(mode: BookPageLayoutMode) { const layoutModeChanged = mode !== this.layoutMode; this.layoutMode = mode; this.cdRef.markForCheck(); - // Remove any max-heights from column layout this.updateImagesWithHeight(); @@ -1233,10 +1363,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } setTimeout(() => { this.scrollbarNeeded = this.bookContentElemRef?.nativeElement?.clientHeight > this.reader?.nativeElement?.clientHeight; + this.horizontalScrollbarNeeded = this.bookContentElemRef?.nativeElement?.clientWidth > this.reader?.nativeElement?.clientWidth; this.cdRef.markForCheck(); }); - // When I switch layout, I might need to resume the progress point. + // When I switch layout, I might need to resume the progress point. if (mode === BookPageLayoutMode.Default && layoutModeChanged) { const lastSelector = this.lastSeenScrollPartPath; setTimeout(() => this.scrollTo(lastSelector)); @@ -1263,7 +1394,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { setTimeout(() => { if (renderer === undefined || elem === undefined) return; if (this.immersiveMode) { - renderer.setStyle(elem, 'height', 'calc(var(--vh, 1vh) * 100)', RendererStyleFlags2.Important); } else { renderer.setStyle(elem, 'height', 'calc(var(--vh, 1vh) * 100 - ' + this.topOffset + 'px)', RendererStyleFlags2.Important); } diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html index f28ce296e..30c5e8520 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html @@ -70,6 +70,16 @@  {{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}
+
+ + Changes the direction of the text. Horizontal is left to right, vertical is top to bottom. + + + +
Click the edges of the screen to paginate @@ -116,10 +126,10 @@
- + - +
@@ -150,4 +160,4 @@ - \ No newline at end of file + diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts index 0860c0b64..694e41857 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.ts @@ -5,6 +5,7 @@ import { Subject, take, takeUntil } from 'rxjs'; import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode'; import { BookTheme } from 'src/app/_models/preferences/book-theme'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; +import { WritingStyle } from 'src/app/_models/preferences/writing-style'; import { ThemeProvider } from 'src/app/_models/preferences/site-theme'; import { User } from 'src/app/_models/user'; import { AccountService } from 'src/app/_services/account.service'; @@ -88,6 +89,10 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { * Outputs when reading direction is changed */ @Output() readingDirection: EventEmitter = new EventEmitter(); + /** + * Outputs when reading mode is changed + */ + @Output() bookReaderWritingStyle: EventEmitter = new EventEmitter(); /** * Outputs when immersive mode is changed */ @@ -106,6 +111,9 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { readingDirectionModel: ReadingDirection = ReadingDirection.LeftToRight; + writingStyleModel: WritingStyle = WritingStyle.Horizontal; + + activeTheme: BookTheme | undefined; isFullscreen: boolean = false; @@ -129,6 +137,10 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { return ReadingDirection; } + get WritingStyle() { + return WritingStyle; + } + constructor(private bookService: BookService, private accountService: AccountService, @@ -160,7 +172,12 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { if (this.user.preferences.bookReaderReadingDirection === undefined) { this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight; } + if (this.user.preferences.bookReaderWritingStyle === undefined) { + this.user.preferences.bookReaderWritingStyle = WritingStyle.Horizontal; + } this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection; + this.writingStyleModel = this.user.preferences.bookReaderWritingStyle; + this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); @@ -194,11 +211,13 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, [])); this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { - this.pageStyles['margin-left'] = value + '%'; - this.pageStyles['margin-right'] = value + '%'; + this.pageStyles['margin-left'] = value + 'vw'; + this.pageStyles['margin-right'] = value + 'vw'; this.styleUpdate.emit(this.pageStyles); }); + + this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, [])); this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => { this.layoutModeUpdate.emit(layoutMode); @@ -218,6 +237,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { // Emit first time so book reader gets the setting this.readingDirection.emit(this.readingDirectionModel); + this.bookReaderWritingStyle.emit(this.writingStyleModel); this.clickToPaginateChanged.emit(this.user.preferences.bookReaderTapToPaginate); this.layoutModeUpdate.emit(this.user.preferences.bookReaderLayoutMode); this.immersiveMode.emit(this.user.preferences.bookReaderImmersiveMode); @@ -239,7 +259,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { resetSettings() { if (this.user) { - this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + '%', this.user.preferences.bookReaderLineSpacing + '%'); + this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + 'vw', this.user.preferences.bookReaderLineSpacing + '%'); } else { this.setPageStyles(); } @@ -252,6 +272,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate); this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.user.preferences.bookReaderLayoutMode); this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.user.preferences.bookReaderImmersiveMode); + this.settingsForm.get('bookReaderWritingStyle')?.setValue(this.user.preferences.bookReaderWritingStyle); this.cdRef.detectChanges(); this.styleUpdate.emit(this.pageStyles); } @@ -265,9 +286,9 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { || this.document.body.clientWidth; - let defaultMargin = '15%'; + let defaultMargin = '15vw'; if (windowWidth <= mobileBreakpointMarginOverride) { - defaultMargin = '5%'; + defaultMargin = '5vw'; } this.pageStyles = { 'font-family': fontFamily || this.pageStyles['font-family'] || 'default', @@ -296,6 +317,17 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy { this.readingDirection.emit(this.readingDirectionModel); } + toggleWritingStyle() { + if (this.writingStyleModel === WritingStyle.Horizontal) { + this.writingStyleModel = WritingStyle.Vertical + } else { + this.writingStyleModel = WritingStyle.Horizontal + } + + this.cdRef.markForCheck(); + this.bookReaderWritingStyle.emit(this.writingStyleModel); + } + toggleFullscreen() { this.isFullscreen = !this.isFullscreen; this.cdRef.markForCheck(); 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 c92d9c08f..fe9a594d8 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 @@ -47,7 +47,7 @@
- + Blurs summary text on volumes or chapters that have no read progress (to avoid spoilers) Blurs summary text on volumes or chapters that have no read progress (to avoid spoilers)
@@ -59,7 +59,7 @@
- + Prompt when a download exceedes 100MB in size Prompt when a download exceedes 100MB in size
@@ -142,11 +142,12 @@
+ class="form-control" + id="settings-backgroundcolor-option" + (colorPickerChange)="handleBackgroundColorChange()" + [style.background]="user.preferences.backgroundColor" + [cpAlphaChannel]="'disabled'" + [(colorPicker)]="user.preferences.backgroundColor"/>
@@ -251,6 +252,15 @@
+
+ + Changes the direction of the text. Horizontal is left to right, vertical is top to bottom. + + +
+
  How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page @@ -259,26 +269,28 @@
+
+
-   - What color theme to apply to the book reader content and menuing - - -
+   + What color theme to apply to the book reader content and menuing + + +
- {{settingsForm.get('bookReaderFontSize')?.value + '%'}}
- +
@@ -286,7 +298,7 @@ How much spacing between the lines of the book How much spacing between the lines of the book
- {{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}
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 04ac999a3..07e2cfd95 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 @@ -3,7 +3,17 @@ import { FormControl, FormGroup } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; import { take, takeUntil } from 'rxjs/operators'; import { Title } from '@angular/platform-browser'; -import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes, pageLayoutModes } from 'src/app/_models/preferences/preferences'; +import { + readingDirections, + scalingOptions, + pageSplitOptions, + readingModes, + Preferences, + bookLayoutModes, + layoutModes, + pageLayoutModes, + bookWritingStyles +} from 'src/app/_models/preferences/preferences'; import { User } from 'src/app/_models/user'; import { AccountService } from 'src/app/_services/account.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -45,6 +55,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { bookLayoutModes = bookLayoutModes; bookColorThemes = bookColorThemes; pageLayoutModes = pageLayoutModes; + bookWritingStyles = bookWritingStyles; settingsForm: FormGroup = new FormGroup({}); user: User | undefined = undefined; @@ -134,6 +145,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, [])); this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, [])); this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, [])); + this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, [])) 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, [])); @@ -179,6 +191,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.get('bookReaderMargin')?.setValue(this.user.preferences.bookReaderMargin); this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate); this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.user.preferences.bookReaderReadingDirection); + this.settingsForm.get('bookReaderWritingStyle')?.setValue(this.user.preferences.bookReaderWritingStyle); 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); @@ -211,6 +224,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { bookReaderMargin: modelSettings.bookReaderMargin, bookReaderTapToPaginate: modelSettings.bookReaderTapToPaginate, bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10), + bookReaderWritingStyle: parseInt(modelSettings.bookReaderWritingStyle, 10), bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10), bookReaderThemeName: modelSettings.bookReaderThemeName, theme: modelSettings.theme,