diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f1419f298..941468045 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -268,7 +268,7 @@ jobs: cd UI/Web || exit echo 'Installing web dependencies' - npm install --legacy-peer-deps + npm ci echo 'Building UI' npm run prod diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index e6dc8301a..02faebe82 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 28fb7a7fa..88844ad4b 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 7e66c8a1e..5a7046a7e 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -381,6 +381,49 @@ public class ReaderServiceTests Assert.Equal("2", actualChapter.Range); } + [Fact] + public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats() + { + // V1 -> V2 + await ResetDb(); + + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1.0") + .WithChapter(new ChapterBuilder("1").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2.1") + .WithChapter(new ChapterBuilder("21").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2.2") + .WithChapter(new ChapterBuilder("31").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3.1") + .WithChapter(new ChapterBuilder("31").Build()) + .Build()) + + + .Build(); + series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + Assert.Equal("31", actualChapter.Range); + } + [Fact] public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume() { diff --git a/API/API.csproj b/API/API.csproj index c83155c31..435dc59e4 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -66,23 +66,23 @@ - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + @@ -100,8 +100,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 207fcafba..f64bc2d55 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -158,5 +158,4 @@ public class BookController : BaseApiController return BadRequest(ex.Message); } } - } diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index 4b816bab7..48e609d6b 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.DTOs; +using API.Extensions; using API.Services.Plus; using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; @@ -69,7 +70,7 @@ public class RatingController : BaseApiController return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId), + AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()), FavoriteCount = 0 }); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 356592dfd..ca9d736ad 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -792,4 +792,59 @@ public class ReaderController : BaseApiController return _readerService.GetTimeEstimate(0, pagesLeft, false); } + /// + /// Returns the user's personal table of contents for the given chapter + /// + /// + /// + [HttpGet("ptoc")] + public ActionResult> GetPersonalToC(int chapterId) + { + return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId)); + } + + [HttpDelete("ptoc")] + public async Task DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title) + { + if (string.IsNullOrWhiteSpace(title)) return BadRequest("Name cannot be empty"); + if (pageNum < 0) return BadRequest("Must be valid page number"); + var toc = await _unitOfWork.UserTableOfContentRepository.Get(User.GetUserId(), chapterId, pageNum, title); + if (toc == null) return Ok(); + _unitOfWork.UserTableOfContentRepository.Remove(toc); + await _unitOfWork.CommitAsync(); + return Ok(); + } + + /// + /// Create a new personal table of content entry for a given chapter + /// + /// The title and page number must be unique to that book + /// + /// + [HttpPost("create-ptoc")] + public async Task CreatePersonalToC(CreatePersonalToCDto dto) + { + // Validate there isn't already an existing page title combo? + if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest("Name cannot be empty"); + if (dto.PageNumber < 0) return BadRequest("Must be valid page number"); + var userId = User.GetUserId(); + if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber, + dto.Title.Trim())) + { + return BadRequest("Duplicate ToC entry already exists"); + } + + _unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() + { + Title = dto.Title.Trim(), + ChapterId = dto.ChapterId, + PageNumber = dto.PageNumber, + SeriesId = dto.SeriesId, + LibraryId = dto.LibraryId, + BookScrollId = dto.BookScrollId, + AppUserId = userId + }); + await _unitOfWork.CommitAsync(); + return Ok(); + } } diff --git a/API/DTOs/RatingDto.cs b/API/DTOs/RatingDto.cs index fd3299868..89f4aebe5 100644 --- a/API/DTOs/RatingDto.cs +++ b/API/DTOs/RatingDto.cs @@ -7,4 +7,5 @@ public class RatingDto public int AverageScore { get; set; } public int FavoriteCount { get; set; } public ScrobbleProvider Provider { get; set; } + public string? ProviderUrl { get; set; } } diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs new file mode 100644 index 000000000..25526b490 --- /dev/null +++ b/API/DTOs/Reader/CreatePersonalToCDto.cs @@ -0,0 +1,12 @@ +namespace API.DTOs.Reader; + +public class CreatePersonalToCDto +{ + public required int ChapterId { get; set; } + public required int VolumeId { get; set; } + public required int SeriesId { get; set; } + public required int LibraryId { get; set; } + public required int PageNumber { get; set; } + public required string Title { get; set; } + public string? BookScrollId { get; set; } +} diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/API/DTOs/Reader/PersonalToCDto.cs new file mode 100644 index 000000000..6763a157a --- /dev/null +++ b/API/DTOs/Reader/PersonalToCDto.cs @@ -0,0 +1,9 @@ +namespace API.DTOs.Reader; + +public class PersonalToCDto +{ + public required int ChapterId { get; set; } + public required int PageNumber { get; set; } + public required string Title { get; set; } + public string? BookScrollId { get; set; } +} diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/API/DTOs/Scrobbling/ScrobbleDto.cs index 5d2dc3ebc..e58de0576 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleDto.cs @@ -66,8 +66,16 @@ public class ScrobbleDto /// public DateTime? StartedReadingDateUtc { get; set; } /// + /// The latest date the series was read. Will be null for non ReadingProgress events + /// + public DateTime? LatestReadingDateUtc { get; set; } + /// /// The date that the series was scrobbled. Will be null for non ReadingProgress events /// public DateTime? ScrobbleDateUtc { get; set; } + /// + /// Optional but can help with matching + /// + public string? Isbn { get; set; } } diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 906d66bd4..3ed0cd569 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -10,9 +10,9 @@ public class ScrobbleEventDto public bool IsProcessed { get; set; } public int? VolumeNumber { get; set; } public int? ChapterNumber { get; set; } - public DateTime? ProcessDateUtc { get; set; } public DateTime LastModified { get; set; } public DateTime Created { get; set; } public float? Rating { get; set; } public ScrobbleEventType ScrobbleEventType { get; set; } + } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 004f94b7e..59b7708cb 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -30,9 +30,13 @@ public class SeriesDto : IHasReadTimeEstimate /// /// Rating from logged in user. Calculated at API-time. /// - public int UserRating { get; set; } - public MangaFormat Format { get; set; } + public float UserRating { get; set; } + /// + /// If the user has set the rating or not + /// + public bool HasUserRated { get; set; } + public MangaFormat Format { get; set; } public DateTime Created { get; set; } public bool NameLocked { get; set; } diff --git a/API/DTOs/UpdateSeriesRatingDto.cs b/API/DTOs/UpdateSeriesRatingDto.cs index d1f407687..5dafa35af 100644 --- a/API/DTOs/UpdateSeriesRatingDto.cs +++ b/API/DTOs/UpdateSeriesRatingDto.cs @@ -1,9 +1,7 @@ -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs; +namespace API.DTOs; public class UpdateSeriesRatingDto { public int SeriesId { get; init; } - public int UserRating { get; init; } + public float UserRating { get; init; } } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index e4dc6b746..5faec1cde 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -53,6 +53,7 @@ public sealed class DataContext : IdentityDbContext ScrobbleError { get; set; } = null!; public DbSet ScrobbleHold { get; set; } = null!; public DbSet AppUserOnDeckRemoval { get; set; } = null!; + public DbSet AppUserTableOfContent { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) diff --git a/API/Data/ManualMigrations/MigrateExistingRatings.cs b/API/Data/ManualMigrations/MigrateExistingRatings.cs new file mode 100644 index 000000000..4314c724b --- /dev/null +++ b/API/Data/ManualMigrations/MigrateExistingRatings.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.5.6 and v0.7.6, Ratings > 0 need to have "HasRatingSet" +/// +/// Added in v0.7.5.6 +// ReSharper disable once InconsistentNaming +public static class MigrateExistingRatings +{ + public static async Task Migrate(DataContext context, ILogger logger) + { + logger.LogCritical("Running MigrateExistingRatings migration - Please be patient, this may take some time. This is not an error"); + + foreach (var r in context.AppUserRating.Where(r => r.Rating > 0f)) + { + r.HasBeenRated = true; + context.Entry(r).State = EntityState.Modified; + } + + if (context.ChangeTracker.HasChanges()) + { + await context.SaveChangesAsync(); + } + + logger.LogCritical("Running MigrateExistingRatings migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateSeriesRelationsExport.cs b/API/Data/ManualMigrations/MigrateSeriesRelationsExport.cs deleted file mode 100644 index 4427a6687..000000000 --- a/API/Data/ManualMigrations/MigrateSeriesRelationsExport.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.Enums; -using CsvHelper; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -internal sealed class SeriesRelationMigrationOutput -{ - public required string SeriesName { get; set; } - public int SeriesId { get; set; } - public required string TargetSeriesName { get; set; } - public int TargetId { get; set; } - public RelationKind Relationship { get; set; } -} - -/// -/// Introduced in v0.6.1.2 and v0.7, this exports to a temp file the existing series relationships. It is a 3 part migration. -/// This will run first, to export the data, then the DB migration will change the way the DB is constructed, then the last migration -/// will import said file and re-construct the relationships. -/// -public static class MigrateSeriesRelationsExport -{ - private const string OutputFile = "config/relations.csv"; - private const string CompleteOutputFile = "config/relations-imported.csv"; - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - logger.LogCritical("Running MigrateSeriesRelationsExport migration - Please be patient, this may take some time. This is not an error"); - if (BuildInfo.Version > new Version(0, 6, 1, 3) - || new FileInfo(OutputFile).Exists - || new FileInfo(CompleteOutputFile).Exists) - { - logger.LogCritical("Running MigrateSeriesRelationsExport migration - complete. Nothing to do"); - return; - } - - var seriesWithRelationships = await dataContext.Series - .Where(s => s.Relations.Any()) - .Include(s => s.Relations) - .ThenInclude(r => r.TargetSeries) - .ToListAsync(); - - var records = new List(); - var excludedRelationships = new List() - { - RelationKind.Parent, - }; - foreach (var series in seriesWithRelationships) - { - foreach (var relationship in series.Relations.Where(r => !excludedRelationships.Contains(r.RelationKind))) - { - records.Add(new SeriesRelationMigrationOutput() - { - SeriesId = series.Id, - SeriesName = series.Name, - Relationship = relationship.RelationKind, - TargetId = relationship.TargetSeriesId, - TargetSeriesName = relationship.TargetSeries.Name - }); - } - } - - await using var writer = new StreamWriter(OutputFile); - await using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - await csv.WriteRecordsAsync(records); - } - - await writer.DisposeAsync(); - - logger.LogCritical("{OutputFile} has a backup of all data", OutputFile); - - logger.LogCritical("Deleting all relationships in the DB. This is not an error"); - var entities = await dataContext.SeriesRelation - .Include(s => s.Series) - .Include(s => s.TargetSeries) - .Select(s => s) - .ToListAsync(); - - foreach (var seriesWithRelationship in entities) - { - logger.LogCritical("Deleting {SeriesName} --{RelationshipKind}--> {TargetSeriesName}", - seriesWithRelationship.Series.Name, seriesWithRelationship.RelationKind, seriesWithRelationship.TargetSeries.Name); - dataContext.SeriesRelation.Remove(seriesWithRelationship); - - await dataContext.SaveChangesAsync(); - } - - // In case of corrupted entities (where series were deleted but their Id still existed, we delete the rest of the table) - dataContext.SeriesRelation.RemoveRange(dataContext.SeriesRelation); - await dataContext.SaveChangesAsync(); - - - logger.LogCritical("Running MigrateSeriesRelationsExport migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/MigrateSeriesRelationsImport.cs b/API/Data/ManualMigrations/MigrateSeriesRelationsImport.cs deleted file mode 100644 index 9faefde6a..000000000 --- a/API/Data/ManualMigrations/MigrateSeriesRelationsImport.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.Metadata; -using CsvHelper; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.6.1.2 and v0.7, this imports to a temp file the existing series relationships. It is a 3 part migration. -/// This will run last, to import the data and re-construct the relationships. -/// -public static class MigrateSeriesRelationsImport -{ - private const string OutputFile = "config/relations.csv"; - private const string CompleteOutputFile = "config/relations-imported.csv"; - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - logger.LogCritical("Running MigrateSeriesRelationsImport migration - Please be patient, this may take some time. This is not an error"); - if (!new FileInfo(OutputFile).Exists) - { - logger.LogCritical("Running MigrateSeriesRelationsImport migration - complete. Nothing to do"); - return; - } - - logger.LogCritical("Loading backed up relationships into the DB"); - List records; - using var reader = new StreamReader(OutputFile); - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - records = csv.GetRecords().ToList(); - } - - foreach (var relation in records) - { - logger.LogCritical("Importing {SeriesName} --{RelationshipKind}--> {TargetSeriesName}", - relation.SeriesName, relation.Relationship, relation.TargetSeriesName); - - // Filter out series that don't exist - if (!await dataContext.Series.AnyAsync(s => s.Id == relation.SeriesId) || - !await dataContext.Series.AnyAsync(s => s.Id == relation.TargetId)) - continue; - - await dataContext.SeriesRelation.AddAsync(new SeriesRelation() - { - SeriesId = relation.SeriesId, - TargetSeriesId = relation.TargetId, - RelationKind = relation.Relationship - }); - - } - await dataContext.SaveChangesAsync(); - - File.Move(OutputFile, CompleteOutputFile); - - logger.LogCritical("Running MigrateSeriesRelationsImport migration - Completed. This is not an error"); - } -} diff --git a/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs b/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs new file mode 100644 index 000000000..50e9ffd61 --- /dev/null +++ b/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs @@ -0,0 +1,2266 @@ +// +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("20230719173458_PersonalToC")] + partial class PersonalToC + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.8"); + + 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("AniListAccessToken") + .HasColumnType("TEXT"); + + 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.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + 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") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + 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("ShareReviews") + .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.Property("Tagline") + .HasColumnType("TEXT"); + + 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.AppUserTableOfContent", 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("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + 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("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + 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.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + 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("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .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.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + 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("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.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + 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.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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("ScrobbleHolds"); + + b.Navigation("TableOfContents"); + + 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/20230719173458_PersonalToC.cs b/API/Data/Migrations/20230719173458_PersonalToC.cs new file mode 100644 index 000000000..c3eb9e025 --- /dev/null +++ b/API/Data/Migrations/20230719173458_PersonalToC.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class PersonalToC : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppUserTableOfContent", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PageNumber = table.Column(type: "INTEGER", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + SeriesId = table.Column(type: "INTEGER", nullable: false), + ChapterId = table.Column(type: "INTEGER", nullable: false), + VolumeId = table.Column(type: "INTEGER", nullable: false), + LibraryId = table.Column(type: "INTEGER", nullable: false), + BookScrollId = table.Column(type: "TEXT", nullable: true), + Created = table.Column(type: "TEXT", nullable: false), + CreatedUtc = table.Column(type: "TEXT", nullable: false), + LastModified = table.Column(type: "TEXT", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserTableOfContent", x => x.Id); + table.ForeignKey( + name: "FK_AppUserTableOfContent_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserTableOfContent_Chapter_ChapterId", + column: x => x.ChapterId, + principalTable: "Chapter", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserTableOfContent_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserTableOfContent_AppUserId", + table: "AppUserTableOfContent", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserTableOfContent_ChapterId", + table: "AppUserTableOfContent", + column: "ChapterId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserTableOfContent_SeriesId", + table: "AppUserTableOfContent", + column: "SeriesId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserTableOfContent"); + } + } +} diff --git a/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs b/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs new file mode 100644 index 000000000..8b5edb0ff --- /dev/null +++ b/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs @@ -0,0 +1,2269 @@ +// +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("20230725133536_ChangeRatingScale")] + partial class ChangeRatingScale + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.9"); + + 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("AniListAccessToken") + .HasColumnType("TEXT"); + + 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.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + 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") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + 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("ShareReviews") + .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("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + 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.AppUserTableOfContent", 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("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + 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("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + 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.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + 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("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .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.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + 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("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.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + 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.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + 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("ScrobbleHolds"); + + b.Navigation("TableOfContents"); + + 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/20230725133536_ChangeRatingScale.cs b/API/Data/Migrations/20230725133536_ChangeRatingScale.cs new file mode 100644 index 000000000..4f97e008b --- /dev/null +++ b/API/Data/Migrations/20230725133536_ChangeRatingScale.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ChangeRatingScale : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Rating", + table: "AppUserRating", + type: "REAL", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddColumn( + name: "HasBeenRated", + table: "AppUserRating", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HasBeenRated", + table: "AppUserRating"); + + migrationBuilder.AlterColumn( + name: "Rating", + table: "AppUserRating", + type: "INTEGER", + nullable: false, + oldClrType: typeof(float), + oldType: "REAL"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index cc8d2660f..6efb397b4 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.8"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.9"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -371,9 +371,12 @@ namespace API.Data.Migrations b.Property("AppUserId") .HasColumnType("INTEGER"); - b.Property("Rating") + b.Property("HasBeenRated") .HasColumnType("INTEGER"); + b.Property("Rating") + .HasColumnType("REAL"); + b.Property("Review") .HasColumnType("TEXT"); @@ -407,6 +410,59 @@ namespace API.Data.Migrations b.ToTable("AspNetUserRoles", (string)null); }); + modelBuilder.Entity("API.Entities.AppUserTableOfContent", 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("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + modelBuilder.Entity("API.Entities.Chapter", b => { b.Property("Id") @@ -1746,6 +1802,33 @@ namespace API.Data.Migrations b.Navigation("User"); }); + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + modelBuilder.Entity("API.Entities.Chapter", b => { b.HasOne("API.Entities.Volume", "Volume") @@ -2130,6 +2213,8 @@ namespace API.Data.Migrations b.Navigation("ScrobbleHolds"); + b.Navigation("TableOfContents"); + b.Navigation("UserPreferences"); b.Navigation("UserRoles"); diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 28e7ed91e..e93bcc753 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -6,6 +6,7 @@ using API.Data.ManualMigrations; using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -31,6 +32,8 @@ public interface IAppUserProgressRepository Task AnyUserProgressForSeriesAsync(int seriesId, int userId); Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); + Task GetLatestProgressForSeries(int seriesId, int userId); + Task GetFirstProgressForSeries(int seriesId, int userId); } #nullable disable public class AppUserProgressRepository : IAppUserProgressRepository @@ -162,9 +165,9 @@ public class AppUserProgressRepository : IAppUserProgressRepository (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Select(p => p.chapter.Number) + .Select(p => p.chapter.Range) .ToListAsync(); - return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(float.Parse(d))); + return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d))); } public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) @@ -179,7 +182,23 @@ public class AppUserProgressRepository : IAppUserProgressRepository return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } - #nullable enable + public async Task GetLatestProgressForSeries(int seriesId, int userId) + { + var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + .Select(p => p.LastModifiedUtc) + .ToListAsync(); + return list.Count == 0 ? null : list.DefaultIfEmpty().Max(); + } + + public async Task GetFirstProgressForSeries(int seriesId, int userId) + { + var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + .Select(p => p.LastModifiedUtc) + .ToListAsync(); + return list.Count == 0 ? null : list.DefaultIfEmpty().Min(); + } + +#nullable enable public async Task GetUserProgressAsync(int chapterId, int userId) { return await _context.AppUserProgresses diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index cf47e8688..9a5534b94 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -137,7 +137,7 @@ public interface ISeriesRepository Task> GetSeriesMetadataForIds(IEnumerable seriesIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); - Task GetAverageUserRating(int seriesId); + Task GetAverageUserRating(int seriesId, int userId); Task RemoveFromOnDeck(int seriesId, int userId); Task ClearOnDeckRemoval(int seriesId, int userId); } @@ -625,6 +625,7 @@ public class SeriesRepository : ISeriesRepository if (rating != null) { s.UserRating = rating.Rating; + s.HasUserRated = rating.HasBeenRated; } if (userProgress.Count > 0) @@ -1682,12 +1683,19 @@ public class SeriesRepository : ISeriesRepository /// Returns the Average rating for all users within Kavita instance /// /// - public async Task GetAverageUserRating(int seriesId) + public async Task GetAverageUserRating(int seriesId, int userId) { + // If there is 0 or 1 rating and that rating is you, return 0 back + var countOfRatingsThatAreUser = await _context.AppUserRating + .Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId); + if (countOfRatingsThatAreUser == 1) + { + return 0; + } var avg = (await _context.AppUserRating - .Where(r => r.SeriesId == seriesId) + .Where(r => r.SeriesId == seriesId && r.HasBeenRated) .AverageAsync(r => (int?) r.Rating)); - return avg.HasValue ? (int) avg.Value : 0; + return avg.HasValue ? (int) (avg.Value * 20) : 0; } public async Task RemoveFromOnDeck(int seriesId, int userId) @@ -1707,7 +1715,7 @@ public class SeriesRepository : ISeriesRepository public async Task ClearOnDeckRemoval(int seriesId, int userId) { var existingEntry = await _context.AppUserOnDeckRemoval - .Where(u => u.Id == userId && u.SeriesId == seriesId) + .Where(u => u.AppUserId == userId && u.SeriesId == seriesId) .FirstOrDefaultAsync(); if (existingEntry == null) return; _context.AppUserOnDeckRemoval.Remove(existingEntry); diff --git a/API/Data/Repositories/UserTableOfContentRepository.cs b/API/Data/Repositories/UserTableOfContentRepository.cs new file mode 100644 index 000000000..b640ec9a0 --- /dev/null +++ b/API/Data/Repositories/UserTableOfContentRepository.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs.Reader; +using API.Entities; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +namespace API.Data.Repositories; +#nullable enable + +public interface IUserTableOfContentRepository +{ + void Attach(AppUserTableOfContent toc); + void Remove(AppUserTableOfContent toc); + Task IsUnique(int userId, int chapterId, int page, string title); + IEnumerable GetPersonalToC(int userId, int chapterId); + Task Get(int userId, int chapterId, int pageNum, string title); +} + +public class UserTableOfContentRepository : IUserTableOfContentRepository +{ + private readonly DataContext _context; + private readonly IMapper _mapper; + + public UserTableOfContentRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Attach(AppUserTableOfContent toc) + { + _context.AppUserTableOfContent.Attach(toc); + } + + public void Remove(AppUserTableOfContent toc) + { + _context.AppUserTableOfContent.Remove(toc); + } + + public async Task IsUnique(int userId, int chapterId, int page, string title) + { + return await _context.AppUserTableOfContent.AnyAsync(t => + t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId); + } + + public IEnumerable GetPersonalToC(int userId, int chapterId) + { + return _context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId) + .ProjectTo(_mapper.ConfigurationProvider) + .OrderBy(t => t.PageNumber) + .AsEnumerable(); + } + + public async Task Get(int userId,int chapterId, int pageNum, string title) + { + return await _context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title) + .FirstOrDefaultAsync(); + } +} diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 6d79f1922..8eb1f3a31 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -27,6 +27,7 @@ public interface IUnitOfWork IDeviceRepository DeviceRepository { get; } IMediaErrorRepository MediaErrorRepository { get; } IScrobbleRepository ScrobbleRepository { get; } + IUserTableOfContentRepository UserTableOfContentRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -66,6 +67,7 @@ public class UnitOfWork : IUnitOfWork public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper); public IMediaErrorRepository MediaErrorRepository => new MediaErrorRepository(_context, _mapper); public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper); + public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 58f7d7033..f50262ef0 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -37,9 +37,9 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// public ICollection Devices { get; set; } = null!; /// - /// A list of Series the user doesn't want on deck + /// A list of Table of Contents for a given Chapter /// - //public ICollection OnDeckRemovals { get; set; } = null!; + public ICollection TableOfContents { get; set; } = null!; /// /// An API Key to interact with external services, like OPDS /// diff --git a/API/Entities/AppUserRating.cs b/API/Entities/AppUserRating.cs index b2d08fb7b..91734b445 100644 --- a/API/Entities/AppUserRating.cs +++ b/API/Entities/AppUserRating.cs @@ -1,13 +1,17 @@  namespace API.Entities; - +#nullable enable public class AppUserRating { public int Id { get; set; } /// - /// A number between 0-5 that represents how good a series is. + /// A number between 0-5.0 that represents how good a series is. /// - public int Rating { get; set; } + public float Rating { get; set; } + /// + /// If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated + /// + public bool HasBeenRated { get; set; } /// /// A short summary the user can write when giving their review. /// @@ -17,7 +21,7 @@ public class AppUserRating /// public string? Tagline { get; set; } public int SeriesId { get; set; } - public Series Series { get; set; } + public Series Series { get; set; } = null!; // Relationships diff --git a/API/Entities/AppUserTableOfContent.cs b/API/Entities/AppUserTableOfContent.cs new file mode 100644 index 000000000..bc0f604bc --- /dev/null +++ b/API/Entities/AppUserTableOfContent.cs @@ -0,0 +1,49 @@ +using System; +using API.Entities.Interfaces; + +namespace API.Entities; + +/// +/// A personal table of contents for a given user linked with a given book +/// +public class AppUserTableOfContent : IEntityDate +{ + public int Id { get; set; } + + /// + /// The page to bookmark + /// + public required int PageNumber { get; set; } + /// + /// The title of the bookmark. Defaults to Page {PageNumber} if not set + /// + public required string Title { get; set; } + + public required int SeriesId { get; set; } + public virtual Series Series { get; set; } + + public required int ChapterId { get; set; } + public virtual Chapter Chapter { get; set; } + + public int VolumeId { get; set; } + public int LibraryId { get; set; } + /// + /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page + /// + public string? BookScrollId { get; set; } + + public DateTime Created { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModified { get; set; } + public DateTime LastModifiedUtc { get; set; } + + // Relationships + /// + /// Navigational Property for EF. Links to a unique AppUser + /// + public AppUser AppUser { get; set; } = null!; + /// + /// User this table of content belongs to + /// + public int AppUserId { get; set; } +} diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index 29436283a..2fd36eef3 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -46,6 +46,7 @@ public class ScrobbleEvent : IEntityDate /// public DateTime? ProcessDateUtc { get; set; } + public required int SeriesId { get; set; } public Series Series { get; set; } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 38edabbca..c42a09eff 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using API.Data.Migrations; using API.DTOs; using API.DTOs.Account; using API.DTOs.CollectionTags; @@ -19,6 +20,10 @@ using API.Entities.Metadata; using API.Entities.Scrobble; using API.Helpers.Converters; using AutoMapper; +using CollectionTag = API.Entities.CollectionTag; +using MediaError = API.Entities.MediaError; +using PublicationStatus = API.Entities.Enums.PublicationStatus; +using SiteTheme = API.Entities.SiteTheme; namespace API.Helpers; @@ -211,6 +216,7 @@ public class AutoMapperProfiles : Profile .ConvertUsing(); CreateMap(); + CreateMap(); } } diff --git a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs new file mode 100644 index 000000000..b379242ac --- /dev/null +++ b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs @@ -0,0 +1,36 @@ +using System.Linq; +using API.DTOs; +using API.Entities; +using API.Services.Plus; + +namespace API.Helpers.Builders; + +public class PlusSeriesDtoBuilder : IEntityBuilder +{ + private readonly PlusSeriesDto _seriesDto; + public PlusSeriesDto Build() => _seriesDto; + + /// + /// This must be a FULL Series + /// + /// + public PlusSeriesDtoBuilder(Series series) + { + _seriesDto = new PlusSeriesDto() + { + MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), + SeriesName = series.Name, + AltSeriesName = series.LocalizedName, + AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.AniListWeblinkWebsite), + MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.MalWeblinkWebsite), + GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.GoogleBooksWeblinkWebsite), + VolumeCount = series.Volumes.Count, + ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), + Year = series.Metadata.ReleaseYear + }; + } + +} diff --git a/API/Program.cs b/API/Program.cs index 78a4f5e07..625949f65 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -70,7 +70,8 @@ public class Program var logger = services.GetRequiredService>(); var context = services.GetRequiredService(); var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); - if (pendingMigrations.Any()) + var isDbCreated = await context.Database.CanConnectAsync(); + if (isDbCreated && pendingMigrations.Any()) { logger.LogInformation("Performing backup as migrations are needed. Backup will be kavita.db in temp folder"); var migrationDirectory = await GetMigrationDirectory(context, directoryService); @@ -84,16 +85,6 @@ public class Program } } - // This must run before the migration - try - { - await MigrateSeriesRelationsExport.Migrate(context, logger); - } - catch (Exception) - { - // If fresh install, could fail and we should just carry on as it's not applicable - } - await context.Database.MigrateAsync(); await Seed.SeedRoles(services.GetRequiredService>()); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 3459cbdf0..95863e263 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Web; using API.Data.Metadata; using API.DTOs.Reader; using API.Entities; @@ -898,7 +897,7 @@ public class BookService : IBookService /// Epub mappings /// Page number we are loading /// - public async Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary mappings, int page) + private async Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary mappings, int page) { await InlineStyles(doc, book, apiBase, body); diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/API/Services/HostedServices/StartupTasksHostedService.cs index 43f181016..d7d74f77d 100644 --- a/API/Services/HostedServices/StartupTasksHostedService.cs +++ b/API/Services/HostedServices/StartupTasksHostedService.cs @@ -26,7 +26,6 @@ public class StartupTasksHostedService : IHostedService taskScheduler.ScheduleUpdaterTasks(); - try { // These methods will automatically check if stat collection is disabled to prevent sending any data regardless diff --git a/API/Services/Plus/RatingService.cs b/API/Services/Plus/RatingService.cs index e2bb5eae3..0993948fd 100644 --- a/API/Services/Plus/RatingService.cs +++ b/API/Services/Plus/RatingService.cs @@ -9,6 +9,7 @@ using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Helpers; +using API.Helpers.Builders; using Flurl.Http; using Kavita.Common; using Kavita.Common.EnvironmentInfo; @@ -59,19 +60,7 @@ public class RatingService : IRatingService .WithHeader("x-kavita-version", BuildInfo.Version) .WithHeader("Content-Type", "application/json") .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) - .PostJsonAsync(new PlusSeriesDto() - { - MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), - SeriesName = series.Name, - AltSeriesName = series.LocalizedName, - AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.AniListWeblinkWebsite), - MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MalWeblinkWebsite), - VolumeCount = series.Volumes.Count, - ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), - Year = series.Metadata.ReleaseYear - }) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) .ReceiveJson>(); } catch (Exception e) diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index 8da137b00..f3ca6276e 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -11,6 +11,7 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services.Tasks.Scanner.Parser; using Flurl.Http; using Kavita.Common; @@ -24,6 +25,7 @@ public record PlusSeriesDto { public int? AniListId { get; set; } public long? MalId { get; set; } + public string? GoogleBooksId { get; set; } public string SeriesName { get; set; } public string? AltSeriesName { get; set; } public MediaFormat MediaFormat { get; set; } @@ -134,19 +136,7 @@ public class RecommendationService : IRecommendationService .WithHeader("x-kavita-version", BuildInfo.Version) .WithHeader("Content-Type", "application/json") .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) - .PostJsonAsync(new PlusSeriesDto() - { - MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), - SeriesName = series.Name, - AltSeriesName = series.LocalizedName, - AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.AniListWeblinkWebsite), - MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MalWeblinkWebsite), - VolumeCount = series.Volumes.Count, - ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), - Year = series.Metadata.ReleaseYear - }) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) .ReceiveJson>(); } diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 9730cb9c6..c2f3477c0 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -39,7 +39,7 @@ public interface IScrobblingService { Task CheckExternalAccessTokens(); Task HasTokenExpired(int userId, ScrobbleProvider provider); - Task ScrobbleRatingUpdate(int userId, int seriesId, int rating); + Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody); Task ScrobbleReadingUpdate(int userId, int seriesId); Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); @@ -63,11 +63,14 @@ public class ScrobblingService : IScrobblingService public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; + public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; private static readonly IDictionary WeblinkExtractionMap = new Dictionary() { {AniListWeblinkWebsite, 0}, {MalWeblinkWebsite, 0}, + {GoogleBooksWeblinkWebsite, 0}, + }; private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) @@ -208,8 +211,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.Review, - AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), - MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, Format = LibraryTypeHelper.GetFormat(series.Library.Type), ReviewBody = reviewBody, @@ -220,7 +223,7 @@ public class ScrobblingService : IScrobblingService _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId); } - public async Task ScrobbleRatingUpdate(int userId, int seriesId, int rating) + public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) { if (!await _licenseService.HasActiveLicense()) return; var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList); @@ -253,8 +256,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ScoreUpdated, - AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), - MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, Format = LibraryTypeHelper.GetFormat(series.Library.Type), Rating = rating @@ -310,8 +313,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ChapterRead, - AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), - MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, VolumeNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), @@ -353,8 +356,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead, - AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), - MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, Format = LibraryTypeHelper.GetFormat(series.Library.Type), }; @@ -542,7 +545,7 @@ public class ScrobblingService : IScrobblingService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ProcessUpdatesSinceLastSync() { - // Check how many scrobbles we have available then only do those. + // Check how many scrobble events we have available then only do those. _logger.LogInformation("Starting Scrobble Processing"); var userRateLimits = new Dictionary(); var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); @@ -623,7 +626,7 @@ public class ScrobblingService : IScrobblingService readEvt.AppUser.Id); _unitOfWork.ScrobbleRepository.Update(readEvt); } - progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto() + progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, async evt => new ScrobbleDto() { Format = evt.Format, AniListId = evt.AniListId, @@ -634,12 +637,14 @@ public class ScrobblingService : IScrobblingService AniListToken = evt.AppUser.AniListAccessToken, SeriesName = evt.Series.Name, LocalizedSeriesName = evt.Series.LocalizedName, - StartedReadingDateUtc = evt.CreatedUtc, ScrobbleDateUtc = evt.LastModifiedUtc, - Year = evt.Series.Metadata.ReleaseYear + Year = evt.Series.Metadata.ReleaseYear, + StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), + LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), }); - progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto() + progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() { Format = evt.Format, AniListId = evt.AniListId, @@ -650,9 +655,10 @@ public class ScrobblingService : IScrobblingService LocalizedSeriesName = evt.Series.LocalizedName, Rating = evt.Rating, Year = evt.Series.Metadata.ReleaseYear - }); + })); - progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto() + progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() { Format = evt.Format, AniListId = evt.AniListId, @@ -665,21 +671,22 @@ public class ScrobblingService : IScrobblingService Year = evt.Series.Metadata.ReleaseYear, ReviewBody = evt.ReviewBody, ReviewTitle = evt.ReviewTitle - }); + })); - progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto() - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Year = evt.Series.Metadata.ReleaseYear - }); + progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Year = evt.Series.Metadata.ReleaseYear + })); } catch (FlurlHttpException) { @@ -693,7 +700,7 @@ public class ScrobblingService : IScrobblingService } private async Task ProcessEvents(IEnumerable events, IDictionary userRateLimits, - int usersToScrobble, int progressCounter, int totalProgress, Func createEvent) + int usersToScrobble, int progressCounter, int totalProgress, Func> createEvent) { var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); foreach (var evt in events) @@ -714,7 +721,7 @@ public class ScrobblingService : IScrobblingService try { - var data = createEvent(evt); + var data = await createEvent(evt); userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt); evt.IsProcessed = true; evt.ProcessDateUtc = DateTime.UtcNow; @@ -784,17 +791,31 @@ public class ScrobblingService : IScrobblingService /// /// /// - public static long? ExtractId(string webLinks, string website) + public static T? ExtractId(string webLinks, string website) { var index = WeblinkExtractionMap[website]; foreach (var webLink in webLinks.Split(',')) { if (!webLink.StartsWith(website)) continue; var tokens = webLink.Split(website)[1].Split('/'); - return long.Parse(tokens[index]); + var value = tokens[index]; + if (typeof(T) == typeof(int)) + { + if (int.TryParse(value, out var intValue)) + return (T)(object)intValue; + } + else if (typeof(T) == typeof(long)) + { + if (long.TryParse(value, out var longValue)) + return (T)(object)longValue; + } + else if (typeof(T) == typeof(string)) + { + return (T)(object)value; + } } - return 0; + return default(T?); } private async Task SetAndCheckRateLimit(IDictionary userRateLimits, AppUser user, string license) diff --git a/API/Services/ReviewService.cs b/API/Services/ReviewService.cs index a0f4d18f5..6ad170df8 100644 --- a/API/Services/ReviewService.cs +++ b/API/Services/ReviewService.cs @@ -9,6 +9,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Helpers; +using API.Helpers.Builders; using API.Services.Plus; using Flurl.Http; using HtmlAgilityPack; @@ -133,19 +134,7 @@ public class ReviewService : IReviewService .WithHeader("x-kavita-version", BuildInfo.Version) .WithHeader("Content-Type", "application/json") .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) - .PostJsonAsync(new PlusSeriesDto() - { - MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), - SeriesName = series.Name, - AltSeriesName = series.LocalizedName, - AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.AniListWeblinkWebsite), - MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MalWeblinkWebsite), - VolumeCount = series.Volumes.Count, - ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), - Year = series.Metadata.ReleaseYear - }) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) .ReceiveJson>(); } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 0f8c19f7c..fcc6846d4 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -217,22 +217,14 @@ public class SeriesService : ISeriesService // Trigger code to cleanup tags, collections, people, etc await _taskScheduler.CleanupDbEntries(); - if (updateSeriesMetadataDto.CollectionTags != null) + if (updateSeriesMetadataDto.CollectionTags == null) return true; + foreach (var tag in updateSeriesMetadataDto.CollectionTags) { - foreach (var tag in updateSeriesMetadataDto.CollectionTags) - { - await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection, - MessageFactory.SeriesAddedToCollectionEvent(tag.Id, - updateSeriesMetadataDto.SeriesMetadata.SeriesId), false); - } - - await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, - MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false); - - await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); - - return true; + await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection, + MessageFactory.SeriesAddedToCollectionEvent(tag.Id, + updateSeriesMetadataDto.SeriesMetadata.SeriesId), false); } + return true; } catch (Exception ex) { @@ -302,7 +294,8 @@ public class SeriesService : ISeriesService new AppUserRating(); try { - userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0, 5); + userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; userRating.SeriesId = updateSeriesRatingDto.SeriesId; if (userRating.Id == 0) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 4cbe433a0..a7a309844 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -61,6 +61,7 @@ public class TaskScheduler : ITaskScheduler public const string DefaultQueue = "default"; public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; + public const string CheckForUpdateId = "check-updates"; public const string CleanupDbTaskId = "cleanup-db"; public const string CleanupTaskId = "cleanup"; public const string BackupTaskId = "backup"; @@ -126,7 +127,13 @@ public class TaskScheduler : ITaskScheduler if (setting != null) { _logger.LogDebug("Scheduling Backup Task for {Setting}", setting); - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); + var schedule = CronConverter.ConvertToCronNotation(setting); + if (schedule == Cron.Daily()) + { + // Override daily and make 2am so that everything on system has cleaned up and no blocking + schedule = Cron.Daily(2); + } + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => schedule, RecurringJobOptions); } else { @@ -226,10 +233,8 @@ public class TaskScheduler : ITaskScheduler public void ScheduleUpdaterTasks() { _logger.LogInformation("Scheduling Auto-Update tasks"); - RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(5, 23)), new RecurringJobOptions() - { - TimeZone = TimeZoneInfo.Local - }); + RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); + BackgroundJob.Enqueue(() => CheckForUpdate()); } public void ScanFolder(string folderPath, TimeSpan delay) diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index e9684e4e8..3b1f7746c 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -145,7 +145,7 @@ public class BackupService : IBackupService private void CopyFaviconsToBackupDirectory(string tempDirectory) { - _directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, tempDirectory); + _directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); } private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index bfb4a34c7..ea3c64699 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -72,13 +72,11 @@ public class VersionUpdaterService : IVersionUpdaterService /// /// Fetches the latest release from Github /// - /// Latest update or null if current version is greater than latest update - public async Task CheckForUpdate() + /// Latest update + public async Task CheckForUpdate() { var update = await GetGithubRelease(); - var dto = CreateDto(update); - if (dto == null) return null; - return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto; + return CreateDto(update); } public async Task> GetAllReleases() diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index 60d53bcac..57413442c 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -65,7 +65,7 @@ public class PresenceTracker : IPresenceTracker _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); } - catch (Exception ex) + catch (Exception) { // Swallow the exception } diff --git a/API/Startup.cs b/API/Startup.cs index 64f2a3f73..ed68f050e 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -247,6 +247,9 @@ public class Startup // v0.7.4 await MigrateDisableScrobblingOnComicLibraries.Migrate(unitOfWork, dataContext, logger); + // v0.7.6 + await MigrateExistingRatings.Migrate(dataContext, logger); + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); installVersion.Value = BuildInfo.Version.ToString(); diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index 56c3f0401..4d04eb94d 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -100,7 +100,7 @@ public static class Configuration { try { - return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key"; + return !GetJwtToken(GetAppSettingFilename()).StartsWith("super secret unguessable key"); } catch (Exception ex) { diff --git a/Kavita.Common/HashUtil.cs b/Kavita.Common/HashUtil.cs index 4956682b3..b9e85d404 100644 --- a/Kavita.Common/HashUtil.cs +++ b/Kavita.Common/HashUtil.cs @@ -1,10 +1,5 @@ using System; -using System.Diagnostics; using System.Text; -using System.Text.RegularExpressions; -using DeviceId; -using DeviceId.Components; -using Kavita.Common.EnvironmentInfo; namespace Kavita.Common; @@ -62,27 +57,4 @@ public static class HashUtil return id.ToString(); } - - private static string RunAndCapture(string filename, string args) - { - var p = new Process - { - StartInfo = - { - FileName = filename, - Arguments = args, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true - } - }; - - p.Start(); - - // To avoid deadlocks, always read the output stream first and then wait. - var output = p.StandardOutput.ReadToEnd(); - p.WaitForExit(1000); - - return output; - } } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index b7a50ce53..45ad01b39 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,17 +4,12 @@ net7.0 kavitareader.com Kavita - 0.7.5.0 + 0.7.6.0 en true - - - - - diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 1d67ea41b..e3ba7a6c9 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -8,24 +8,24 @@ "name": "kavita-webui", "version": "0.4.2", "dependencies": { - "@angular/animations": "^16.0.2", - "@angular/cdk": "^16.0.1", - "@angular/common": "^16.0.2", - "@angular/compiler": "^16.0.2", - "@angular/core": "^16.0.2", - "@angular/forms": "^16.0.2", - "@angular/localize": "^16.0.2", - "@angular/platform-browser": "^16.0.2", - "@angular/platform-browser-dynamic": "^16.0.2", - "@angular/router": "^16.0.2", + "@angular/animations": "^16.1.6", + "@angular/cdk": "^16.1.5", + "@angular/common": "^16.1.6", + "@angular/compiler": "^16.1.6", + "@angular/core": "^16.1.6", + "@angular/forms": "^16.1.6", + "@angular/localize": "^16.1.6", + "@angular/platform-browser": "^16.1.6", + "@angular/platform-browser-dynamic": "^16.1.6", + "@angular/router": "^16.1.6", "@fortawesome/fontawesome-free": "^6.4.0", "@iharbeck/ngx-virtual-scroller": "^16.0.0", "@iplab/ngx-file-upload": "^16.0.1", - "@microsoft/signalr": "^7.0.5", + "@microsoft/signalr": "^7.0.9", "@ng-bootstrap/ng-bootstrap": "^15.1.0", "@popperjs/core": "^2.11.7", "@swimlane/ngx-charts": "^20.1.2", - "@tweenjs/tween.js": "^20.0.3", + "@tweenjs/tween.js": "^21.0.0", "@types/file-saver": "^2.0.5", "bootstrap": "^5.2.3", "eventsource": "^2.0.2", @@ -34,8 +34,9 @@ "ng-circle-progress": "^1.7.1", "ngx-color-picker": "^14.0.0", "ngx-extended-pdf-viewer": "^16.2.16", - "ngx-file-drop": "^15.0.0", - "ngx-slider-v2": "^15.0.4", + "ngx-file-drop": "^16.0.0", + "ngx-slider-v2": "^16.0.2", + "ngx-stars": "^1.6.5", "ngx-toastr": "^17.0.2", "rxjs": "^7.8.0", "screenfull": "^6.0.2", @@ -44,22 +45,22 @@ "zone.js": "^0.13.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.0.2", - "@angular-eslint/builder": "^16.0.2", - "@angular-eslint/eslint-plugin": "^16.0.2", - "@angular-eslint/eslint-plugin-template": "^16.0.2", - "@angular-eslint/schematics": "^16.0.2", - "@angular-eslint/template-parser": "^16.0.2", - "@angular/cli": "^16.0.2", - "@angular/compiler-cli": "^16.0.2", + "@angular-devkit/build-angular": "^16.1.5", + "@angular-eslint/builder": "^16.1.0", + "@angular-eslint/eslint-plugin": "^16.1.0", + "@angular-eslint/eslint-plugin-template": "^16.1.0", + "@angular-eslint/schematics": "^16.1.0", + "@angular-eslint/template-parser": "^16.1.0", + "@angular/cli": "^16.1.5", + "@angular/compiler-cli": "^16.1.6", "@types/d3": "^7.4.0", - "@types/node": "^20.2.1", - "@typescript-eslint/eslint-plugin": "5.48.1", - "@typescript-eslint/parser": "5.59.6", - "eslint": "^8.41.0", + "@types/node": "^20.4.4", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "eslint": "^8.45.0", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", - "typescript": "~5.0.4", + "typescript": "^5.1.6", "webpack-bundle-analyzer": "^4.8.0" } }, @@ -85,12 +86,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1601.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.3.tgz", - "integrity": "sha512-HvW51cCEoIYe2mYqcmnm2RZiMMFbFn7iIdsjbCJe7etFhcG+Y3hGDZMh4IFSiQiss+pwPSYOvQY2zwGrndMgLw==", + "version": "0.1601.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.5.tgz", + "integrity": "sha512-f6oOXR+0gXdMl2papEkTt28GJJBsYtuuoHSQYM09UltkXPkj4bc9QEOzRXKQ0hDjNYYj3UT00E3CalBO/5uGTA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.3", + "@angular-devkit/core": "16.1.5", "rxjs": "7.8.1" }, "engines": { @@ -100,17 +101,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.3.tgz", - "integrity": "sha512-1scrdUdKRa9TkJ9jev/KRzFttbLUVACQvVRL0G67nUAdtJ/bQX8eui85axpCNPFihK4ReSW3R4lrgcVC2NUSoA==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.5.tgz", + "integrity": "sha512-F78nvdDgVknqA9MekZ6xnmwKLH+qIQ5gBuP+/JnUiSZs1u1i8qS/GGNUL+T/Zsxk1HMRbG+erQ7N6nn1sBKMMw==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1601.3", - "@angular-devkit/build-webpack": "0.1601.3", - "@angular-devkit/core": "16.1.3", + "@angular-devkit/architect": "0.1601.5", + "@angular-devkit/build-webpack": "0.1601.5", + "@angular-devkit/core": "16.1.5", "@babel/core": "7.22.5", - "@babel/generator": "7.22.5", + "@babel/generator": "7.22.7", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.5", "@babel/plugin-proposal-async-generator-functions": "7.20.7", @@ -120,7 +121,7 @@ "@babel/runtime": "7.22.5", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.1.3", + "@ngtools/webpack": "16.1.5", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -228,12 +229,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1601.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.3.tgz", - "integrity": "sha512-744+72vi/Vx010VxizGgilhpnDCOG29qyhMmu7BkUhtpq8E8eQn2HU3nPpxAqrg3bKVAwD7v3F111MVIhub8kA==", + "version": "0.1601.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.5.tgz", + "integrity": "sha512-Mc61mrSFFpplBMl11r8ryUrKRDf9Clugnpe8770JcoDe99xEsBFwUUMRS9xNqmVroCgdLMxFijgLSI1ANkUvMg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.3", + "@angular-devkit/architect": "0.1601.5", "rxjs": "7.8.1" }, "engines": { @@ -247,9 +248,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.3.tgz", - "integrity": "sha512-cFhNdJHumNMZGD3NYxOtNuMGRQXeDnKbwvK+IJmKAttXt8na6EvURR/ZxZOI7rl/YRVX+vcNSdtXz3hE6g+Isw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.5.tgz", + "integrity": "sha512-0Mui2Nuib7kzOLWkz18v1OdxK6wd0SWdSFsAXavrRv03495vv+JUqVq0z0vGMtcbURkjOxIwdj3coj+Y0szkPQ==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -273,12 +274,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.3.tgz", - "integrity": "sha512-hWEuQnfQOgcSs4YX6iF4QR/34ROeSPaMi7lQOYg33hStg+pnk/JDdIU0f2nrIIz3t0jqAj+5VXVLBJvOCd84vg==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.5.tgz", + "integrity": "sha512-s6D/I0pfJJ7+XaAYyXh6IsS3Tya4WKeBuVcWWE7IK6TMEd5a1yDQ5O9RO2/G8UcxBwlKZmeuFbJkclpF6q3hYA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.3", + "@angular-devkit/core": "16.1.5", "jsonc-parser": "3.2.0", "magic-string": "0.30.0", "ora": "5.4.1", @@ -291,13 +292,13 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.0.3.tgz", - "integrity": "sha512-pv/CrnOHHOnBqhyBmqUPsIHKXOHYMJztxYJ83tjxeXL5Moyu5e6CBMIQ58UtqmgWfEIA3n7owYy9KvHTJcemyQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.1.0.tgz", + "integrity": "sha512-KIkE2SI1twFKoCiF/k2VR3ojOcc7TD1xPyY4kbUrx/Gxp+XEzar7O29I/ztzL4eHPBM+Uh3/NwS/jvjjBxjgAg==", "dev": true, "dependencies": { - "@nx/devkit": "16.2.2", - "nx": "16.2.2" + "@nx/devkit": "16.5.1", + "nx": "16.5.1" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -305,19 +306,19 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.0.3.tgz", - "integrity": "sha512-8zwY6ustiPXBEF3+jELKVwGk6j2HJn7GHbqAhDFR02YiE27iRMSGTHIAWGs6ZI7F1JgfrIsOHrUgzC1x95K6rg==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.1.0.tgz", + "integrity": "sha512-5EFAWXuFJADr3imo/ZYshY8s0K7U7wyysnE2LXnpT9PAi5rmkzt70UNZNRuamCbXr4tdIiu+fXWOj7tUuJKnnw==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.0.3.tgz", - "integrity": "sha512-1c+dFytcQDOA2wJ8/rtydMV6UYq1BgVfOcBXOr0WJxC9g8Cad9czcUOkW41WGrTp5kICMliV0ypH5eEaCM2WDQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.1.0.tgz", + "integrity": "sha512-BFzzJJlgQgWc8avdSBkaDWAzNSUqcwWy0L1iZSBdXGoIOxj72kLbwe99emb8M+rUfCveljQkeM2pcYu8XLbJIA==", "dev": true, "dependencies": { - "@angular-eslint/utils": "16.0.3", - "@typescript-eslint/utils": "5.59.7" + "@angular-eslint/utils": "16.1.0", + "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -325,16 +326,16 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.0.3.tgz", - "integrity": "sha512-OKTMWOjC7F5tdv7gm2tlmgyr/uVyS1RWJZn4X/6D6p0kOpiDXmajtbYHD5tzbshX2Ep62Nt+rg8+1XGHrU0ScA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.1.0.tgz", + "integrity": "sha512-wQHWR5vqWGgO7mqoG5ixXeplIlz/OmxBJE9QMLPTZE8GdaTx8+F/5J37OWh84zCpD3mOa/FHYZxBDm2MfUmA1Q==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", - "@angular-eslint/utils": "16.0.3", - "@typescript-eslint/type-utils": "5.59.7", - "@typescript-eslint/utils": "5.59.7", - "aria-query": "5.1.3", + "@angular-eslint/bundled-angular-compiler": "16.1.0", + "@angular-eslint/utils": "16.1.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "aria-query": "5.3.0", "axobject-query": "3.1.1" }, "peerDependencies": { @@ -343,16 +344,16 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.0.3.tgz", - "integrity": "sha512-vRdSY0ovE+wfTvYeguPp/QAxvGejLADO8CzJkas0PxdCQiyLuTscKsYE82XcvX2kitMexvH71lNF0ggnGoMRXA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.1.0.tgz", + "integrity": "sha512-L1tmP3R2krHyveaRXAvn/SeDoBFNpS1VtPPrzZm1NYr1qPcAxf3NtG2nnoyVFu6WZGt59ZGHNQ/dZxnXvm0UGg==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "16.0.3", - "@angular-eslint/eslint-plugin-template": "16.0.3", - "@nx/devkit": "16.2.2", + "@angular-eslint/eslint-plugin": "16.1.0", + "@angular-eslint/eslint-plugin-template": "16.1.0", + "@nx/devkit": "16.5.1", "ignore": "5.2.4", - "nx": "16.2.2", + "nx": "16.5.1", "strip-json-comments": "3.1.1", "tmp": "0.2.1" }, @@ -361,12 +362,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.0.3.tgz", - "integrity": "sha512-IAWdwp/S9QC3EMiVxSS0E3ABy9PSidN3PW0Ll2EtM3mzXMYlpZXmxqd+B1xV/xKWzhk1Mp04QX8hHfG6Vq+qaQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.1.0.tgz", + "integrity": "sha512-DOQtzVehtbO7+BQ+FMOXRsxGRjHb3ve6M+S4qASKTiI+twtONjRODcHezD3N4PDkjpKPbOnk7YnFsHur5csUNw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", + "@angular-eslint/bundled-angular-compiler": "16.1.0", "eslint-scope": "^7.0.0" }, "peerDependencies": { @@ -375,13 +376,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.0.3.tgz", - "integrity": "sha512-QsbUVHJLk+fE08/D4y3wOyGk1iX2LVSygw+uzilbaAXfjD5/c0Ei5FbVx2mMYPk+aOl4yrvGQW3dmetMiAR0MQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.1.0.tgz", + "integrity": "sha512-u5XscYUq1F/7RuwyVIV2a280QL27lyQz434VYR+Np/oO21NGj5jxoRKb55xhXT9EFVs5Sy4JYeEUp6S75J/cUw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", - "@typescript-eslint/utils": "5.59.7" + "@angular-eslint/bundled-angular-compiler": "16.1.0", + "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -389,9 +390,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.1.3.tgz", - "integrity": "sha512-ET6ahrlbOyTYXOTouKs2VJxx0CMTrYkfz0HfI6IHnSKBC6wguDxXYnamMouHgrCkDDEB5qClfGHyS9se0AOX4w==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.1.6.tgz", + "integrity": "sha512-LsU3/qXom/tLB76Exvjz+7SkifwJ9QG/+gSjWj+DTVlj1+dO7awb8iWQi+YmTmqN7ijTJi9ye6is3iuJSYuCLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -399,13 +400,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.3" + "@angular/core": "16.1.6" } }, "node_modules/@angular/cdk": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.1.3.tgz", - "integrity": "sha512-PsBcJSIX6D1w1OhHfcfi21Dug/eBWexlQ1XuU3CkLxC4BLvmpOEtugRKwIhSpaio3RauSaQydvlDHkiQsQbiKw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.1.5.tgz", + "integrity": "sha512-8wjYhLwW9bWicBYSdDXuN71SBP7NbJmXs+XiWiRkaFUkVDeU9z8Qkitogl+qqsSXvsOmi+12MowrbJ3tPizaLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -419,15 +420,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.3.tgz", - "integrity": "sha512-D0gU12z/N2oJ+s6pggAnWYrTUZ+2duGb3Y5oUyClsubz7JWpAwHjSZpb8exPUrgYhr+qIEMGO685y1JazJQ2tA==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.5.tgz", + "integrity": "sha512-um9hWEU3HUMRnQnaDfw0QSNdDLbdLA9tSzp81kz3Rx2VFsSssCABq6xQ+7w09xCUx0jg88r1lBPc2pwV7Pu7FQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.3", - "@angular-devkit/core": "16.1.3", - "@angular-devkit/schematics": "16.1.3", - "@schematics/angular": "16.1.3", + "@angular-devkit/architect": "0.1601.5", + "@angular-devkit/core": "16.1.5", + "@angular-devkit/schematics": "16.1.5", + "@schematics/angular": "16.1.5", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -453,9 +454,9 @@ } }, "node_modules/@angular/common": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.3.tgz", - "integrity": "sha512-ZzJ6EwQHUkiZYV0zH/UxyUYW5uxomsyk7tdtqZIxAR5m2ktYkQ5XlqgPjBO8voF54Rs5Ot43RkPCLesbZyJDsw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.6.tgz", + "integrity": "sha512-30Y8DGs+oosA+BcDZd1SuZ4HDT+DVZ6lVT4L+mBUH1BSkNna08FrbmrGQxO82CcxU6ZK0q1YLVkkb5cGx8y9ew==", "dependencies": { "tslib": "^2.3.0" }, @@ -463,14 +464,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.3", + "@angular/core": "16.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.3.tgz", - "integrity": "sha512-7Ckvssk9+s5xLyXvp72IwAw5vd/Osa3tR6oiQatdbw+O3XjLO04QycoGXwkp/fYVexGsjFyOn6QJ5n1F/PYPbQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.6.tgz", + "integrity": "sha512-BVDfKZLMb7mmLwdek+ZyzI43Zv0WNNNqnYpMeOI6egmkhtjNCxpQAy2YFKgNPse3bBGP8tKutwAtBB+Lqu/Kcw==", "dependencies": { "tslib": "^2.3.0" }, @@ -478,7 +479,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.3" + "@angular/core": "16.1.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -487,9 +488,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.3.tgz", - "integrity": "sha512-aUqnIV9rRTBNgiQRS0Gv6lhghaGj1vpVRyXgiE4VnTR9uBONSsGKMNALYBBhXRTSk2e0cvutt0ubLgmNpdyWyQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.6.tgz", + "integrity": "sha512-unWmH2CcuCkVuEr1fQuzlJtSGzRirsyg8cGQmRh16orx6N8oa/fCvxYRSOed/5aE9YiRpIFZ2TQWT2myY10/6Q==", "dev": true, "dependencies": { "@babel/core": "7.22.5", @@ -510,14 +511,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.1.3", + "@angular/compiler": "16.1.6", "typescript": ">=4.9.3 <5.2" } }, "node_modules/@angular/core": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.3.tgz", - "integrity": "sha512-yhRo9hVS8KhfcEgzciWuRWF4Pnnko98bmSJTqd7u8Kys6z3Uj0qgXMssXHIPUALe3mQKjVkdSZPLIZ9/CaVn/Q==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.6.tgz", + "integrity": "sha512-tUXvVLc+Vbl8Se7hajwyUTNmKD9uPq+SZH6x8sRM2n5azzjBZltOoJfzxK5JKAkiFf/KkQhteHkMBfoDLZ2tmw==", "dependencies": { "tslib": "^2.3.0" }, @@ -530,9 +531,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.1.3.tgz", - "integrity": "sha512-9tJHgoi/Jmeo30zfnReVZWFcd1WthR+QwYUNwPev+ys58u1mB0cDGORvROySmC2YUyXFSpXt8sxwyWCkYvaV2w==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.1.6.tgz", + "integrity": "sha512-6MMQx3qCFrXyX4sSNvQRLRm6smGZshMjuWSCSkyEvvTYpZSA3F7h8ba762PDKYE3vMAON2OczCr8y9MyjBEruA==", "dependencies": { "tslib": "^2.3.0" }, @@ -540,19 +541,19 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.3", - "@angular/core": "16.1.3", - "@angular/platform-browser": "16.1.3", + "@angular/common": "16.1.6", + "@angular/core": "16.1.6", + "@angular/platform-browser": "16.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.3.tgz", - "integrity": "sha512-tLn1eo8WjuH/IDiPxZYw2HFLm3vo+VJ/hjhxFiEdJBIeUyickGXu58o8hO3M1ir51IAyMLFbWEkCRH3e6sAQxA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.6.tgz", + "integrity": "sha512-DNcY2N6OXNe/SFqupEggGFSnKgKJSHGMN+1qIpadG7cLJitAJS1Z6yh57+sqOJQT8LPP2qm53ab+NF0tib45tg==", "dependencies": { "@babel/core": "7.22.5", - "fast-glob": "3.2.12", + "fast-glob": "3.3.0", "yargs": "^17.2.1" }, "bin": { @@ -564,14 +565,29 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.1.3", - "@angular/compiler-cli": "16.1.3" + "@angular/compiler": "16.1.6", + "@angular/compiler-cli": "16.1.6" + } + }, + "node_modules/@angular/localize/node_modules/fast-glob": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" } }, "node_modules/@angular/platform-browser": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.3.tgz", - "integrity": "sha512-qZA6Lua2fpBe+KD/QArY/4hilypSZFcTcJsPjZwIzo5pavXqYDI8BVghwh5dcZoUa56hVRDJjv+XW6kl8m9Tdw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.6.tgz", + "integrity": "sha512-qHwQpqhcWFAwroWwe7iiSsJrs38lrW82vHEm/sX/fcbUuLtqEDaNMf90KqzeSPIkFEkX5wwUZxdRwEh2bepf6g==", "dependencies": { "tslib": "^2.3.0" }, @@ -579,9 +595,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.1.3", - "@angular/common": "16.1.3", - "@angular/core": "16.1.3" + "@angular/animations": "16.1.6", + "@angular/common": "16.1.6", + "@angular/core": "16.1.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -590,9 +606,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.3.tgz", - "integrity": "sha512-UHxSWpPB5+FSv8zm8T+4ZikLqyy+VE6GlOLp/DdgEz77j81rz2C1pMqozwTnVbD16XbI4rhTp+RFY3C9ArWOtw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.6.tgz", + "integrity": "sha512-NZ3bcXgWq9k0pJE7EvZsatvY8++5NzAtxCdV9IM+fqgzBzkSR4le0Iud4hdBSNQF1DOwwB8KdU7Xpe9q4YsdqA==", "dependencies": { "tslib": "^2.3.0" }, @@ -600,16 +616,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.3", - "@angular/compiler": "16.1.3", - "@angular/core": "16.1.3", - "@angular/platform-browser": "16.1.3" + "@angular/common": "16.1.6", + "@angular/compiler": "16.1.6", + "@angular/core": "16.1.6", + "@angular/platform-browser": "16.1.6" } }, "node_modules/@angular/router": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.3.tgz", - "integrity": "sha512-bkn8cWGBKKZidDaP+R7g/S/6miSfH8iP24d2k86Awo+vaO+7G/5WWGfKJMKK8UNM/A5ueX6ugAZrMHpQ9e6Y4w==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.6.tgz", + "integrity": "sha512-4+MniaDghKurV117URJZzCQBNCdMfiu6lRpTdcKWbHgGZqOQRG9N/gcHah5eLYVB0s6mfQ1OQ9HavNyjujF6Fg==", "dependencies": { "tslib": "^2.3.0" }, @@ -617,9 +633,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.3", - "@angular/core": "16.1.3", - "@angular/platform-browser": "16.1.3", + "@angular/common": "16.1.6", + "@angular/core": "16.1.6", + "@angular/platform-browser": "16.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -686,9 +702,9 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz", + "integrity": "sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ==", "dependencies": { "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", @@ -2343,20 +2359,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz", - "integrity": "sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ==", - "dependencies": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", @@ -3140,9 +3142,9 @@ "dev": true }, "node_modules/@microsoft/signalr": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.7.tgz", - "integrity": "sha512-RMWZLTxnjWPSaS9PYZxXAttql2JDM/+IsSJk0nACFhpLjnSw8UWfvUxOv/QjZSqLxhuksXxzBJ/91xUP6Y7Nvg==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.9.tgz", + "integrity": "sha512-aGfBLAYTh+6ydYvLXV/jcocWr8KKmTOgWyl/mDx5Hzrii1aAfrn+bpBNzrl5sto5ehsHCdTIzTCuOCT3baIjOw==", "dependencies": { "abort-controller": "^3.0.0", "eventsource": "^2.0.2", @@ -3168,9 +3170,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.3.tgz", - "integrity": "sha512-YTL1RzP7ErJqskx+ZwdC/nWsOSBfC4yYWmMyWL2J0d+oJ3N2XIzrKVoDcZ4IVzv3Du+3zoGp0ups/wWXvfzM/Q==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.5.tgz", + "integrity": "sha512-XQkanGAtB9S2EE14xa/04hPNYSkAzJOeaHJkirfqk/p40p8nA2pNsfLYCMd2N7K4kzqcET8UYAZ+wIpQxp12HA==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -3362,36 +3364,36 @@ } }, "node_modules/@nrwl/devkit": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.2.2.tgz", - "integrity": "sha512-R8OSh33HtGycSuu0KshpH/tsTdi6j4w7DuIb+Sa59UDIkchpvMeNAz8tj/05Z2tTntDZnYqPkmCs6rkZ4PvY4Q==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.5.1.tgz", + "integrity": "sha512-NB+DE/+AFJ7lKH/WBFyatJEhcZGj25F24ncDkwjZ6MzEiSOGOJS0LaV/R+VUsmS5EHTPXYOpn3zHWWAcJhyOmA==", "dev": true, "dependencies": { - "@nx/devkit": "16.2.2" + "@nx/devkit": "16.5.1" } }, "node_modules/@nrwl/tao": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-16.2.2.tgz", - "integrity": "sha512-cPj6b+wSWs2WNFQ0p1fMyrvSLjkKJo7vXQTtd7MXNJT2NWEZdCtRy+nidZzjs7gKvVXGdZ8zDBXmCHWorOieXw==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-16.5.1.tgz", + "integrity": "sha512-x+gi/fKdM6uQNIti9exFlm3V5LBP3Y8vOEziO42HdOigyrXa0S0HD2WMpccmp6PclYKhwEDUjKJ39xh5sdh4Ig==", "dev": true, "dependencies": { - "nx": "16.2.2" + "nx": "16.5.1" }, "bin": { "tao": "index.js" } }, "node_modules/@nx/devkit": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-16.2.2.tgz", - "integrity": "sha512-MTYzetk4AQ9u2syEb9z+drDsu6U6NRAXVuUDMNg0tpZcbtE9bCSLH2ngfvTCqmLrAMBsJZRdv0twS1iepMhlAg==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-16.5.1.tgz", + "integrity": "sha512-T1acZrVVmJw/sJ4PIGidCBYBiBqlg/jT9e8nIGXLSDS20xcLvfo4zBQf8UZLrmHglnwwpDpOWuVJCp2rYA5aDg==", "dev": true, "dependencies": { - "@nrwl/devkit": "16.2.2", + "@nrwl/devkit": "16.5.1", "ejs": "^3.1.7", "ignore": "^5.0.4", - "semver": "7.3.4", + "semver": "7.5.3", "tmp": "~0.2.1", "tslib": "^2.3.0" }, @@ -3399,43 +3401,10 @@ "nx": ">= 15 <= 17" } }, - "node_modules/@nx/devkit/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nx/devkit/node_modules/semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nx/devkit/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@nx/nx-darwin-arm64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.2.2.tgz", - "integrity": "sha512-CKfyLl92mhWqpv1hRTj3WgjVBY6yj3Et5T31m1N0assNWdTfuSB4ycdWzdlxXHx3yptnTOD/FCymTpUQI0GZRQ==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.5.1.tgz", + "integrity": "sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q==", "cpu": [ "arm64" ], @@ -3449,9 +3418,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.2.2.tgz", - "integrity": "sha512-++uDfp/Oo8DDVU53DiJVkRNjNbOLzahDH6dINeA/3yTCU/IS0wXoaoclNZBReMWlDKTVvWgLF/eSbGINMqUHRg==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz", + "integrity": "sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==", "cpu": [ "x64" ], @@ -3464,10 +3433,26 @@ "node": ">= 10" } }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz", + "integrity": "sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.2.2.tgz", - "integrity": "sha512-A4XFk63Q7fxgZaHnigIeofp/xOT2ZGDoNUyzld+UTlyJyNcClcOcqrro74aKOCG7PH0D56oE06JW3g7GKszgsA==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz", + "integrity": "sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==", "cpu": [ "arm" ], @@ -3481,9 +3466,9 @@ } }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.2.2.tgz", - "integrity": "sha512-aQpTLVSawFVr33pBWjj8elqvjA5uWvzDW7hGaFQPgWgmjxrtJikIAkcLjfNOz8XYjRAP4OZkTVh4/E3GUch0kQ==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz", + "integrity": "sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==", "cpu": [ "arm64" ], @@ -3497,9 +3482,9 @@ } }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.2.2.tgz", - "integrity": "sha512-20vyNYQ2SYSaWdxORj9HdOyGxiqE8SauaFiBjjid6/e5mSyaSKu+HHGsvhDUqzlWn3OaABKBqx0iYa9Kmf3BOQ==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz", + "integrity": "sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==", "cpu": [ "arm64" ], @@ -3513,9 +3498,9 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.2.2.tgz", - "integrity": "sha512-0G8kYpEmGHD+tT7RvUEvVXvPbvQD9GfEjeWEzZAdNAAMJu7JFjIo/oZDJYV7cMvXnC+tbpI9Gba5xfv8Al95eA==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", + "integrity": "sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==", "cpu": [ "x64" ], @@ -3529,9 +3514,9 @@ } }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.2.2.tgz", - "integrity": "sha512-Incv7DbKLfh6kakzMBuy6GYRgI+jEdZBRiFw0GoN9EsknmrPT/URn+w6uuicGGEXOLYpO3HUO3E374+b5Wz2zg==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz", + "integrity": "sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==", "cpu": [ "x64" ], @@ -3545,9 +3530,9 @@ } }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.2.2.tgz", - "integrity": "sha512-8m+Usj9faCl0pdQLFeBGhbYUObT3/tno5oGMPtJLyRjITNvTZAaIS4FFctp/rwJPehDBRQsUxwMJ2JRaU4jQdA==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz", + "integrity": "sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==", "cpu": [ "arm64" ], @@ -3561,9 +3546,9 @@ } }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.2.2.tgz", - "integrity": "sha512-liHtyVVOttcqHIV3Xrg/1AJzEgfiOCeqJsleHXHGgPr1fxPx7SIZaa3/QnDY1lNMN+t6Gvj0/r2Ba3iuptYD3Q==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz", + "integrity": "sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==", "cpu": [ "x64" ], @@ -3620,13 +3605,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.3.tgz", - "integrity": "sha512-bNSxCLf6f+/dsQ1k3PhcZhrC/qgJSCpM6h3m6ATpjR+tYW/v7WR1OyE5r3DQmDe7NJSazBvpbrRtg8xjRsMzvw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.5.tgz", + "integrity": "sha512-GwSweqKGjMc9oqU6fukqYKlBflCCDA/l2qfA60YIdf4SubaZnIUpkpNXTbWeFRPJh8zQShb8OZE7cVj8MZRruw==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.3", - "@angular-devkit/schematics": "16.1.3", + "@angular-devkit/core": "16.1.5", + "@angular-devkit/schematics": "16.1.5", "jsonc-parser": "3.2.0" }, "engines": { @@ -3768,9 +3753,9 @@ } }, "node_modules/@tweenjs/tween.js": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-20.0.3.tgz", - "integrity": "sha512-SYUe1UgY5HM05EB4+0B4arq2IPjvyzKXoklXKxSYrc2IFxGm1cBrqg5XbiB5uwbs0xY5j+rj986NAJMM0KZaUw==" + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz", + "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==" }, "node_modules/@types/body-parser": { "version": "1.19.2", @@ -4152,9 +4137,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz", - "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==", + "version": "20.4.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", + "integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==", "dev": true }, "node_modules/@types/qs": { @@ -4230,31 +4215,34 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz", - "integrity": "sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", + "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.48.1", - "@typescript-eslint/type-utils": "5.48.1", - "@typescript-eslint/utils": "5.48.1", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/type-utils": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", - "ignore": "^5.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4263,65 +4251,25 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz", - "integrity": "sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", + "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.48.1", - "@typescript-eslint/utils": "5.48.1", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/utils": "6.1.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz", - "integrity": "sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz", - "integrity": "sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.48.1", - "@typescript-eslint/visitor-keys": "5.48.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4330,73 +4278,84 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.1.tgz", - "integrity": "sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", + "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.48.1", - "@typescript-eslint/types": "5.48.1", - "@typescript-eslint/typescript-estree": "5.48.1", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=10" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz", - "integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", + "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4404,64 +4363,17 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz", - "integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz", - "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz", - "integrity": "sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", + "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.48.1", - "@typescript-eslint/visitor-keys": "5.48.1" + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz", - "integrity": "sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4469,13 +4381,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz", - "integrity": "sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.7", - "@typescript-eslint/utils": "5.59.7", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -4496,9 +4408,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz", - "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4509,13 +4421,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz", - "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4536,12 +4448,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz", - "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", + "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -4553,12 +4465,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz", - "integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", + "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4566,21 +4478,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz", - "integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", + "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4592,35 +4504,51 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz", - "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=10" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.7.tgz", - "integrity": "sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.7", - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/typescript-estree": "5.59.7", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -4636,13 +4564,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz", - "integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4653,9 +4581,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz", - "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4666,13 +4594,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz", - "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4693,12 +4621,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz", - "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", + "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -4732,29 +4660,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz", - "integrity": "sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", + "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.48.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.1.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz", - "integrity": "sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4938,13 +4853,16 @@ "dev": true }, "node_modules/@yarnpkg/parsers": { - "version": "3.0.0-rc.48.1", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.48.1.tgz", - "integrity": "sha512-qEewJouhRvaecGjbkjz9kMKn96UASbDodNrE5MYy2TrXkHcisIkbMxZdGBYfAq+s1dFtCSx/5H4k5bEkfakM+A==", + "version": "3.0.0-rc.46", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", + "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", "dev": true, "dependencies": { "js-yaml": "^3.10.0", "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.15.0" } }, "node_modules/@zkochan/js-yaml": { @@ -5254,12 +5172,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -6725,6 +6643,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -7142,9 +7069,9 @@ } }, "node_modules/eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -7172,7 +7099,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -7184,7 +7110,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -7213,33 +7138,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", @@ -7726,6 +7624,7 @@ "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -10636,9 +10535,9 @@ } }, "node_modules/ngx-file-drop": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-15.0.0.tgz", - "integrity": "sha512-P1BRa9w+l6CFCQFEHRaUcQy8DvrgwMnWZUWwndcXQ+Qqqa3BOXfrN26uDd+px9FD/P5OkKidhglI7VRX6qmLwg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz", + "integrity": "sha512-33RPoZBAiMkV110Rzu3iOrzGcG5M20S4sAiwLzNylfJobu9qVw5XR83FhUelSeqJRoaDxXBRKAozYCSnUf2CNw==", "dependencies": { "tslib": "^2.3.0" }, @@ -10652,18 +10551,30 @@ } }, "node_modules/ngx-slider-v2": { - "version": "15.0.6", - "resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-15.0.6.tgz", - "integrity": "sha512-UAQxLa1ORv5Jx+Exo1C06rdLKkhwQ+CzFqCQKmgoF5Ns0y90lW7NJtOXFUAUC5SWtJJzkfI0iqLX5OZhNQslTA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-16.0.2.tgz", + "integrity": "sha512-Lpl7SlErL+tJJvTRZYdyZoXTThKN8Ro1z3vscJQ1O5azHXwvbv3pnTcsOwY4ltfaP+dpzY27KL1QXyDr6QMaxQ==", "dependencies": { "detect-passive-events": "^2.0.3", "rxjs": "^7.4.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": "^15.0.4", - "@angular/core": "^15.0.4", - "@angular/forms": "^15.0.4" + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0" + } + }, + "node_modules/ngx-stars": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/ngx-stars/-/ngx-stars-1.6.5.tgz", + "integrity": "sha512-ZJ2R1XgIkBj5TsHSP8tl3QvbRBCi1awLO03Aod7ffDNG1i785ODw9gYlOAvsIrUmnY9ha1h21tTs5pBWXqA+5Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=2.0.0", + "@angular/core": ">=2.0.0" } }, "node_modules/ngx-toastr": { @@ -10970,16 +10881,16 @@ } }, "node_modules/nx": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/nx/-/nx-16.2.2.tgz", - "integrity": "sha512-gOcpqs6wf8YdFIq6P0IlMxBGr2c27pM55zpqO7epSlN6NqW6SOFKnZa+6z4NV9qmifMqzWPx2VF0BY54ARuqYg==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-16.5.1.tgz", + "integrity": "sha512-I3hJRE4hG7JWAtncWwDEO3GVeGPpN0TtM8xH5ArZXyDuVeTth/i3TtJzdDzqXO1HHtIoAQN0xeq4n9cLuMil5g==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nrwl/tao": "16.2.2", + "@nrwl/tao": "16.5.1", "@parcel/watcher": "2.0.4", "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "^3.0.0-rc.18", + "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.6", "axios": "^1.0.0", "chalk": "^4.1.0", @@ -11000,7 +10911,7 @@ "minimatch": "3.0.5", "npm-run-path": "^4.0.1", "open": "^8.4.0", - "semver": "7.3.4", + "semver": "7.5.3", "string-width": "^4.2.3", "strong-log-transformer": "^2.1.0", "tar-stream": "~2.2.0", @@ -11015,15 +10926,16 @@ "nx": "bin/nx.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "16.2.2", - "@nx/nx-darwin-x64": "16.2.2", - "@nx/nx-linux-arm-gnueabihf": "16.2.2", - "@nx/nx-linux-arm64-gnu": "16.2.2", - "@nx/nx-linux-arm64-musl": "16.2.2", - "@nx/nx-linux-x64-gnu": "16.2.2", - "@nx/nx-linux-x64-musl": "16.2.2", - "@nx/nx-win32-arm64-msvc": "16.2.2", - "@nx/nx-win32-x64-msvc": "16.2.2" + "@nx/nx-darwin-arm64": "16.5.1", + "@nx/nx-darwin-x64": "16.5.1", + "@nx/nx-freebsd-x64": "16.5.1", + "@nx/nx-linux-arm-gnueabihf": "16.5.1", + "@nx/nx-linux-arm64-gnu": "16.5.1", + "@nx/nx-linux-arm64-musl": "16.5.1", + "@nx/nx-linux-x64-gnu": "16.5.1", + "@nx/nx-linux-x64-musl": "16.5.1", + "@nx/nx-win32-arm64-msvc": "16.5.1", + "@nx/nx-win32-x64-msvc": "16.5.1" }, "peerDependencies": { "@swc-node/register": "^1.4.2", @@ -11147,18 +11059,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/nx/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/nx/node_modules/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", @@ -11171,21 +11071,6 @@ "node": "*" } }, - "node_modules/nx/node_modules/semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/nx/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11198,12 +11083,6 @@ "node": ">=8" } }, - "node_modules/nx/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -12227,18 +12106,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -13635,6 +13502,18 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -13776,16 +13655,16 @@ "dev": true }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/UI/Web/package.json b/UI/Web/package.json index 0aabacae9..ffa564f00 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -12,24 +12,24 @@ }, "private": true, "dependencies": { - "@angular/animations": "^16.0.2", - "@angular/cdk": "^16.0.1", - "@angular/common": "^16.0.2", - "@angular/compiler": "^16.0.2", - "@angular/core": "^16.0.2", - "@angular/forms": "^16.0.2", - "@angular/localize": "^16.0.2", - "@angular/platform-browser": "^16.0.2", - "@angular/platform-browser-dynamic": "^16.0.2", - "@angular/router": "^16.0.2", + "@angular/animations": "^16.1.6", + "@angular/cdk": "^16.1.5", + "@angular/common": "^16.1.6", + "@angular/compiler": "^16.1.6", + "@angular/core": "^16.1.6", + "@angular/forms": "^16.1.6", + "@angular/localize": "^16.1.6", + "@angular/platform-browser": "^16.1.6", + "@angular/platform-browser-dynamic": "^16.1.6", + "@angular/router": "^16.1.6", "@fortawesome/fontawesome-free": "^6.4.0", "@iharbeck/ngx-virtual-scroller": "^16.0.0", "@iplab/ngx-file-upload": "^16.0.1", - "@microsoft/signalr": "^7.0.5", + "@microsoft/signalr": "^7.0.9", "@ng-bootstrap/ng-bootstrap": "^15.1.0", "@popperjs/core": "^2.11.7", "@swimlane/ngx-charts": "^20.1.2", - "@tweenjs/tween.js": "^20.0.3", + "@tweenjs/tween.js": "^21.0.0", "@types/file-saver": "^2.0.5", "bootstrap": "^5.2.3", "eventsource": "^2.0.2", @@ -38,8 +38,9 @@ "ng-circle-progress": "^1.7.1", "ngx-color-picker": "^14.0.0", "ngx-extended-pdf-viewer": "^16.2.16", - "ngx-file-drop": "^15.0.0", - "ngx-slider-v2": "^15.0.4", + "ngx-file-drop": "^16.0.0", + "ngx-slider-v2": "^16.0.2", + "ngx-stars": "^1.6.5", "ngx-toastr": "^17.0.2", "rxjs": "^7.8.0", "screenfull": "^6.0.2", @@ -48,22 +49,22 @@ "zone.js": "^0.13.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.0.2", - "@angular-eslint/builder": "^16.0.2", - "@angular-eslint/eslint-plugin": "^16.0.2", - "@angular-eslint/eslint-plugin-template": "^16.0.2", - "@angular-eslint/schematics": "^16.0.2", - "@angular-eslint/template-parser": "^16.0.2", - "@angular/cli": "^16.0.2", - "@angular/compiler-cli": "^16.0.2", + "@angular-devkit/build-angular": "^16.1.5", + "@angular-eslint/builder": "^16.1.0", + "@angular-eslint/eslint-plugin": "^16.1.0", + "@angular-eslint/eslint-plugin-template": "^16.1.0", + "@angular-eslint/schematics": "^16.1.0", + "@angular-eslint/template-parser": "^16.1.0", + "@angular/cli": "^16.1.5", + "@angular/compiler-cli": "^16.1.6", "@types/d3": "^7.4.0", - "@types/node": "^20.2.1", - "@typescript-eslint/eslint-plugin": "5.48.1", - "@typescript-eslint/parser": "5.59.6", - "eslint": "^8.41.0", + "@types/node": "^20.4.4", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "eslint": "^8.45.0", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", - "typescript": "~5.0.4", + "typescript": "^5.1.6", "webpack-bundle-analyzer": "^4.8.0" } } diff --git a/UI/Web/src/app/_models/rating.ts b/UI/Web/src/app/_models/rating.ts index e501aa30a..a4c4b79ed 100644 --- a/UI/Web/src/app/_models/rating.ts +++ b/UI/Web/src/app/_models/rating.ts @@ -5,4 +5,5 @@ export interface Rating { meanScore: number; favoriteCount: number; provider: ScrobbleProvider; + providerUrl: string | undefined; } diff --git a/UI/Web/src/app/_models/readers/personal-toc.ts b/UI/Web/src/app/_models/readers/personal-toc.ts new file mode 100644 index 000000000..3d4c3c9af --- /dev/null +++ b/UI/Web/src/app/_models/readers/personal-toc.ts @@ -0,0 +1,8 @@ +export interface PersonalToC { + chapterId: number; + pageNumber: number; + title: string; + bookScrollId: string | undefined; + /* Ui Only */ + position: 0; +} diff --git a/UI/Web/src/app/_models/series.ts b/UI/Web/src/app/_models/series.ts index 77eed1a90..c994a3527 100644 --- a/UI/Web/src/app/_models/series.ts +++ b/UI/Web/src/app/_models/series.ts @@ -27,6 +27,7 @@ export interface Series { * User's rating (0-5) */ userRating: number; + hasUserRated: boolean; libraryId: number; /** * DateTime the entity was created diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 0aec8f647..f94fba4fd 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -1,6 +1,6 @@ import { HttpClient, HttpParams } from '@angular/common/http'; -import {DestroyRef, inject, Injectable} from '@angular/core'; -import { Location } from '@angular/common'; +import {DestroyRef, Inject, inject, Injectable} from '@angular/core'; +import {DOCUMENT, Location} from '@angular/common'; import { Router } from '@angular/router'; import { environment } from 'src/environments/environment'; import { ChapterInfo } from '../manga-reader/_models/chapter-info'; @@ -17,9 +17,8 @@ import { FileDimension } from '../manga-reader/_models/file-dimension'; import screenfull from 'screenfull'; import { TextResonse } from '../_types/text-response'; import { AccountService } from './account.service'; -import { Subject, takeUntil } from 'rxjs'; -import { OnDestroy } from '@angular/core'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {PersonalToC} from "../_models/readers/personal-toc"; export const CHAPTER_ID_DOESNT_EXIST = -1; export const CHAPTER_ID_NOT_FETCHED = -2; @@ -279,4 +278,51 @@ export class ReaderService { this.location.back(); } } + + removePersonalToc(chapterId: number, pageNumber: number, title: string) { + return this.httpClient.delete(this.baseUrl + `reader/ptoc?chapterId=${chapterId}&pageNum=${pageNumber}&title=${encodeURIComponent(title)}`); + } + + getPersonalToC(chapterId: number) { + return this.httpClient.get>(this.baseUrl + 'reader/ptoc?chapterId=' + chapterId); + } + + createPersonalToC(libraryId: number, seriesId: number, volumeId: number, chapterId: number, pageNumber: number, title: string, bookScrollId: string | null) { + return this.httpClient.post(this.baseUrl + 'reader/create-ptoc', {libraryId, seriesId, volumeId, chapterId, pageNumber, title, bookScrollId}); + } + + getElementFromXPath(path: string) { + const node = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + if (node?.nodeType === Node.ELEMENT_NODE) { + return node as Element; + } + return null; + } + + /** + * + * @param element + * @param pureXPath Will ignore shortcuts like id('') + */ + getXPathTo(element: any, pureXPath = false): string { + if (element === null) return ''; + if (!pureXPath) { + if (element.id !== '') { return 'id("' + element.id + '")'; } + if (element === document.body) { return element.tagName; } + } + + + let ix = 0; + const siblings = element.parentNode?.childNodes || []; + for (let sibling of siblings) { + if (sibling === element) { + return this.getXPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']'; + } + if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { + ix++; + } + + } + return ''; + } } diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index e815410f7..9edca977c 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -1,32 +1,20 @@ import {HttpClient, HttpParams} from '@angular/common/http'; -import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core'; -import { of, ReplaySubject, Subject } from 'rxjs'; -import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; +import {Injectable} from '@angular/core'; +import { map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; -import { Preferences } from '../_models/preferences/preferences'; -import { User } from '../_models/user'; -import { Router } from '@angular/router'; -import { EVENTS, MessageHubService } from './message-hub.service'; -import { ThemeService } from './theme.service'; -import { InviteUserResponse } from '../_models/auth/invite-user-response'; -import { UserUpdateEvent } from '../_models/events/user-update-event'; -import { UpdateEmailResponse } from '../_models/auth/update-email-response'; -import { AgeRating } from '../_models/metadata/age-rating'; -import { AgeRestriction } from '../_models/metadata/age-restriction'; import { TextResonse } from '../_types/text-response'; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ScrobbleError} from "../_models/scrobbling/scrobble-error"; import {ScrobbleEvent} from "../_models/scrobbling/scrobble-event"; import {ScrobbleHold} from "../_models/scrobbling/scrobble-hold"; -import {PaginatedResult, Pagination} from "../_models/pagination"; +import {PaginatedResult} from "../_models/pagination"; import {ScrobbleEventFilter} from "../_models/scrobbling/scrobble-event-filter"; import {UtilityService} from "../shared/_services/utility.service"; -import {ReadingList} from "../_models/reading-list"; export enum ScrobbleProvider { Kavita = 0, AniList= 1, Mal = 2, + GoogleBooks = 3 } @Injectable({ @@ -34,7 +22,6 @@ export enum ScrobbleProvider { }) export class ScrobblingService { - private readonly destroyRef = inject(DestroyRef); baseUrl = environment.apiUrl; diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 266e2c43e..82af8c73f 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -13,7 +13,7 @@ Use fully qualified URL of the email service. Do not include ending slash.
- + diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html index 88ae883f3..1768f4c83 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html @@ -4,7 +4,7 @@

WebP/AVIF can drastically reduce space requirements for files. WebP/AVIF is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit Can I Use WebP or Can I Use AVIF. You cannot convert back to PNG once you've gone to WebP/AVIF. You would need to refresh covers on your libraries to regenerate all covers. Bookmarks and favicons cannot be converted.

- +
diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index bfe408d4b..605ba4814 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -47,6 +47,7 @@ export class ManageMediaSettingsComponent implements OnInit { saveSettings() { const modelSettings = Object.assign({}, this.serverSettings); modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10); + modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value; this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => { this.serverSettings = settings; diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.html b/UI/Web/src/app/admin/manage-system/manage-system.component.html index 15f4abea1..e917bccf9 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.html +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.html @@ -13,29 +13,30 @@

More Info


-
Home page:
-
-
-
Wiki:
- -
-
-
Discord:
- -
-
-
Donations:
- -
- -
-
Feature Requests:
- +
+
+
Wiki:
+ +
+
+
Discord:
+ +
+
+
Donations:
+ +
+ +
+
Feature Requests:
+ + +
diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html new file mode 100644 index 000000000..2b0f9199d --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html @@ -0,0 +1,44 @@ +
+ +
+ + +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ + +
+
+ This field is required +
+
+
+
+
+
+
+ + +
diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.scss b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.scss new file mode 100644 index 000000000..434de65b5 --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.scss @@ -0,0 +1,8 @@ +.overlay { + background-color: var(--br-actionbar-bg-color); + color: var(--bs-body-bg); + padding: 5px; + border-radius: 4px; + z-index: 9999; + width: 100vw; +} diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts new file mode 100644 index 000000000..1f1383ddf --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts @@ -0,0 +1,141 @@ +import { + ChangeDetectionStrategy, ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, EventEmitter, + inject, + Input, + OnInit, Output, +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {fromEvent, merge, of} from "rxjs"; +import {catchError, filter, tap} from "rxjs/operators"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect"; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; +import {ReaderService} from "../../../_services/reader.service"; +import {ToastrService} from "ngx-toastr"; + +enum BookLineOverlayMode { + None = 0, + Bookmark = 1 +} + +@Component({ + selector: 'app-book-line-overlay', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './book-line-overlay.component.html', + styleUrls: ['./book-line-overlay.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BookLineOverlayComponent implements OnInit { + @Input({required: true}) libraryId!: number; + @Input({required: true}) seriesId!: number; + @Input({required: true}) volumeId!: number; + @Input({required: true}) chapterId!: number; + @Input({required: true}) pageNumber: number = 0; + @Input({required: true}) parent: ElementRef | undefined; + @Output() refreshToC: EventEmitter = new EventEmitter(); + + xPath: string = ''; + selectedText: string = ''; + mode: BookLineOverlayMode = BookLineOverlayMode.None; + bookmarkForm: FormGroup = new FormGroup({ + name: new FormControl('', [Validators.required]), + }); + + private readonly destroyRef = inject(DestroyRef); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly readerService = inject(ReaderService); + + get BookLineOverlayMode() { return BookLineOverlayMode; } + constructor(private elementRef: ElementRef, private toastr: ToastrService) {} + + + ngOnInit() { + if (this.parent) { + + const mouseUp$ = fromEvent(this.parent.nativeElement, 'mouseup'); + const touchEnd$ = fromEvent(this.parent.nativeElement, 'touchend'); + + merge(mouseUp$, touchEnd$) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((event: MouseEvent | TouchEvent) => { + this.handleEvent(event); + }); + } + } + + handleEvent(event: MouseEvent | TouchEvent) { + const selection = window.getSelection(); + if (!event.target) return; + + if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) { + this.reset(); + return; + } + + this.selectedText = selection ? selection.toString().trim() : ''; + + if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) { + this.xPath = this.readerService.getXPathTo(event.target); + if (this.xPath !== '') { + this.xPath = '//' + this.xPath; + } + + event.preventDefault(); + event.stopPropagation(); + } + this.cdRef.markForCheck(); + } + + switchMode(mode: BookLineOverlayMode) { + this.mode = mode; + this.cdRef.markForCheck(); + if (this.mode === BookLineOverlayMode.Bookmark) { + this.bookmarkForm.get('name')?.setValue(this.selectedText); + this.focusOnBookmarkInput(); + } + } + + createPTOC() { + this.readerService.createPersonalToC(this.libraryId, this.seriesId, this.volumeId, this.chapterId, this.pageNumber, + this.bookmarkForm.get('name')?.value, this.xPath).pipe(catchError(err => { + this.focusOnBookmarkInput(); + return of(); + })).subscribe(() => { + this.reset(); + this.refreshToC.emit(); + this.cdRef.markForCheck(); + }); + } + + focusOnBookmarkInput() { + if (this.mode !== BookLineOverlayMode.Bookmark) return; + setTimeout(() => this.elementRef.nativeElement.querySelector('#bookmark-name')?.focus(), 10); + } + + reset() { + this.bookmarkForm.reset(); + this.mode = BookLineOverlayMode.None; + this.xPath = ''; + this.selectedText = ''; + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + } + this.cdRef.markForCheck(); + } + + async copy() { + const selection = window.getSelection(); + if (selection) { + await navigator.clipboard.writeText(selection.toString()); + this.toastr.info('Copied to clipboard'); + } + this.reset(); + } + + +} 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 dc22194f6..7957579e0 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 @@ -2,6 +2,14 @@
Skip to main content + +
Book Settings @@ -63,7 +71,23 @@
  • Table of Contents - + +
  • @@ -73,7 +97,7 @@
    -
    +
    - -
    @@ -99,7 +121,6 @@
    -
    -
    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 506571b33..051e877ed 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 @@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, - ElementRef, + ElementRef, EventEmitter, HostListener, inject, Inject, @@ -16,8 +16,8 @@ import { import { DOCUMENT, Location, NgTemplateOutlet, NgIf, NgStyle, NgClass } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; -import { forkJoin, fromEvent, of, Subject } from 'rxjs'; -import { catchError, debounceTime, take, takeUntil } from 'rxjs/operators'; +import { forkJoin, fromEvent, of } from 'rxjs'; +import { catchError, debounceTime, take } from 'rxjs/operators'; import { Chapter } from 'src/app/_models/chapter'; import { AccountService } from 'src/app/_services/account.service'; import { NavService } from 'src/app/_services/nav.service'; @@ -46,13 +46,20 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { TableOfContentsComponent } from '../table-of-contents/table-of-contents.component'; import { NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbNavOutlet, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { DrawerComponent } from '../../../shared/drawer/drawer.component'; +import {BookLineOverlayComponent} from "../book-line-overlay/book-line-overlay.component"; +import { + PersonalTableOfContentsComponent, + PersonalToCEvent +} from "../personal-table-of-contents/personal-table-of-contents.component"; enum TabID { Settings = 1, - TableOfContents = 2 + TableOfContents = 2, + PersonalTableOfContents = 3 } + interface HistoryPoint { /** * Page Number @@ -94,7 +101,7 @@ const elementLevelStyles = ['line-height', 'font-family']; ]) ], standalone: true, - imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip] + imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip, BookLineOverlayComponent, PersonalTableOfContentsComponent] }) export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { @@ -150,6 +157,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { * Belongs to the drawer component */ activeTabId: TabID = TabID.Settings; + /** + * Sub Nav tab id + */ + tocId: TabID = TabID.TableOfContents; /** * Belongs to drawer component */ @@ -280,6 +291,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { writingStyle: WritingStyle = WritingStyle.Horizontal; + /** + * Used to refresh the Personal PoC + */ + refreshPToC: EventEmitter = new EventEmitter(); + private readonly destroyRef = inject(DestroyRef); @ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef; @@ -666,6 +682,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { @HostListener('window:keydown', ['$event']) handleKeyPress(event: KeyboardEvent) { + const activeElement = document.activeElement as HTMLElement; + const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA'; + if (isInputFocused) return; + if (event.key === KEY_CODES.RIGHT_ARROW) { this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS); } else if (event.key === KEY_CODES.LEFT_ARROW) { @@ -783,6 +803,15 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.loadPage('id("' + event.part + '")'); } + /** + * From personal table of contents/bookmark + * @param event + */ + loadChapterPart(event: PersonalToCEvent) { + this.setPageNum(event.pageNum); + this.loadPage(event.scrollPart); + } + /** * Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value * from 'kavita-part', which will cause the reader to scroll to the marker. @@ -987,7 +1016,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } 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) { setTimeout(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.scrollWidth, this.bookContentElemRef.nativeElement)); @@ -1213,7 +1241,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { intersectingEntries.sort(this.sortElements); if (intersectingEntries.length > 0) { - let path = this.getXPathTo(intersectingEntries[0]); + let path = this.readerService.getXPathTo(intersectingEntries[0]); if (path === '') { return; } if (!path.startsWith('id')) { path = '//html[1]/' + path; @@ -1339,35 +1367,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - getElementFromXPath(path: string) { - const node = this.document.evaluate(path, this.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + const node = this.document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (node?.nodeType === Node.ELEMENT_NODE) { return node as Element; } return null; } - getXPathTo(element: any): string { - if (element === null) return ''; - if (element.id !== '') { return 'id("' + element.id + '")'; } - if (element === this.document.body) { return element.tagName; } - - - let ix = 0; - const siblings = element.parentNode?.childNodes || []; - for (let sibling of siblings) { - if (sibling === element) { - return this.getXPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']'; - } - if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { - ix++; - } - - } - return ''; - } - /** * Turns off Incognito mode. This can only happen once if the user clicks the icon. This will modify URL state */ @@ -1583,4 +1590,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.mousePosition.x = $event.screenX; this.mousePosition.y = $event.screenY; } + + refreshPersonalToC() { + this.refreshPToC.emit(); + } + + protected readonly undefined = undefined; } diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html new file mode 100644 index 000000000..a8213b027 --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html @@ -0,0 +1,22 @@ +
    +
    + Nothing Bookmarked yet +
    +
      +
    • + Page {{page}} +
        +
      • + {{bookmark.title}} + +
      • +
      +
    • +
    +
    diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.scss b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.scss new file mode 100644 index 000000000..86163686d --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.scss @@ -0,0 +1,15 @@ +.table-of-contents li { + cursor: pointer; + + &.active { + font-weight: bold; + } +} + +.chapter-title { + padding-inline-start: 1rem; +} +.ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts new file mode 100644 index 000000000..9e7555639 --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, DestroyRef, EventEmitter, + Inject, + inject, + Input, + OnInit, + Output +} from '@angular/core'; +import {CommonModule, DOCUMENT} from '@angular/common'; +import {ReaderService} from "../../../_services/reader.service"; +import {PersonalToC} from "../../../_models/readers/personal-toc"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; + +export interface PersonalToCEvent { + pageNum: number; + scrollPart: string | undefined; +} + +@Component({ + selector: 'app-personal-table-of-contents', + standalone: true, + imports: [CommonModule, NgbTooltip], + templateUrl: './personal-table-of-contents.component.html', + styleUrls: ['./personal-table-of-contents.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PersonalTableOfContentsComponent implements OnInit { + + @Input({required: true}) chapterId!: number; + @Input({required: true}) pageNum: number = 0; + @Input({required: true}) tocRefresh!: EventEmitter; + @Output() loadChapter: EventEmitter = new EventEmitter(); + + private readonly readerService = inject(ReaderService); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); + + + bookmarks: {[key: number]: Array} = []; + + get Pages() { + return Object.keys(this.bookmarks).map(p => parseInt(p, 10)); + } + + constructor(@Inject(DOCUMENT) private document: Document) {} + + ngOnInit() { + this.tocRefresh.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.load(); + }); + + this.load(); + } + + load() { + this.readerService.getPersonalToC(this.chapterId).subscribe(res => { + res.forEach(t => { + if (!this.bookmarks.hasOwnProperty(t.pageNumber)) { + this.bookmarks[t.pageNumber] = []; + } + this.bookmarks[t.pageNumber].push(t); + }) + this.cdRef.markForCheck(); + }); + } + + loadChapterPage(pageNum: number, scrollPart: string | undefined) { + this.loadChapter.emit({pageNum, scrollPart}); + } + + removeBookmark(bookmark: PersonalToC) { + this.readerService.removePersonalToc(bookmark.chapterId, bookmark.pageNumber, bookmark.title).subscribe(() => { + this.bookmarks[bookmark.pageNumber] = this.bookmarks[bookmark.pageNumber].filter(t => t.title != bookmark.title); + + if (this.bookmarks[bookmark.pageNumber].length === 0) { + delete this.bookmarks[bookmark.pageNumber]; + } + this.cdRef.markForCheck(); + }); + } + +} diff --git a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.ts b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.ts index 9177b59dd..cc8a22995 100644 --- a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.ts +++ b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.ts @@ -1,5 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; -import { Subject } from 'rxjs'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { BookChapterItem } from '../../_models/book-chapter-item'; import { NgIf, NgFor } from '@angular/common'; @@ -11,7 +10,7 @@ import { NgIf, NgFor } from '@angular/common'; standalone: true, imports: [NgIf, NgFor] }) -export class TableOfContentsComponent implements OnDestroy { +export class TableOfContentsComponent { @Input({required: true}) chapterId!: number; @Input({required: true}) pageNum!: number; @@ -20,17 +19,8 @@ export class TableOfContentsComponent implements OnDestroy { @Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter(); - private onDestroy: Subject = new Subject(); - - pageAnchors: {[n: string]: number } = {}; - constructor() {} - ngOnDestroy(): void { - this.onDestroy.next(); - this.onDestroy.complete(); - } - cleanIdSelector(id: string) { const tokens = id.split('/'); if (tokens.length > 0) { diff --git a/UI/Web/src/app/book-reader/_services/book.service.ts b/UI/Web/src/app/book-reader/_services/book.service.ts index 4ca377636..65549ab48 100644 --- a/UI/Web/src/app/book-reader/_services/book.service.ts +++ b/UI/Web/src/app/book-reader/_services/book.service.ts @@ -5,12 +5,6 @@ import { environment } from 'src/environments/environment'; import { BookChapterItem } from '../_models/book-chapter-item'; import { BookInfo } from '../_models/book-info'; -export interface BookPage { - bookTitle: string; - styles: string; - html: string; -} - export interface FontFamily { /** * What the user should see @@ -32,7 +26,7 @@ export class BookService { constructor(private http: HttpClient) { } getFontFamilies(): Array { - return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'}, + return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'}, {title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'}, {title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}]; } diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html index 2c2d684b8..5ed8ce116 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html @@ -9,16 +9,16 @@
    -
    @@ -63,7 +63,7 @@ - +
    @@ -73,7 +73,7 @@ {{item.title}} - +
    @@ -98,7 +98,7 @@
  • {{tabs[TabID.Cover].title}} - - {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} @@ -143,7 +143,7 @@ Pages: {{file.pages | number:''}}
  • - Added: + Added: {{data.created | date: 'short' | defaultDate}} @@ -166,4 +166,4 @@
    - \ No newline at end of file + diff --git a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.html b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.html index e325170a9..62d507173 100644 --- a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.html +++ b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.html @@ -1,107 +1,121 @@ -
    - -
    - - {{chapter.releaseDate | date:'shortDate' | defaultDate}} - -
    -
    -
    - -
    - - {{chapter.ageRating | ageRating | async}} - -
    -
    -
    +
    +
    + + + {{item.title}} + - -
    - - {{totalPages | compactNumber}} Pages - -
    -
    -
    + + {{item.title}} + +
    - -
    - - {{totalWordCount | compactNumber}} Words - -
    -
    -
    +
    + +
    + + {{chapter.releaseDate | date:'shortDate' | defaultDate}} + +
    +
    +
    - -
    - - <1 Hour - - {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}} - - -
    -
    + +
    + + {{chapter.ageRating | ageRating | async}} + +
    +
    +
    - -
    -
    - - {{chapter.created | date:'short' | defaultDate}} - -
    -
    + +
    + + {{totalPages | compactNumber}} Pages + +
    +
    +
    - -
    -
    - - {{size | bytes}} - -
    -
    + +
    + + {{totalWordCount | compactNumber}} Words + +
    +
    +
    - -
    -
    - - {{entity.id}} - -
    - -
    -
    - - - - - -
    -
    + +
    + + <1 Hour + + {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}} + + +
    +
    - -
    -
    - - {{chapter.isbn}} - -
    -
    - - +
    - - {{chapter.lastReadingProgress | date: 'shortDate'}} - + + {{chapter.created | date:'short' | defaultDate}} +
    -
    -
    +
    + + +
    +
    + + {{size | bytes}} + +
    +
    + + +
    +
    + + {{entity.id}} + +
    + +
    +
    + + + + + +
    +
    + + +
    +
    + + {{chapter.isbn}} + +
    +
    + + +
    +
    + + {{chapter.lastReadingProgress | date: 'shortDate'}} + +
    +
    +
    +
    diff --git a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts index 8259129df..54172094f 100644 --- a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts +++ b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts @@ -24,11 +24,13 @@ import {BytesPipe} from "../../pipe/bytes.pipe"; import {CompactNumberPipe} from "../../pipe/compact-number.pipe"; import {AgeRatingPipe} from "../../pipe/age-rating.pipe"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component"; +import {FilterQueryParam} from "../../shared/_services/filter-utilities.service"; @Component({ selector: 'app-entity-info-cards', standalone: true, - imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip], + imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent], templateUrl: './entity-info-cards.component.html', styleUrls: ['./entity-info-cards.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -36,6 +38,7 @@ import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; export class EntityInfoCardsComponent implements OnInit { @Input({required: true}) entity!: Volume | Chapter; + @Input({required: true}) libraryId!: number; /** * This will pull extra information */ @@ -75,8 +78,6 @@ export class EntityInfoCardsComponent implements OnInit { return this.chapter.webLinks.split(','); } - - constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {} ngOnInit(): void { @@ -127,8 +128,5 @@ export class EntityInfoCardsComponent implements OnInit { this.cdRef.markForCheck(); } - getTimezone(timezone: string): string { - const localDate = new Date(timezone); - return localDate.toLocaleString('en-US', { timeZoneName: 'short' }).split(' ')[3]; - } + protected readonly FilterQueryParam = FilterQueryParam; } diff --git a/UI/Web/src/app/cards/list-item/list-item.component.html b/UI/Web/src/app/cards/list-item/list-item.component.html index 95ca6d39c..9dc48a53d 100644 --- a/UI/Web/src/app/cards/list-item/list-item.component.html +++ b/UI/Web/src/app/cards/list-item/list-item.component.html @@ -22,7 +22,7 @@ Read - +
    {{Title}}
    @@ -30,7 +30,7 @@
    - +
    diff --git a/UI/Web/src/app/cards/list-item/list-item.component.ts b/UI/Web/src/app/cards/list-item/list-item.component.ts index fed7fc667..553590b32 100644 --- a/UI/Web/src/app/cards/list-item/list-item.component.ts +++ b/UI/Web/src/app/cards/list-item/list-item.component.ts @@ -5,15 +5,14 @@ import { EventEmitter, inject, Input, - OnDestroy, OnInit, Output } from '@angular/core'; import { ToastrService } from 'ngx-toastr'; -import { map, Observable, Subject, takeUntil } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { Download } from 'src/app/shared/_models/download'; import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service'; -import { UtilityService } from 'src/app/shared/_services/utility.service'; +import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; import { LibraryType } from 'src/app/_models/library'; import { RelationKind } from 'src/app/_models/series-detail/relation-kind'; @@ -42,6 +41,7 @@ export class ListItemComponent implements OnInit { * Volume or Chapter to render */ @Input({required: true}) entity!: Volume | Chapter; + @Input({required: true}) libraryId!: number; /** * Image to show */ @@ -103,8 +103,14 @@ export class ListItemComponent implements OnInit { return ''; } + get ShowExtended() { + return this.utilityService.getActiveBreakpoint() === Breakpoint.Desktop; + } - constructor(private utilityService: UtilityService, private downloadService: DownloadService, + protected readonly Breakpoint = Breakpoint; + + + constructor(public utilityService: UtilityService, private downloadService: DownloadService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } ngOnInit(): void { diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index f0e757c54..81ce522e2 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -72,6 +72,7 @@ export class DashboardComponent implements OnInit { this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => { + if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return; this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries]; this.cdRef.markForCheck(); }); 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 ff462febb..b5da925c0 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 @@ -9,8 +9,8 @@ import { OnInit } from '@angular/core'; import { NgbModal, NgbModalRef, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; -import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; -import { map, shareReplay, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component'; @@ -79,7 +79,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => { + this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event: Message) => { if (event.event === EVENTS.NotificationProgress) { this.processNotificationProgressEvent(event); } else if (event.event === EVENTS.Error) { @@ -94,6 +94,9 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { this.infoSource.next(values); this.activeEvents += 1; this.cdRef.markForCheck(); + } else if (event.event === EVENTS.UpdateAvailable) { + console.log('event: ', event); + this.handleUpdateAvailableClick(event.payload); } }); @@ -150,10 +153,15 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { } - handleUpdateAvailableClick(message: NotificationProgressEvent) { + handleUpdateAvailableClick(message: NotificationProgressEvent | UpdateVersionEvent) { if (this.updateNotificationModalRef != null) { return; } this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); - this.updateNotificationModalRef.componentInstance.updateData = message.body as UpdateVersionEvent; + if (message.hasOwnProperty('body')) { + this.updateNotificationModalRef.componentInstance.updateData = (message as NotificationProgressEvent).body as UpdateVersionEvent; + } else { + this.updateNotificationModalRef.componentInstance.updateData = message as UpdateVersionEvent; + } + this.updateNotificationModalRef.closed.subscribe(() => { this.updateNotificationModalRef = null; }); @@ -176,7 +184,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { } config.header = event.title; config.content = event.subTitle; - var result = await this.confirmService.alert(event.subTitle || event.title, config); + const result = await this.confirmService.alert(event.subTitle || event.title, config); if (result) { this.removeErrorOrInfo(event); } diff --git a/UI/Web/src/app/pipe/provider-image.pipe.ts b/UI/Web/src/app/pipe/provider-image.pipe.ts index edb7b63b1..0697e49e5 100644 --- a/UI/Web/src/app/pipe/provider-image.pipe.ts +++ b/UI/Web/src/app/pipe/provider-image.pipe.ts @@ -13,6 +13,8 @@ export class ProviderImagePipe implements PipeTransform { return 'assets/images/ExternalServices/AniList.png'; case ScrobbleProvider.Mal: return 'assets/images/ExternalServices/MAL.png'; + case ScrobbleProvider.GoogleBooks: + return 'assets/images/ExternalServices/GoogleBooks.png'; case ScrobbleProvider.Kavita: return 'assets/images/logo-32.png'; } diff --git a/UI/Web/src/app/registration/_components/register/register.component.ts b/UI/Web/src/app/registration/_components/register/register.component.ts index bf2acbe85..0356d6e84 100644 --- a/UI/Web/src/app/registration/_components/register/register.component.ts +++ b/UI/Web/src/app/registration/_components/register/register.component.ts @@ -10,7 +10,7 @@ import { NgIf, NgTemplateOutlet } from '@angular/common'; import { SplashContainerComponent } from '../splash-container/splash-container.component'; /** - * This is exclusivly used to register the first user on the server and nothing else + * This is exclusively used to register the first user on the server and nothing else */ @Component({ selector: 'app-register', @@ -28,9 +28,9 @@ export class RegisterComponent { password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]), }); - constructor(private router: Router, private accountService: AccountService, + constructor(private router: Router, private accountService: AccountService, private toastr: ToastrService, private memberService: MemberService) { - + this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => { if (adminExists) { this.router.navigateByUrl('login'); diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html index 290fe4f44..4cf3663f8 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html @@ -3,9 +3,10 @@ popoverTitle="Your Rating + Overall" popoverClass="md-popover"> - {{userRating * 20}} - + {{overallRating}}% - % + {{userRating * 20}} + N/A + + {{overallRating}} + % @@ -22,13 +23,12 @@ - - - - - {{userRating * 20}}% + + {{userRating * 20}}% - {{rating.favoriteCount}} +
    {{rating.favoriteCount}}
    + Entry
    diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.scss b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.scss index bf59d1ef5..0dc21384e 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.scss +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.scss @@ -19,4 +19,19 @@ } } +.rating-star { + i { + position: relative; + display: inline-block; + padding-right: 0.1rem; + color: #d3d3d3; + } + .filled { + color: var(--primary-color); + + } +} +::ng-deep .star { + background-color: var(--primary-color); +} diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index e4b4d10a4..e1fcefd62 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -16,11 +16,13 @@ import {LoadingComponent} from "../../../shared/loading/loading.component"; import {AccountService} from "../../../_services/account.service"; import {LibraryType} from "../../../_models/library"; import {ProviderNamePipe} from "../../../pipe/provider-name.pipe"; +import {NgxStarsModule} from "ngx-stars"; +import {ThemeService} from "../../../_services/theme.service"; @Component({ selector: 'app-external-rating', standalone: true, - imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe], + imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule], templateUrl: './external-rating.component.html', styleUrls: ['./external-rating.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -29,15 +31,19 @@ import {ProviderNamePipe} from "../../../pipe/provider-name.pipe"; export class ExternalRatingComponent implements OnInit { @Input({required: true}) seriesId!: number; @Input({required: true}) userRating!: number; + @Input({required: true}) hasUserRated!: boolean; @Input({required: true}) libraryType!: LibraryType; private readonly cdRef = inject(ChangeDetectorRef); private readonly seriesService = inject(SeriesService); private readonly accountService = inject(AccountService); + private readonly themeService = inject(ThemeService); ratings: Array = []; isLoading: boolean = false; overallRating: number = -1; + starColor = this.themeService.getCssVariable('--rating-star-color'); + ngOnInit() { @@ -58,9 +64,11 @@ export class ExternalRatingComponent implements OnInit { }); } - updateRating(rating: any) { + updateRating(rating: number) { this.seriesService.updateRating(this.seriesId, rating).subscribe(() => { this.userRating = rating; + this.hasUserRated = true; + this.cdRef.markForCheck(); }); } } diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html new file mode 100644 index 000000000..2f3392f01 --- /dev/null +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html @@ -0,0 +1,21 @@ +
    +
    +
    {{heading}}
    +
    +
    + + + + + + + + + + + + + + +
    +
    diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.scss b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts new file mode 100644 index 000000000..e4dfbb7fc --- /dev/null +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts @@ -0,0 +1,37 @@ +import {ChangeDetectionStrategy, Component, ContentChild, inject, Input, TemplateRef} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {A11yClickDirective} from "../../../shared/a11y-click.directive"; +import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component"; +import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-badge.component"; +import {FilterQueryParam} from "../../../shared/_services/filter-utilities.service"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-metadata-detail', + standalone: true, + imports: [CommonModule, A11yClickDirective, BadgeExpanderComponent, TagBadgeComponent], + templateUrl: './metadata-detail.component.html', + styleUrls: ['./metadata-detail.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MetadataDetailComponent { + + @Input({required: true}) tags: Array = []; + @Input({required: true}) libraryId!: number; + @Input({required: true}) heading!: string; + @Input() queryParam: FilterQueryParam = FilterQueryParam.None; + @ContentChild('titleTemplate') titleTemplate!: TemplateRef; + @ContentChild('itemTemplate') itemTemplate?: TemplateRef; + + private readonly router = inject(Router); + protected readonly TagBadgeCursor = TagBadgeCursor; + + + goTo(queryParamName: FilterQueryParam, filter: any) { + if (queryParamName === FilterQueryParam.None) return; + let params: any = {}; + params[queryParamName] = filter; + params[FilterQueryParam.Page] = 1; + this.router.navigate(['library', this.libraryId], {queryParams: params}); + } +} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index b43fe046c..f76900b4a 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -174,7 +174,7 @@ - - -
    - - `${item.title}_${item.number}_${item.volumeId}_${item.pagesRead}`; - trackByRelatedSeriesIdentify = (index: number, item: RelatedSeris) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`; + trackByRelatedSeriesIdentify = (index: number, item: RelatedSeriesPair) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`; trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`; trackByStoryLineIdentity = (index: number, item: StoryLineItem) => { if (item.isChapter) { @@ -174,7 +174,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { /** * Related Series. Sorted by backend */ - relations: Array = []; + relations: Array = []; /** * Recommended Series */ @@ -342,7 +342,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.libraryId = parseInt(libraryId, 10); this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId); this.cdRef.markForCheck(); - this.loadSeries(this.seriesId); + this.loadSeries(this.seriesId, true); this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((val: PageLayoutMode | null) => { if (val == null) return; @@ -493,7 +493,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { }); } - loadSeries(seriesId: number) { + loadSeries(seriesId: number, loadExternal: boolean = false) { this.seriesService.getMetadata(seriesId).subscribe(metadata => { this.seriesMetadata = metadata; this.cdRef.markForCheck(); @@ -517,7 +517,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.libraryType = results.libType; this.series = results.series; - if (this.libraryType !== LibraryType.Comic) { + if (this.libraryType !== LibraryType.Comic && loadExternal) { this.loadReviews(true); } @@ -591,7 +591,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } createRelatedSeries(series: Series, relation: RelationKind) { - return {series, relation} as RelatedSeris; + return {series, relation} as RelatedSeriesPair; } /** @@ -751,11 +751,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => { window.scrollTo(0, 0); if (closeResult.success) { - this.seriesService.getSeries(this.seriesId).subscribe(s => { - this.series = s; - this.cdRef.detectChanges(); - }); - this.loadSeries(this.seriesId); } diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html index 53a050608..aa64662ad 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html @@ -2,219 +2,121 @@
    -
    -
    -
    Ratings
    -
    -
    - -
    -
    + + + + + + -
    -
    -
    Links
    -
    -
    - - - -
    -
    + + + + + + +
    -
    -
    -
    Genres
    -
    -
    - - - {{item.title}} - - -
    -
    -
    -
    -
    Tags
    -
    -
    - - - {{item.title}} - - -
    -
    -
    -
    -
    Collections
    -
    -
    - - - - {{item.title}} - - - -
    -
    -
    -
    -
    Reading Lists
    -
    -
    - - - - -   - (promoted) - - {{item.title}} - - - -
    -
    -
    -
    -
    Writers/Authors
    -
    -
    - - - - - -
    -
    + + {{item.title}} + + + + {{item.title}} + + + + + + {{item.title}} + + + + + + + + + +   + (promoted) + + {{item.title}} + + + + + + + + + + +
    -
    -
    -
    Cover Artists
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Characters
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Colorists
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Editors
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Inkers
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Letterers
    -
    -
    - - - - - -
    -
    -
    -
    -
    Translators
    -
    -
    - - - - - -
    -
    + + + + + -
    -
    -
    Pencillers
    -
    -
    - - - - - -
    -
    + + + + + + + + + + + + + + + + + -
    -
    -
    Publishers
    -
    -
    - - - - - -
    -
    @@ -226,5 +128,4 @@
    - diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index 0035c8be5..7819203f0 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -19,12 +19,15 @@ import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.co import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap"; import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component"; import {LibraryType} from "../../../_models/library"; +import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component"; @Component({ selector: 'app-series-metadata-detail', standalone: true, - imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent, ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent], + imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent, + ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent, + MetadataDetailComponent], templateUrl: './series-metadata-detail.component.html', styleUrls: ['./series-metadata-detail.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -74,11 +77,11 @@ export class SeriesMetadataDetailComponent implements OnChanges { this.seriesMetadata.letterers.length > 0 || this.seriesMetadata.pencillers.length > 0 || this.seriesMetadata.publishers.length > 0 || + this.seriesMetadata.characters.length > 0 || this.seriesMetadata.translators.length > 0; - if (this.seriesMetadata !== null) { - this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '
    '); - } + + this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '
    '); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index b0873eeb4..6708a6f6a 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -34,7 +34,11 @@ export enum FilterQueryParam { /** * This is a pagination control */ - Page = 'page' + Page = 'page', + /** + * Special case for the UI. Does not trigger filtering + */ + None = 'none' } @Injectable({ @@ -46,19 +50,19 @@ export class FilterUtilitiesService { /** * Updates the window location with a custom url based on filter and pagination objects - * @param pagination - * @param filter + * @param pagination + * @param filter */ updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) { const params = '?page=' + pagination.currentPage; - + const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter); window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination)); } /** - * Patches the page query param in the window location. - * @param pagination + * Patches the page query param in the window location. + * @param pagination */ updateUrlFromPagination(pagination: Pagination) { window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination)); @@ -127,7 +131,7 @@ export class FilterUtilitiesService { if (filter.seriesNameQuery !== '') { params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`; } - + return currentUrl + params; } @@ -262,7 +266,7 @@ export class FilterUtilitiesService { anyChanged = true; } - // Rating, seriesName, + // Rating, seriesName, const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating); if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) { filter.rating = parseInt(rating, 10); @@ -301,7 +305,7 @@ export class FilterUtilitiesService { filter.seriesNameQuery = decodeURIComponent(searchNameQuery); anyChanged = true; } - + return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better } diff --git a/UI/Web/src/app/shared/update-notification/update-notification-modal.component.html b/UI/Web/src/app/shared/update-notification/update-notification-modal.component.html index af2b2c1d8..59527fd09 100644 --- a/UI/Web/src/app/shared/update-notification/update-notification-modal.component.html +++ b/UI/Web/src/app/shared/update-notification/update-notification-modal.component.html @@ -1,8 +1,6 @@ \ No newline at end of file + How to Update + + Download + diff --git a/UI/Web/src/app/shared/update-notification/update-notification-modal.component.ts b/UI/Web/src/app/shared/update-notification/update-notification-modal.component.ts index fa5f3ed2b..6ac65b7d9 100644 --- a/UI/Web/src/app/shared/update-notification/update-notification-modal.component.ts +++ b/UI/Web/src/app/shared/update-notification/update-notification-modal.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'; import {CommonModule} from "@angular/common"; @@ -14,12 +14,21 @@ import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; styleUrls: ['./update-notification-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class UpdateNotificationModalComponent { +export class UpdateNotificationModalComponent implements OnInit { @Input({required: true}) updateData!: UpdateVersionEvent; + updateUrl: string = 'https://wiki.kavitareader.com/en/install/windows-install#updating-kavita'; constructor(public modal: NgbActiveModal) { } + ngOnInit() { + if (this.updateData.isDocker) { + this.updateUrl = 'https://wiki.kavitareader.com/en/install/docker-install#updating-kavita'; + } else { + this.updateUrl = 'https://wiki.kavitareader.com/en/install/windows-install#updating-kavita'; + } + } + close() { this.modal.close({success: false, series: undefined}); } diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss index 05bf23368..b08c8ae82 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss @@ -120,6 +120,10 @@ border-top-left-radius: var(--side-nav-border-radius); border-top-right-radius: var(--side-nav-border-radius); } + + &.no-donate { + height: calc((var(--vh)*100) - 56px); + } } .side-nav-overlay { diff --git a/UI/Web/src/assets/images/ExternalServices/AniList.png b/UI/Web/src/assets/images/ExternalServices/AniList.png index 11f68dd4d..9146b0f92 100644 Binary files a/UI/Web/src/assets/images/ExternalServices/AniList.png and b/UI/Web/src/assets/images/ExternalServices/AniList.png differ diff --git a/UI/Web/src/assets/images/ExternalServices/GoogleBooks.png b/UI/Web/src/assets/images/ExternalServices/GoogleBooks.png new file mode 100644 index 000000000..8d75d021d Binary files /dev/null and b/UI/Web/src/assets/images/ExternalServices/GoogleBooks.png differ diff --git a/UI/Web/src/assets/images/ExternalServices/MAL.png b/UI/Web/src/assets/images/ExternalServices/MAL.png index 3c148a9c9..f9c99e6f1 100644 Binary files a/UI/Web/src/assets/images/ExternalServices/MAL.png and b/UI/Web/src/assets/images/ExternalServices/MAL.png differ diff --git a/UI/Web/src/assets/images/ExternalServices/OpenLibrary.png b/UI/Web/src/assets/images/ExternalServices/OpenLibrary.png new file mode 100644 index 000000000..453ecdab9 Binary files /dev/null and b/UI/Web/src/assets/images/ExternalServices/OpenLibrary.png differ diff --git a/UI/Web/src/environments/environment.prod.ts b/UI/Web/src/environments/environment.prod.ts index b4d9702ce..54642eacc 100644 --- a/UI/Web/src/environments/environment.prod.ts +++ b/UI/Web/src/environments/environment.prod.ts @@ -5,6 +5,6 @@ export const environment = { production: true, apiUrl: `${BASE_URL}api/`, hubUrl:`${BASE_URL}hubs/`, - buyLink: 'https://buy.stripe.com/fZe6qsbrJ8bye88cMO?prefilled_promo_code=FREETRIAL', + buyLink: 'https://buy.stripe.com/3cs7uw67p2Re7JK4gj?prefilled_promo_code=FREETRIAL', manageLink: 'https://billing.stripe.com/p/login/28oaFRa3HdHWb5ecMM' }; diff --git a/UI/Web/src/theme/components/_anchors.scss b/UI/Web/src/theme/components/_anchors.scss index 35b45980b..950732f55 100644 --- a/UI/Web/src/theme/components/_anchors.scss +++ b/UI/Web/src/theme/components/_anchors.scss @@ -26,6 +26,10 @@ a.read-more-link { } } +td > a:not(.dark-exempt) { + color: var(--primary-color-darker-shade); +} + a { text-decoration: none; diff --git a/UI/Web/src/theme/themes/dark.scss b/UI/Web/src/theme/themes/dark.scss index af5ad11f4..bf8aa40a7 100644 --- a/UI/Web/src/theme/themes/dark.scss +++ b/UI/Web/src/theme/themes/dark.scss @@ -252,4 +252,7 @@ --review-spoiler-bg-color: var(--primary-color); --review-spoiler-text-color: var(--body-text-color); + /** Rating Star Color **/ + --rating-star-color: var(--primary-color); + } diff --git a/openapi.json b/openapi.json index 03774ecff..066e15664 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.7.4.5" + "version": "0.7.5.7" }, "servers": [ { @@ -5248,6 +5248,125 @@ } } }, + "/api/Reader/ptoc": { + "get": { + "tags": [ + "Reader" + ], + "summary": "Returns the user's personal table of contents for the given chapter", + "parameters": [ + { + "name": "chapterId", + "in": "query", + "description": "", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonalToCDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonalToCDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonalToCDto" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Reader" + ], + "parameters": [ + { + "name": "chapterId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "pageNum", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/Reader/create-ptoc": { + "post": { + "tags": [ + "Reader" + ], + "summary": "Create a new personal table of content entry for a given chapter", + "description": "The title and page number must be unique to that book", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonalToCDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonalToCDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonalToCDto" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/api/ReadingList": { "get": { "tags": [ @@ -10811,6 +10930,14 @@ "description": "A list of Devices which allows the user to send files to", "nullable": true }, + "tableOfContents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AppUserTableOfContent" + }, + "description": "A list of Table of Contents for a given Chapter", + "nullable": true + }, "apiKey": { "type": "string", "description": "An API Key to interact with external services, like OPDS", @@ -11179,9 +11306,13 @@ "format": "int32" }, "rating": { - "type": "integer", - "description": "A number between 0-5 that represents how good a series is.", - "format": "int32" + "type": "number", + "description": "A number between 0-5.0 that represents how good a series is.", + "format": "float" + }, + "hasBeenRated": { + "type": "boolean", + "description": "If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated" }, "review": { "type": "string", @@ -11230,6 +11361,78 @@ }, "additionalProperties": false }, + "AppUserTableOfContent": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "pageNumber": { + "type": "integer", + "description": "The page to bookmark", + "format": "int32" + }, + "title": { + "type": "string", + "description": "The title of the bookmark. Defaults to Page {PageNumber} if not set", + "nullable": true + }, + "seriesId": { + "type": "integer", + "format": "int32" + }, + "series": { + "$ref": "#/components/schemas/Series" + }, + "chapterId": { + "type": "integer", + "format": "int32" + }, + "chapter": { + "$ref": "#/components/schemas/Chapter" + }, + "volumeId": { + "type": "integer", + "format": "int32" + }, + "libraryId": { + "type": "integer", + "format": "int32" + }, + "bookScrollId": { + "type": "string", + "description": "For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page", + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModified": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, + "appUser": { + "$ref": "#/components/schemas/AppUser" + }, + "appUserId": { + "type": "integer", + "description": "User this table of content belongs to", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "A personal table of contents for a given user linked with a given book" + }, "BookChapterItem": { "type": "object", "properties": { @@ -12368,6 +12571,40 @@ }, "additionalProperties": false }, + "CreatePersonalToCDto": { + "type": "object", + "properties": { + "chapterId": { + "type": "integer", + "format": "int32" + }, + "volumeId": { + "type": "integer", + "format": "int32" + }, + "seriesId": { + "type": "integer", + "format": "int32" + }, + "libraryId": { + "type": "integer", + "format": "int32" + }, + "pageNumber": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "bookScrollId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "CreateReadingListDto": { "type": "object", "properties": { @@ -13757,6 +13994,28 @@ }, "additionalProperties": false }, + "PersonalToCDto": { + "type": "object", + "properties": { + "chapterId": { + "type": "integer", + "format": "int32" + }, + "pageNumber": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "bookScrollId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "ProgressDto": { "required": [ "chapterId", @@ -13840,6 +14099,10 @@ "type": "integer", "description": "Misleading name but is the source of data (like a review coming from AniList)", "format": "int32" + }, + "providerUrl": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -14544,11 +14807,6 @@ "format": "int32", "nullable": true }, - "processDateUtc": { - "type": "string", - "format": "date-time", - "nullable": true - }, "lastModified": { "type": "string", "format": "date-time" @@ -15108,9 +15366,13 @@ "format": "date-time" }, "userRating": { - "type": "integer", + "type": "number", "description": "Rating from logged in user. Calculated at API-time.", - "format": "int32" + "format": "float" + }, + "hasUserRated": { + "type": "boolean", + "description": "If the user has set the rating or not" }, "format": { "enum": [ @@ -16858,8 +17120,8 @@ "format": "int32" }, "userRating": { - "type": "integer", - "format": "int32" + "type": "number", + "format": "float" } }, "additionalProperties": false