From 5e9bbd076855c5c682be77ee47426be9f720bc73 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Mon, 30 Jan 2023 00:50:19 -0800 Subject: [PATCH] Holiday Bugfixes (#1762) * Don't show "not much going on" when we are actively downloading * Swipe to paginate is now behind a flag in the user preferences. * Added a new server setting for host name, if the server sits behind a reverse proxy. If this is set, email link generation will use it and will not perform any checks on accessibility (thus email will always send) * Refactored the code that checks if the server is accessible to check if host name is set, and thus return rue if so. * Added back the system drawing library for markdown parsing. * Fixed a validation error * Fixed a bug where folder watching could get re-triggered when it was disabled at a server level. * Made the manga reader loader absolute positioned for better visibility * Indentation --- API/API.csproj | 1 + API/Controllers/AccountController.cs | 52 +- API/Controllers/ServerController.cs | 7 +- API/Controllers/SettingsController.cs | 9 +- API/Controllers/UsersController.cs | 1 + API/DTOs/Settings/ServerSettingDTO.cs | 4 + API/DTOs/UserPreferencesDto.cs | 5 + ...0129210741_SwipeToPaginatePref.Designer.cs | 1746 +++++++++++++++++ .../20230129210741_SwipeToPaginatePref.cs | 26 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Data/Seed.cs | 1 + API/Entities/AppUserPreferences.cs | 8 +- API/Entities/Enums/ServerSettingKey.cs | 6 + .../Converters/ServerSettingConverter.cs | 3 + API/Services/AccountService.cs | 38 +- API/Services/Tasks/Scanner/LibraryWatcher.cs | 13 +- .../app/_models/preferences/preferences.ts | 1 + .../src/app/admin/_models/server-settings.ts | 1 + .../manage-settings.component.html | 15 +- .../manage-settings.component.ts | 2 + .../manage-users/manage-users.component.ts | 2 - .../manga-reader/manga-reader.component.html | 15 +- .../manga-reader/manga-reader.component.ts | 13 +- .../events-widget.component.html | 6 +- .../events-widget/events-widget.component.ts | 3 +- UI/Web/src/app/pipe/time-ago.pipe.ts | 2 +- .../app/shared/loading/loading.component.html | 16 +- .../app/shared/loading/loading.component.ts | 5 + .../user-preferences.component.html | 6 + .../user-preferences.component.ts | 5 +- openapi.json | 20 +- 31 files changed, 1986 insertions(+), 49 deletions(-) create mode 100644 API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs create mode 100644 API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs diff --git a/API/API.csproj b/API/API.csproj index c1d79a8c8..24df972e2 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -96,6 +96,7 @@ + diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 2eef78157..a9229fa7c 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -324,10 +324,11 @@ public class AccountController : BaseApiController // Send a confirmation email try { - var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email-update", dto.Email); + var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString(); - var accessible = await _emailService.CheckIfAccessible(host); + + + var accessible = await _accountService.CheckIfAccessible(Request); if (accessible) { try @@ -495,7 +496,7 @@ public class AccountController : BaseApiController if (string.IsNullOrEmpty(user.ConfirmationToken)) return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite."); - return GenerateEmailLink(user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl); + return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl); } @@ -603,11 +604,10 @@ public class AccountController : BaseApiController try { - var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email", dto.Email); + var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); _logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken); - var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString(); - var accessible = await _emailService.CheckIfAccessible(host); + var accessible = await _accountService.CheckIfAccessible(Request); if (accessible) { try @@ -795,10 +795,9 @@ public class AccountController : BaseApiController return BadRequest("You do not have an email on account or it has not been confirmed"); var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email); + var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString(); - if (await _emailService.CheckIfAccessible(host)) + if (await _accountService.CheckIfAccessible(Request)) { await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto() { @@ -851,6 +850,11 @@ public class AccountController : BaseApiController }; } + /// + /// Resend an invite to a user already invited + /// + /// + /// [HttpPost("resend-confirmation-email")] public async Task> ResendConfirmationSendEmail([FromQuery] int userId) { @@ -863,26 +867,22 @@ public class AccountController : BaseApiController if (user.EmailConfirmed) return BadRequest("User already confirmed"); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - var emailLink = GenerateEmailLink(token, "confirm-email", user.Email); + var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email); _logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink); _logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token); - await _emailService.SendMigrationEmail(new EmailMigrationDto() + if (await _accountService.CheckIfAccessible(Request)) { - EmailAddress = user.Email, - Username = user.UserName, - ServerConfirmationLink = emailLink, - InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value - }); + await _emailService.SendMigrationEmail(new EmailMigrationDto() + { + EmailAddress = user.Email, + Username = user.UserName, + ServerConfirmationLink = emailLink, + InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value + }); + return Ok(emailLink); + } - - return Ok(emailLink); - } - - private string GenerateEmailLink(string token, string routePart, string email, bool withHost = true) - { - var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString(); - if (withHost) return $"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; - return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; + return Ok("The server is not accessible externally. Please "); } /// diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 29668cc78..0cae64323 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -36,10 +36,11 @@ public class ServerController : BaseApiController private readonly IEmailService _emailService; private readonly IBookmarkService _bookmarkService; private readonly IScannerService _scannerService; + private readonly IAccountService _accountService; public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, - ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService) + ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService) { _applicationLifetime = applicationLifetime; _logger = logger; @@ -51,6 +52,7 @@ public class ServerController : BaseApiController _emailService = emailService; _bookmarkService = bookmarkService; _scannerService = scannerService; + _accountService = accountService; } /// @@ -189,12 +191,13 @@ public class ServerController : BaseApiController /// /// Is this server accessible to the outside net /// + /// If the instance has the HostName set, this will return true whether or not it is accessible externally /// [HttpGet("accessible")] [AllowAnonymous] public async Task> IsServerAccessible() { - return await _emailService.CheckIfAccessible(Request.Host.ToString()); + return Ok(await _accountService.CheckIfAccessible(Request)); } [HttpGet("jobs")] diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 8bc273741..59006c911 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -104,7 +104,7 @@ public class SettingsController : BaseApiController [HttpPost] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { - _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); + _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); // We do not allow CacheDirectory changes, so we will ignore. var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); @@ -182,6 +182,13 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } + if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value) + { + setting.Value = (updateSettingsDto.HostName + string.Empty).Trim(); + if (setting.Value.EndsWith("/")) setting.Value = setting.Value.Substring(0, setting.Value.Length - 1); + _unitOfWork.SettingsRepository.Update(setting); + } + if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) { diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index d23e6facb..6fd8249e4 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -111,6 +111,7 @@ public class UsersController : BaseApiController existingPreferences.LayoutMode = preferencesDto.LayoutMode; existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; + existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; _unitOfWork.UserRepository.Update(existingPreferences); diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 18af645e4..fbec533b9 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -66,4 +66,8 @@ public class ServerSettingDto /// If the server should save covers as WebP encoding /// public bool ConvertCoverToWebP { get; set; } + /// + /// The Host name (ie Reverse proxy domain name) for the server + /// + public string HostName { get; set; } } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 5667f1634..b756534cb 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -46,6 +46,11 @@ public class UserPreferencesDto /// [Required] public string BackgroundColor { get; set; } = "#000000"; + [Required] + /// + /// Manga Reader Option: Should swiping trigger pagination + /// + public bool SwipeToPaginate { get; set; } /// /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction /// diff --git a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs b/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs new file mode 100644 index 000000000..ea948ab31 --- /dev/null +++ b/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs @@ -0,0 +1,1746 @@ +// +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("20230129210741_SwipeToPaginatePref")] + partial class SwipeToPaginatePref + { + 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("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("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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("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("LastModified") + .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("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .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("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("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("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("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("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("LastModified") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("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("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("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("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.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"); + }); + + 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/20230129210741_SwipeToPaginatePref.cs b/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs new file mode 100644 index 000000000..0f99c4c26 --- /dev/null +++ b/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class SwipeToPaginatePref : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SwipeToPaginate", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SwipeToPaginate", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 3ebad1105..e38aac7fb 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -249,6 +249,9 @@ namespace API.Data.Migrations b.Property("ShowScreenHints") .HasColumnType("INTEGER"); + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + b.Property("ThemeId") .HasColumnType("INTEGER"); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 3eaccb211..9a5f65ec0 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -102,6 +102,7 @@ public static class Seed new() {Key = ServerSettingKey.TotalLogs, Value = "30"}, new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"}, new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"}, + new() {Key = ServerSettingKey.HostName, Value = string.Empty}, }.ToArray()); foreach (var defaultSetting in DefaultSettings) diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 9e7b65df7..60ed4a55c 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -1,4 +1,5 @@ -using API.Entities.Enums; +using System; +using API.Entities.Enums; using API.Entities.Enums.UserPreferences; namespace API.Entities; @@ -26,7 +27,6 @@ public class AppUserPreferences /// /// public ReaderMode ReaderMode { get; set; } - /// /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction /// @@ -48,6 +48,10 @@ public class AppUserPreferences /// public string BackgroundColor { get; set; } = "#000000"; /// + /// Manga Reader Option: Should swiping trigger pagination + /// + public bool SwipeToPaginate { get; set; } + /// /// Book Reader Option: Override extra Margin /// public int BookReaderMargin { get; set; } = 15; diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index 352f8dd70..8772ee075 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -105,4 +105,10 @@ public enum ServerSettingKey /// [Description("ConvertCoverToWebP")] ConvertCoverToWebP = 19, + /// + /// The Host name (ie Reverse proxy domain name) for the server. Used for email link generation + /// + [Description("HostName")] + HostName = 20, + } diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 7c2b23570..df983021c 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -66,6 +66,9 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.TotalLogs: destination.TotalLogs = int.Parse(row.Value); break; + case ServerSettingKey.HostName: + destination.HostName = row.Value; + break; } } diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 0d8bed66c..28ac98cf3 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -2,12 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Web; using API.Constants; using API.Data; using API.Entities; using API.Errors; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services; @@ -20,6 +23,8 @@ public interface IAccountService Task> ValidateEmail(string email); Task HasBookmarkPermission(AppUser user); Task HasDownloadPermission(AppUser user); + Task CheckIfAccessible(HttpRequest request); + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); } public class AccountService : IAccountService @@ -27,13 +32,44 @@ public class AccountService : IAccountService private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; + private readonly IHostEnvironment _environment; + private readonly IEmailService _emailService; public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; + private const string LocalHost = "localhost:4200"; - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork) + public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, + IHostEnvironment environment, IEmailService emailService) { _userManager = userManager; _logger = logger; _unitOfWork = unitOfWork; + _environment = environment; + _emailService = emailService; + } + + /// + /// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name. + /// + /// + /// + public async Task CheckIfAccessible(HttpRequest request) + { + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host); + } + + public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) + { + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + var basePart = $"{request.Scheme}://{host}{request.PathBase}/"; + if (!string.IsNullOrEmpty(serverSettings.HostName)) + { + basePart = serverSettings.HostName; + } + + if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; + return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; } public async Task> ChangeUserPassword(AppUser user, string newPassword) diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 6cfafa575..1f3a65dc5 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -74,7 +74,14 @@ public class LibraryWatcher : ILibraryWatcher public async Task StartWatching() { - _logger.LogInformation("[LibraryWatcher] Starting file watchers"); + FileWatchers.Clear(); + WatcherDictionary.Clear(); + + if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching) + { + _logger.LogInformation("Folder watching is disabled at the server level, thus ignoring any requests to create folder watching"); + return; + } var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()) .Where(l => l.FolderWatching) @@ -84,6 +91,8 @@ public class LibraryWatcher : ILibraryWatcher .Where(_directoryService.Exists) .ToList(); + _logger.LogInformation("[LibraryWatcher] Starting file watchers for {Count} library folders", libraryFolders.Count); + foreach (var libraryFolder in libraryFolders) { _logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder); @@ -107,7 +116,7 @@ public class LibraryWatcher : ILibraryWatcher WatcherDictionary[libraryFolder].Add(watcher); } - _logger.LogInformation("[LibraryWatcher] Watching {Count} folders", FileWatchers.Count); + _logger.LogInformation("[LibraryWatcher] Watching {Count} folders", libraryFolders.Count); } public void StopWatching() diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index e3882730f..17b30f672 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -19,6 +19,7 @@ export interface Preferences { backgroundColor: string; showScreenHints: boolean; emulateBook: boolean; + swipeToPaginate: boolean; // Book Reader bookReaderMargin: number; diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index b5ecd1248..cb48d3a5f 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -14,4 +14,5 @@ export interface ServerSettings { totalBackups: number; totalLogs: number; enableFolderWatching: boolean; + hostName: string; } diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 5071e16df..7e950c153 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -1,6 +1,6 @@
-

Port and Swagger require a manual restart of Kavita to take effect.

+

Changing Port requires a manual restart of Kavita to take effect.

  Where the server place temporary files when reading. This will be cleaned up on a regular basis. @@ -20,6 +20,19 @@
+
+   + Domain Name (of Reverse Proxy). If set, email generation will always use this. + Domain Name (of Reverse Proxy). If set, email generation will always use this. + +
+
+ Host name must start with http(s) and not end in / +
+
+
+
  diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 54be134c0..461543bf5 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -51,6 +51,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)])); this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required])); this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, [])); + this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)])); }); } @@ -69,6 +70,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.get('totalLogs')?.setValue(this.serverSettings.totalLogs); this.settingsForm.get('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching); this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP); + this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName); this.settingsForm.markAsPristine(); } diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index 5855fdc18..3bb43a2d2 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -134,14 +134,12 @@ export class ManageUsersComponent implements OnInit, OnDestroy { } await this.confirmService.alert( 'Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking.
' + email + ''); - }); }); } setup(member: Member) { this.accountService.getInviteUrl(member.id, false).subscribe(url => { - console.log('Invite Url: ', url); if (url) { this.router.navigateByUrl(url); } diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index b7e9228ae..e83fe3e87 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -26,7 +26,7 @@
- +
@@ -233,6 +233,15 @@
+ +
+
+
+ + +
+
+
@@ -251,8 +260,10 @@
+ +
- +
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index 54f651a79..587408ba2 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -467,7 +467,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)), layoutMode: new FormControl(this.layoutMode), darkness: new FormControl(100), - emulateBook: new FormControl(this.user.preferences.emulateBook) + emulateBook: new FormControl(this.user.preferences.emulateBook), + swipeToPaginate: new FormControl(this.user.preferences.swipeToPaginate) }); this.readerModeSubject.next(this.readerMode); @@ -973,6 +974,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } triggerSwipePagination(direction: KeyDirection) { + if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return; + switch(direction) { case KeyDirection.Down: this.nextPage(); @@ -1097,6 +1100,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } this.resetSwipeModifiers(); + + this.isLoading = true; + this.cdRef.markForCheck(); this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); @@ -1125,6 +1131,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.resetSwipeModifiers(); + this.isLoading = true; + this.cdRef.markForCheck(); + this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); @@ -1241,6 +1250,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { // Originally this was only for fit to height, but when swiping was introduced, it made more sense to do it always to reset to the same view this.readingArea.nativeElement.scroll(0,0); + this.isLoading = false; this.cdRef.markForCheck(); } @@ -1601,6 +1611,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { data.autoCloseMenu = this.autoCloseMenu; data.readingDirection = this.readingDirection; data.emulateBook = modelSettings.emulateBook; + data.swipeToPaginate = modelSettings.swipeToPaginate; this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { this.toastr.success('User preferences updated'); if (this.user) { diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.html b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.html index a456943bb..4d131097e 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.html +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.html @@ -165,9 +165,13 @@
  • {{onlineUsers.length}} Users online
  • -
  • Not much going on here
  • Active Events: {{activeEvents}}
  • + + +
  • Not much going on here
  • +
    + \ No newline at end of file diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts index 9d6672279..dfb47c9b9 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts @@ -55,7 +55,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { constructor(public messageHub: MessageHubService, private modalService: NgbModal, private accountService: AccountService, private confirmService: ConfirmService, - private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) { } + private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) { + } ngOnDestroy(): void { this.onDestroy.next(); diff --git a/UI/Web/src/app/pipe/time-ago.pipe.ts b/UI/Web/src/app/pipe/time-ago.pipe.ts index da6546df0..c67b51b41 100644 --- a/UI/Web/src/app/pipe/time-ago.pipe.ts +++ b/UI/Web/src/app/pipe/time-ago.pipe.ts @@ -42,7 +42,7 @@ export class TimeAgoPipe implements PipeTransform, OnDestroy { const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000)); const timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) * 1000; - this.timer = this.ngZone.runOutsideAngular(() => { + this.timer = this.ngZone.runOutsideAngular(() => { if (typeof window !== 'undefined') { return window.setTimeout(() => { this.ngZone.run(() => this.changeDetectorRef.markForCheck()); diff --git a/UI/Web/src/app/shared/loading/loading.component.html b/UI/Web/src/app/shared/loading/loading.component.html index 45d7c9905..ddacd3448 100644 --- a/UI/Web/src/app/shared/loading/loading.component.html +++ b/UI/Web/src/app/shared/loading/loading.component.html @@ -1,7 +1,17 @@ + +
    +
    + Loading... +
    +
    +
    + +
    -
    - Loading... -
    +
    + Loading... +
    +
    \ No newline at end of file diff --git a/UI/Web/src/app/shared/loading/loading.component.ts b/UI/Web/src/app/shared/loading/loading.component.ts index feb0af942..8237c28bc 100644 --- a/UI/Web/src/app/shared/loading/loading.component.ts +++ b/UI/Web/src/app/shared/loading/loading.component.ts @@ -10,10 +10,15 @@ export class LoadingComponent implements OnInit { @Input() loading: boolean = false; @Input() message: string = ''; + /** + * Uses absolute positioning to ensure it loads over content + */ + @Input() absolute: boolean = false; constructor(private readonly cdRef: ChangeDetectorRef) { } ngOnInit(): void { + console.log('absolute: ', this.absolute); } } 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 e1a7c8c13..c92d9c08f 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 @@ -179,6 +179,12 @@
    +
    +
    + + +
    +
    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 eace0a07c..04ac999a3 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 @@ -127,6 +127,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, [])); this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, [])); this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, [])); + this.settingsForm.addControl('swipeToPaginate', new FormControl(this.user.preferences.swipeToPaginate, [])); this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, [])); @@ -187,6 +188,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.get('promptForDownloadSize')?.setValue(this.user.preferences.promptForDownloadSize); this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions); this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook); + this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate); this.cdRef.markForCheck(); this.settingsForm.markAsPristine(); } @@ -217,7 +219,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { blurUnreadSummaries: modelSettings.blurUnreadSummaries, promptForDownloadSize: modelSettings.promptForDownloadSize, noTransitions: modelSettings.noTransitions, - emulateBook: modelSettings.emulateBook + emulateBook: modelSettings.emulateBook, + swipeToPaginate: modelSettings.swipeToPaginate }; this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { diff --git a/openapi.json b/openapi.json index 807e7fad2..b4a9ea391 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.6.1.28" + "version": "0.6.1.29" }, "servers": [ { @@ -729,10 +729,12 @@ "tags": [ "Account" ], + "summary": "Resend an invite to a user already invited", "parameters": [ { "name": "userId", "in": "query", + "description": "", "schema": { "type": "integer", "format": "int32" @@ -7245,6 +7247,7 @@ "Server" ], "summary": "Is this server accessible to the outside net", + "description": "If the instance has the HostName set, this will return true whether or not it is accessible externally", "responses": { "200": { "description": "Success", @@ -9511,6 +9514,10 @@ "description": "Manga Reader Option: Background color of the reader", "nullable": true }, + "swipeToPaginate": { + "type": "boolean", + "description": "Manga Reader Option: Should swiping trigger pagination" + }, "bookReaderMargin": { "type": "integer", "description": "Book Reader Option: Override extra Margin", @@ -13318,6 +13325,11 @@ "convertCoverToWebP": { "type": "boolean", "description": "If the server should save covers as WebP encoding" + }, + "hostName": { + "type": "string", + "description": "The Host name (ie Reverse proxy domain name) for the server", + "nullable": true } }, "additionalProperties": false @@ -14285,7 +14297,8 @@ "readerMode", "readingDirection", "scalingOption", - "showScreenHints" + "showScreenHints", + "swipeToPaginate" ], "type": "object", "properties": { @@ -14313,6 +14326,9 @@ "type": "string", "description": "Manga Reader Option: Background color of the reader" }, + "swipeToPaginate": { + "type": "boolean" + }, "autoCloseMenu": { "type": "boolean", "description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction"