From 28688ada8e7f04cb401e88c6a3dd6c1ac925ccfa Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 15 Dec 2021 10:23:10 -0600 Subject: [PATCH] In-Depth Filtering (#850) * Laying the foundation for the filter rework * Filtering by Genre is now possible. * Cleaned up code and preparing for People filtering * People filtering is hooked up for the frontend * Filtering now works. On Deck does not work with filtering currently due to a unique implementation. * More cleanup * Implemented the ability to reset the filters * Added a mobile drawer for filtering * Added some additional cases for NaturalSortComparer. Filter now uses a drawer on smaller screens. * Fixed a bug where backup service was not pointing to the correct directory. * Undid the fix, it's working as expected --- API.Tests/API.Tests.csproj | 4 +- .../Comparers/NaturalSortComparerTest.cs | 16 + API.Tests/Parser/MangaParserTests.cs | 1 + API/API.csproj | 20 +- API/Comparators/NaturalSortComparer.cs | 4 + API/Controllers/MetadataController.cs | 31 + API/Controllers/SeriesController.cs | 2 +- API/DTOs/Filtering/FilterDto.cs | 61 + API/DTOs/Filtering/ReadStatus.cs | 13 + API/DTOs/PersonDto.cs | 3 +- .../20211214000230_SeriesIncludes.Designer.cs | 1228 +++++++++++++++++ .../20211214000230_SeriesIncludes.cs | 57 + .../Migrations/DataContextModelSnapshot.cs | 20 + API/Data/Repositories/GenreRepository.cs | 14 +- API/Data/Repositories/SeriesRepository.cs | 178 ++- API/Entities/Series.cs | 2 + API/Helpers/SQLHelper.cs | 1 + API/Parser/Parser.cs | 2 +- API/Services/MetadataService.cs | 4 +- .../Tasks/Scanner/ParseScannedFiles.cs | 2 - Kavita.Common/Kavita.Common.csproj | 2 +- UI/Web/src/app/_models/person.ts | 1 + UI/Web/src/app/_models/series-filter.ts | 37 +- UI/Web/src/app/_services/metadata.service.ts | 10 + UI/Web/src/app/_services/series.service.ts | 30 +- UI/Web/src/app/app.component.ts | 8 +- .../card-detail-layout.component.html | 264 +++- .../card-detail-layout.component.scss | 9 + .../card-detail-layout.component.ts | 379 ++++- UI/Web/src/app/cards/cards.module.ts | 3 +- .../chapter-metadata-detail.component.ts | 1 + .../collection-detail.component.html | 1 - .../collection-detail.component.ts | 13 +- .../library-detail.component.html | 1 - .../library-detail.component.ts | 12 +- UI/Web/src/app/on-deck/on-deck.component.html | 3 +- UI/Web/src/app/on-deck/on-deck.component.ts | 15 +- .../recently-added.component.html | 3 +- .../recently-added.component.ts | 13 +- .../series-detail.component.html | 13 +- .../series-detail/series-detail.component.ts | 10 +- .../series-metadata-detail.component.html | 4 + .../series-metadata-detail.component.ts | 9 + .../app/shared/_services/utility.service.ts | 6 + .../app/typeahead/typeahead.component.html | 3 +- .../src/app/typeahead/typeahead.component.ts | 14 +- UI/Web/src/theme/_colors.scss | 14 + 47 files changed, 2354 insertions(+), 187 deletions(-) create mode 100644 API/Controllers/MetadataController.cs create mode 100644 API/DTOs/Filtering/ReadStatus.cs create mode 100644 API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs create mode 100644 API/Data/Migrations/20211214000230_SeriesIncludes.cs diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 794b6b1ec..9e0135612 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,10 +7,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API.Tests/Comparers/NaturalSortComparerTest.cs b/API.Tests/Comparers/NaturalSortComparerTest.cs index 5b06c0a2d..477ba867f 100644 --- a/API.Tests/Comparers/NaturalSortComparerTest.cs +++ b/API.Tests/Comparers/NaturalSortComparerTest.cs @@ -54,6 +54,18 @@ namespace API.Tests.Comparers new[] {"!001", "001", "002"}, new[] {"!001", "001", "002"} )] + [InlineData( + new[] {"001", "", null}, + new[] {"", "001", null} + )] + [InlineData( + new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"}, + new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"} + )] + [InlineData( + new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"}, + new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"} + )] public void TestNaturalSortComparer(string[] input, string[] expected) { Array.Sort(input, _nc); @@ -94,6 +106,10 @@ namespace API.Tests.Comparers new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"} )] + [InlineData( + new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg"}, + new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg"} + )] public void TestNaturalSortComparerLinq(string[] input, string[] expected) { var output = input.OrderBy(c => c, _nc); diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index d088901e5..1eb2a32d6 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -244,6 +244,7 @@ namespace API.Tests.Parser [InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")] [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")] [InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")] + [InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename)); diff --git a/API/API.csproj b/API/API.csproj index 88b786b79..05c3fa130 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -47,29 +47,29 @@ - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/API/Comparators/NaturalSortComparer.cs b/API/Comparators/NaturalSortComparer.cs index 24b0c20d2..650edb7ce 100644 --- a/API/Comparators/NaturalSortComparer.cs +++ b/API/Comparators/NaturalSortComparer.cs @@ -27,6 +27,10 @@ namespace API.Comparators { if (x == y) return 0; + if (x != null && y == null) return -1; + if (x == null) return 1; + + if (!_table.TryGetValue(x ?? Empty, out var x1)) { x1 = Regex.Split(x ?? Empty, "([0-9]+)"); diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs new file mode 100644 index 000000000..b0fcd9930 --- /dev/null +++ b/API/Controllers/MetadataController.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.DTOs.Metadata; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers; + + +public class MetadataController : BaseApiController +{ + private readonly IUnitOfWork _unitOfWork; + + public MetadataController(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + [HttpGet("genres")] + public async Task>> GetAllGenres() + { + return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync()); + } + + [HttpGet("people")] + public async Task>> GetAllPeople() + { + return Ok(await _unitOfWork.PersonRepository.GetAllPeople()); + } +} diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index d4abe7ba3..bf7de16e7 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -334,7 +334,7 @@ namespace API.Controllers var existingTag = allTags.SingleOrDefault(t => t.Title == tag.Title); if (existingTag != null) { - if (!series.Metadata.CollectionTags.Any(t => t.Title == tag.Title)) + if (series.Metadata.CollectionTags.All(t => t.Title != tag.Title)) { newTags.Add(existingTag); } diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index 6e52ef45c..a745b9f0e 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Collections.Generic; +using API.Data.Migrations; +using API.Entities; using API.Entities.Enums; namespace API.DTOs.Filtering @@ -11,5 +13,64 @@ namespace API.DTOs.Filtering /// public IList Formats { get; init; } = new List(); + /// + /// The progress you want to be returned. This can be bitwise manipulated. Defaults to all applicable states. + /// + public ReadStatus ReadStatus { get; init; } = new ReadStatus(); + + /// + /// A list of library ids to restrict search to. Defaults to all libraries by passing empty list + /// + public IList Libraries { get; init; } = new List(); + /// + /// A list of Genre ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Genres { get; init; } = new List(); + /// + /// A list of Writers to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Writers { get; init; } = new List(); + /// + /// A list of Penciller ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Penciller { get; init; } = new List(); + /// + /// A list of Inker ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Inker { get; init; } = new List(); + /// + /// A list of Colorist ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Colorist { get; init; } = new List(); + /// + /// A list of Letterer ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Letterer { get; init; } = new List(); + /// + /// A list of CoverArtist ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList CoverArtist { get; init; } = new List(); + /// + /// A list of Editor ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Editor { get; init; } = new List(); + /// + /// A list of Publisher ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Publisher { get; init; } = new List(); + /// + /// A list of Character ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList Character { get; init; } = new List(); + /// + /// A list of Collection Tag ids to restrict search to. Defaults to all genres by passing an empty list + /// + public IList CollectionTags { get; init; } = new List(); + /// + /// Will return back everything with the rating and above + /// + /// + public int Rating { get; init; } + } } diff --git a/API/DTOs/Filtering/ReadStatus.cs b/API/DTOs/Filtering/ReadStatus.cs new file mode 100644 index 000000000..af7bb2475 --- /dev/null +++ b/API/DTOs/Filtering/ReadStatus.cs @@ -0,0 +1,13 @@ +using System; + +namespace API.DTOs.Filtering; + +/// +/// Represents the Reading Status. This is a flag and allows multiple statues +/// +public class ReadStatus +{ + public bool NotRead { get; set; } = false; + public bool InProgress { get; set; } = false; + public bool Read { get; set; } = false; +} diff --git a/API/DTOs/PersonDto.cs b/API/DTOs/PersonDto.cs index 646817c1d..0ab7a4076 100644 --- a/API/DTOs/PersonDto.cs +++ b/API/DTOs/PersonDto.cs @@ -4,7 +4,8 @@ namespace API.DTOs { public class PersonDto { + public int Id { get; set; } public string Name { get; set; } public PersonRole Role { get; set; } } -} \ No newline at end of file +} diff --git a/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs b/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs new file mode 100644 index 000000000..64a21a956 --- /dev/null +++ b/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs @@ -0,0 +1,1228 @@ +// +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("20211214000230_SeriesIncludes")] + partial class SeriesIncludes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SiteDarkMode") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("GenreId") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GenreId"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format") + .IsUnique(); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("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("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany("Chapters") + .HasForeignKey("GenreId"); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Navigation("Chapters"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20211214000230_SeriesIncludes.cs b/API/Data/Migrations/20211214000230_SeriesIncludes.cs new file mode 100644 index 000000000..092d9001d --- /dev/null +++ b/API/Data/Migrations/20211214000230_SeriesIncludes.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class SeriesIncludes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_AppUserRating_SeriesId", + table: "AppUserRating", + column: "SeriesId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgresses_SeriesId", + table: "AppUserProgresses", + column: "SeriesId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgresses_Series_SeriesId", + table: "AppUserProgresses", + column: "SeriesId", + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserRating_Series_SeriesId", + table: "AppUserRating", + column: "SeriesId", + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgresses_Series_SeriesId", + table: "AppUserProgresses"); + + migrationBuilder.DropForeignKey( + name: "FK_AppUserRating_Series_SeriesId", + table: "AppUserRating"); + + migrationBuilder.DropIndex( + name: "IX_AppUserRating_SeriesId", + table: "AppUserRating"); + + migrationBuilder.DropIndex( + name: "IX_AppUserProgresses_SeriesId", + table: "AppUserProgresses"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 6abe85571..b20f13f6d 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -240,6 +240,8 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); + b.HasIndex("SeriesId"); + b.ToTable("AppUserProgresses"); }); @@ -265,6 +267,8 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); + b.HasIndex("SeriesId"); + b.ToTable("AppUserRating"); }); @@ -887,6 +891,12 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("AppUser"); }); @@ -898,6 +908,12 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("AppUser"); }); @@ -1193,6 +1209,10 @@ namespace API.Data.Migrations { b.Navigation("Metadata"); + b.Navigation("Progress"); + + b.Navigation("Ratings"); + b.Navigation("Volumes"); }); diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 271524994..e1d53934c 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.DTOs.Metadata; using API.Entities; using AutoMapper; +using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; @@ -12,7 +14,8 @@ public interface IGenreRepository void Attach(Genre genre); void Remove(Genre genre); Task FindByNameAsync(string genreName); - Task> GetAllGenres(); + Task> GetAllGenresAsync(); + Task> GetAllGenreDtosAsync(); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); } @@ -57,8 +60,15 @@ public class GenreRepository : IGenreRepository await _context.SaveChangesAsync(); } - public async Task> GetAllGenres() + public async Task> GetAllGenresAsync() { return await _context.Genre.ToListAsync(); } + + public async Task> GetAllGenreDtosAsync() + { + return await _context.Genre + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 8955635b0..0c55a7b49 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -177,17 +177,15 @@ public class SeriesRepository : ISeriesRepository public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter) { - var formats = filter.GetSqlFilter(); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter); - var userLibraries = await GetUserLibraries(libraryId, userId); - - var query = _context.Series - .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) - .OrderBy(s => s.SortName) + var retSeries = query + .OrderByDescending(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider) + .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); } private async Task> GetUserLibraries(int libraryId, int userId) @@ -247,7 +245,7 @@ public class SeriesRepository : ISeriesRepository public async Task DeleteSeriesAsync(int seriesId) { var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync(); - _context.Series.Remove(series); + if (series != null) _context.Series.Remove(series); return await _context.SaveChangesAsync() > 0; } @@ -376,18 +374,84 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter) { - var formats = filter.GetSqlFilter(); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter); - var userLibraries = await GetUserLibraries(libraryId, userId); - - var query = _context.Series - .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) + var retSeries = query .OrderByDescending(s => s.Created) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + } + + private IList ExtractFilters(int libraryId, int userId, FilterDto filter, ref List userLibraries, + out List allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter, + out bool hasRatingFilter, out bool hasProgressFilter, out IList seriesIds) + { + var formats = filter.GetSqlFilter(); + + if (filter.Libraries.Count > 0) + { + userLibraries = userLibraries.Where(l => filter.Libraries.Contains(l)).ToList(); + } + + allPeopleIds = new List(); + allPeopleIds.AddRange(filter.Writers); + allPeopleIds.AddRange(filter.Character); + allPeopleIds.AddRange(filter.Colorist); + allPeopleIds.AddRange(filter.Editor); + allPeopleIds.AddRange(filter.Inker); + allPeopleIds.AddRange(filter.Letterer); + allPeopleIds.AddRange(filter.Penciller); + allPeopleIds.AddRange(filter.Publisher); + allPeopleIds.AddRange(filter.CoverArtist); + + hasPeopleFilter = allPeopleIds.Count > 0; + hasGenresFilter = filter.Genres.Count > 0; + hasCollectionTagFilter = filter.CollectionTags.Count > 0; + hasRatingFilter = filter.Rating > 0; + hasProgressFilter = !filter.ReadStatus.Read || !filter.ReadStatus.InProgress || !filter.ReadStatus.NotRead; + + + bool ProgressComparison(int pagesRead, int totalPages) + { + var result = false; + if (filter.ReadStatus.NotRead) + { + result = (pagesRead == 0); + } + + if (filter.ReadStatus.Read) + { + result = result || (pagesRead == totalPages); + } + + if (filter.ReadStatus.InProgress) + { + result = result || (pagesRead > 0 && pagesRead < totalPages); + } + + return result; + } + + seriesIds = new List(); + if (hasProgressFilter) + { + seriesIds = _context.Series + .Include(s => s.Progress) + .Select(s => new + { + Series = s, + PagesRead = s.Progress.Where(p => p.AppUserId == userId).Sum(p => p.PagesRead), + }) + .ToList() + .Where(s => ProgressComparison(s.PagesRead, s.Series.Pages)) + .Select(s => s.Series.Id) + .ToList(); + } + + return formats; } /// @@ -401,24 +465,23 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter) { - var formats = filter.GetSqlFilter(); + var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter)) + .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => + new + { + Series = s, + PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) + .Sum(s1 => s1.PagesRead), + progress.AppUserId, + LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId) + .Max(p => p.LastModified) + }); - var userLibraries = await GetUserLibraries(libraryId, userId); - var series = _context.Series - .Where(s => formats.Contains(s.Format) && userLibraries.Contains(s.LibraryId)) - .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new - { - Series = s, - PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId).Sum(s1 => s1.PagesRead), - progress.AppUserId, - LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified) - }) - .AsNoTracking(); - var retSeries = series.Where(s => s.AppUserId == userId - && s.PagesRead > 0 - && s.PagesRead < s.Series.Pages) + var retSeries = query.Where(s => s.AppUserId == userId + && s.PagesRead > 0 + && s.PagesRead < s.Series.Pages) .OrderByDescending(s => s.LastModified) .ThenByDescending(s => s.Series.LastModified) .Select(s => s.Series) @@ -430,6 +493,63 @@ public class SeriesRepository : ISeriesRepository return await retSeries.ToListAsync(); } + private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter) + { + var userLibraries = await GetUserLibraries(libraryId, userId); + var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries, + out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter, + out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter, + out var seriesIds); + + var query = _context.Series + .Where(s => userLibraries.Contains(s.LibraryId) + && formats.Contains(s.Format) + && (!hasGenresFilter || s.Metadata.Genres.Any(g => filter.Genres.Contains(g.Id))) + && (!hasPeopleFilter || s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id))) + && (!hasCollectionTagFilter || + s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id))) + && (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating)) + && (!hasProgressFilter || seriesIds.Contains(s.Id)) + ) + .AsNoTracking(); + // IQueryable newFilter = null; + // if (hasProgressFilter) + // { + // newFilter = query + // .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => + // new + // { + // Series = s, + // PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) + // .Sum(s1 => s1.PagesRead), + // progress.AppUserId, + // LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId) + // .Max(p => p.LastModified) + // }) + // .Select(d => new FilterableQuery() + // { + // Series = d.Series, + // AppUserId = d.AppUserId, + // LastModified = d.LastModified, + // PagesRead = d.PagesRead + // }) + // .Where(d => seriesIds.Contains(d.Series.Id)); + // } + // else + // { + // newFilter = query.Select(s => new FilterableQuery() + // { + // Series = s, + // LastModified = DateTime.Now, // TODO: Figure this out + // AppUserId = userId, + // PagesRead = 0 + // }); + // } + + + return query; + } + public async Task GetSeriesMetadata(int seriesId) { var metadataDto = await _context.SeriesMetadata diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index ed2fd64e2..a532028bb 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -53,6 +53,8 @@ namespace API.Entities public MangaFormat Format { get; set; } = MangaFormat.Unknown; public SeriesMetadata Metadata { get; set; } + public ICollection Ratings { get; set; } = new List(); + public ICollection Progress { get; set; } = new List(); // Relationships public List Volumes { get; set; } diff --git a/API/Helpers/SQLHelper.cs b/API/Helpers/SQLHelper.cs index fcd44e7da..d06d246ef 100644 --- a/API/Helpers/SQLHelper.cs +++ b/API/Helpers/SQLHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using API.DTOs; using Microsoft.EntityFrameworkCore; namespace API.Helpers diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 5ebac08cf..fb50cb880 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -412,7 +412,7 @@ namespace API.Parser MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz new Regex( - @"^(?!Vol)(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", + @"^(?!Vol)(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index eff48bb72..87e90bd36 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -369,7 +369,7 @@ public class MetadataService : IMetadataService _logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); var allPeople = await _unitOfWork.PersonRepository.GetAllPeople(); - var allGenres = await _unitOfWork.GenreRepository.GetAllGenres(); + var allGenres = await _unitOfWork.GenreRepository.GetAllGenresAsync(); var seriesIndex = 0; @@ -489,7 +489,7 @@ public class MetadataService : IMetadataService MessageFactory.RefreshMetadataProgressEvent(libraryId, 0F)); var allPeople = await _unitOfWork.PersonRepository.GetAllPeople(); - var allGenres = await _unitOfWork.GenreRepository.GetAllGenres(); + var allGenres = await _unitOfWork.GenreRepository.GetAllGenresAsync(); ProcessSeriesMetadataUpdate(series, allPeople, allGenres, forceUpdate); diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 50f966db6..c4f4659e8 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -128,8 +128,6 @@ namespace API.Services.Tasks.Scanner { info.Chapters = info.ComicInfo.Number; } - - _logger.LogDebug("ComicInfo read added {Time} ms to processing", sw.ElapsedMilliseconds); } TrackSeries(info); diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 7e20e9d3d..abf131604 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/_models/person.ts b/UI/Web/src/app/_models/person.ts index c0ecc7c4e..88b0848fa 100644 --- a/UI/Web/src/app/_models/person.ts +++ b/UI/Web/src/app/_models/person.ts @@ -13,6 +13,7 @@ export enum PersonRole { } export interface Person { + id: number; name: string; role: PersonRole; } \ No newline at end of file diff --git a/UI/Web/src/app/_models/series-filter.ts b/UI/Web/src/app/_models/series-filter.ts index f4eef7b1c..39cd48ab0 100644 --- a/UI/Web/src/app/_models/series-filter.ts +++ b/UI/Web/src/app/_models/series-filter.ts @@ -1,38 +1,53 @@ import { MangaFormat } from "./manga-format"; -export interface FilterItem { +export interface FilterItem { title: string; - value: any; + value: T; selected: boolean; } export interface SeriesFilter { formats: Array; + libraries: Array, + readStatus: ReadStatus; + genres: Array; + writers: Array; + penciller: Array; + inker: Array; + colorist: Array; + letterer: Array; + coverArtist: Array; + editor: Array; + publisher: Array; + character: Array; + collectionTags: Array; + rating: number; +} + +export interface ReadStatus { + notRead: boolean, + inProgress: boolean, + read: boolean, } export const mangaFormatFilters = [ { - title: 'Format: All', - value: null, - selected: false - }, - { - title: 'Format: Images', + title: 'Images', value: MangaFormat.IMAGE, selected: false }, { - title: 'Format: EPUB', + title: 'EPUB', value: MangaFormat.EPUB, selected: false }, { - title: 'Format: PDF', + title: 'PDF', value: MangaFormat.PDF, selected: false }, { - title: 'Format: ARCHIVE', + title: 'ARCHIVE', value: MangaFormat.ARCHIVE, selected: false } diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index e21115671..28d1e3d18 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -4,7 +4,9 @@ import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { ChapterMetadata } from '../_models/chapter-metadata'; +import { Genre } from '../_models/genre'; import { AgeRating } from '../_models/metadata/age-rating'; +import { Person } from '../_models/person'; @Injectable({ providedIn: 'root' @@ -34,4 +36,12 @@ export class MetadataService { return this.ageRatingTypes[ageRating]; })); } + + getAllGenres() { + return this.httpClient.get(this.baseUrl + 'metadata/genres'); + } + + getAllPeople() { + return this.httpClient.get(this.baseUrl + 'metadata/people'); + } } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 5fd621928..b0b779478 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -8,7 +8,7 @@ import { CollectionTag } from '../_models/collection-tag'; import { InProgressChapter } from '../_models/in-progress-chapter'; import { PaginatedResult } from '../_models/pagination'; import { Series } from '../_models/series'; -import { SeriesFilter } from '../_models/series-filter'; +import { ReadStatus, SeriesFilter } from '../_models/series-filter'; import { SeriesMetadata } from '../_models/series-metadata'; import { Volume } from '../_models/volume'; import { ImageService } from './image.service'; @@ -177,15 +177,29 @@ export class SeriesService { createSeriesFilter(filter?: SeriesFilter) { const data: SeriesFilter = { - formats: [] + formats: [], + libraries: [], + genres: [], + writers: [], + penciller: [], + inker: [], + colorist: [], + letterer: [], + coverArtist: [], + editor: [], + publisher: [], + character: [], + collectionTags: [], + rating: 0, + readStatus: { + read: true, + inProgress: true, + notRead: true + } }; - if (filter) { - if (filter.formats != null) { - data.formats = filter.formats; - } - } + if (filter === undefined) return data; - return data; + return filter; } } diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index d1e778d3c..c4a8cd863 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -6,7 +6,7 @@ import { LibraryService } from './_services/library.service'; import { MessageHubService } from './_services/message-hub.service'; import { NavService } from './_services/nav.service'; import { filter } from 'rxjs/operators'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-root', @@ -17,7 +17,11 @@ export class AppComponent implements OnInit { constructor(private accountService: AccountService, public navService: NavService, private messageHub: MessageHubService, private libraryService: LibraryService, - private router: Router, private ngbModal: NgbModal) { + private router: Router, private ngbModal: NgbModal, private ratingConfig: NgbRatingConfig) { + + // Setup default rating config + ratingConfig.max = 5; + ratingConfig.resettable = true; // Close any open modals when a route change occurs router.events diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index 7f85064e3..0fa06722c 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -10,24 +10,264 @@ - -
-
-
-
- - -
-
+
+
+
-
+ +
+ +
+

Book Settings + +

+ +
+
+ +
+
+
+ + +
+
+
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+ +
+
+ + + + {{item.title}} + + + {{item.title}} + + +
+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + + + + +
+
+ +
+ + +
+ +
+
+ +
+
+
+ diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss index e69de29bb..23d8c738a 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss @@ -0,0 +1,9 @@ +@use '../../../theme/colors'; + +.star { + font-size: 1.5rem; + color: colors.$rating-empty; +} +.filled { + color: colors.$rating-filled; +} \ No newline at end of file diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index aab49eff7..ad2274997 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -1,39 +1,42 @@ -import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'; -import { FormGroup, FormControl } from '@angular/forms'; +import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { of, ReplaySubject, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { UtilityService } from 'src/app/shared/_services/utility.service'; +import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings'; +import { CollectionTag } from 'src/app/_models/collection-tag'; +import { Genre } from 'src/app/_models/genre'; +import { Library } from 'src/app/_models/library'; +import { MangaFormat } from 'src/app/_models/manga-format'; import { Pagination } from 'src/app/_models/pagination'; -import { FilterItem } from 'src/app/_models/series-filter'; +import { Person, PersonRole } from 'src/app/_models/person'; +import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter'; import { ActionItem } from 'src/app/_services/action-factory.service'; +import { CollectionTagService } from 'src/app/_services/collection-tag.service'; +import { LibraryService } from 'src/app/_services/library.service'; +import { MetadataService } from 'src/app/_services/metadata.service'; +import { SeriesService } from 'src/app/_services/series.service'; const FILTER_PAG_REGEX = /[^0-9]/g; -export enum FilterAction { - /** - * If an option is selected on a multi select component - */ - Added = 0, - /** - * If an option is unselected on a multi select component - */ - Removed = 1, - /** - * If an option is selected on a single select component - */ - Selected = 2 -} - -export interface UpdateFilterEvent { - filterItem: FilterItem; - action: FilterAction; -} - const ANIMATION_SPEED = 300; +export class FilterSettings { + libraryDisabled = false; + formatDisabled = false; + collectionDisabled = false; + genresDisabled = false; + peopleDisabled = false; + readProgressDisabled = false; + ratingDisabled = false; +} + @Component({ selector: 'app-card-detail-layout', templateUrl: './card-detail-layout.component.html', styleUrls: ['./card-detail-layout.component.scss'] }) -export class CardDetailLayoutComponent implements OnInit { +export class CardDetailLayoutComponent implements OnInit, OnDestroy { @Input() header: string = ''; @Input() isLoading: boolean = false; @@ -43,32 +46,253 @@ export class CardDetailLayoutComponent implements OnInit { * Any actions to exist on the header for the parent collection (library, collection) */ @Input() actions: ActionItem[] = []; - /** - * A list of Filters which can filter the data of the page. If nothing is passed, the control will not show. - */ - @Input() filters: Array = []; @Input() trackByIdentity!: (index: number, item: any) => string; + @Input() filterSettings!: FilterSettings; @Output() itemClicked: EventEmitter = new EventEmitter(); @Output() pageChange: EventEmitter = new EventEmitter(); - @Output() applyFilter: EventEmitter = new EventEmitter(); + @Output() applyFilter: EventEmitter = new EventEmitter(); @ContentChild('cardItem') itemTemplate!: TemplateRef; - filterForm: FormGroup = new FormGroup({ - filter: new FormControl(0, []), - }); + + formatSettings: TypeaheadSettings> = new TypeaheadSettings(); + librarySettings: TypeaheadSettings> = new TypeaheadSettings(); + genreSettings: TypeaheadSettings> = new TypeaheadSettings(); + collectionSettings: TypeaheadSettings> = new TypeaheadSettings(); + peopleSettings: {[PersonRole: string]: TypeaheadSettings>} = {}; + resetTypeaheads: Subject = new ReplaySubject(1); /** * Controls the visiblity of extended controls that sit below the main header. */ filteringCollapsed: boolean = true; - constructor() { } + filter!: SeriesFilter; + libraries: Array> = []; + genres: Array> = []; + persons: Array> = []; + collectionTags: Array> = []; + + readProgressGroup!: FormGroup; + + updateApplied: number = 0; + + private onDestory: Subject = new Subject(); + + get PersonRole(): typeof PersonRole { + return PersonRole; + } + + constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService, + private utilityService: UtilityService, private collectionTagService: CollectionTagService) { + this.filter = this.seriesService.createSeriesFilter(); + this.readProgressGroup = new FormGroup({ + read: new FormControl(this.filter.readStatus.read, []), + notRead: new FormControl(this.filter.readStatus.notRead, []), + inProgress: new FormControl(this.filter.readStatus.inProgress, []), + }); + + this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => { + this.filter.readStatus.read = this.readProgressGroup.get('read')?.value; + this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value; + this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value; + }); + } ngOnInit(): void { - this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.pagination?.currentPage}_${this.filterForm.get('filter')?.value}_${item.id}_${index}`; + this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.pagination?.currentPage}_${this.updateApplied}`; + this.setupFormatTypeahead(); + + if (this.filterSettings === undefined) { + this.filterSettings = new FilterSettings(); + } + + + this.metadataService.getAllGenres().subscribe(genres => { + this.genres = genres.map(genre => { + return { + title: genre.title, + value: genre, + selected: false, + } + }); + this.setupGenreTypeahead(); + + }); + + this.libraryService.getLibrariesForMember().subscribe(libs => { + this.libraries = libs.map(lib => { + return { + title: lib.name, + value: lib, + selected: true, + } + }); + this.setupLibraryTypeahead(); + }); + + this.metadataService.getAllPeople().subscribe(res => { + this.persons = res.map(lib => { + return { + title: lib.name, + value: lib, + selected: true, + } + }); + this.setupPersonTypeahead(); + }); + + this.collectionTagService.allTags().subscribe(tags => { + this.collectionTags = tags.map(lib => { + return { + title: lib.title, + value: lib, + selected: false, + } + }); + this.setupCollectionTagTypeahead(); + }); } + ngOnDestroy() { + this.onDestory.next(); + this.onDestory.complete(); + } + + + setupFormatTypeahead() { + this.formatSettings.minCharacters = 0; + this.formatSettings.multiple = true; + this.formatSettings.id = 'format'; + this.formatSettings.unique = true; + this.formatSettings.addIfNonExisting = false; + this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters); + this.formatSettings.compareFn = (options: FilterItem[], filter: string) => { + const f = filter.toLowerCase(); + return options.filter(m => m.title.toLowerCase() === f); + } + this.formatSettings.savedData = mangaFormatFilters; + } + + setupLibraryTypeahead() { + this.librarySettings.minCharacters = 0; + this.librarySettings.multiple = true; + this.librarySettings.id = 'libraries'; + this.librarySettings.unique = true; + this.librarySettings.addIfNonExisting = false; + this.librarySettings.fetchFn = (filter: string) => { + return of (this.libraries) + }; + this.librarySettings.compareFn = (options: FilterItem[], filter: string) => { + const f = filter.toLowerCase(); + return options.filter(m => m.title.toLowerCase() === f); + } + } + + setupGenreTypeahead() { + this.genreSettings.minCharacters = 0; + this.genreSettings.multiple = true; + this.genreSettings.id = 'genres'; + this.genreSettings.unique = true; + this.genreSettings.addIfNonExisting = false; + this.genreSettings.fetchFn = (filter: string) => { + return of (this.genres) + }; + this.genreSettings.compareFn = (options: FilterItem[], filter: string) => { + const f = filter.toLowerCase(); + return options.filter(m => m.title.toLowerCase() === f); + } + } + + setupCollectionTagTypeahead() { + this.collectionSettings.minCharacters = 0; + this.collectionSettings.multiple = true; + this.collectionSettings.id = 'collections'; + this.collectionSettings.unique = true; + this.collectionSettings.addIfNonExisting = false; + this.collectionSettings.fetchFn = (filter: string) => { + return of (this.collectionTags) + }; + this.collectionSettings.compareFn = (options: FilterItem[], filter: string) => { + const f = filter.toLowerCase(); + return options.filter(m => m.title.toLowerCase() === f); + } + } + + setupPersonTypeahead() { + this.peopleSettings = {}; + + var personSettings = this.createBlankPersonSettings('writers'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Writer && this.utilityService.filter(p.value.name, filter))); + }; + this.peopleSettings[PersonRole.Writer] = personSettings; + + personSettings = this.createBlankPersonSettings('character'); + personSettings.fetchFn = (filter: string) => { + + return of (this.persons.filter(p => p.value.role == PersonRole.Character && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Character] = personSettings; + + personSettings = this.createBlankPersonSettings('colorist'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Colorist && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Colorist] = personSettings; + + personSettings = this.createBlankPersonSettings('cover-artist'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.CoverArtist && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.CoverArtist] = personSettings; + + personSettings = this.createBlankPersonSettings('editor'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Editor && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Editor] = personSettings; + + personSettings = this.createBlankPersonSettings('inker'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Inker && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Inker] = personSettings; + + personSettings = this.createBlankPersonSettings('letterer'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Letterer && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Letterer] = personSettings; + + personSettings = this.createBlankPersonSettings('penciller'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Penciller && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Penciller] = personSettings; + + personSettings = this.createBlankPersonSettings('publisher'); + personSettings.fetchFn = (filter: string) => { + return of (this.persons.filter(p => p.value.role == PersonRole.Publisher && this.utilityService.filter(p.title, filter))) + }; + this.peopleSettings[PersonRole.Publisher] = personSettings; + } + + createBlankPersonSettings(id: string) { + var personSettings = new TypeaheadSettings>(); + personSettings.minCharacters = 0; + personSettings.multiple = true; + personSettings.unique = true; + personSettings.addIfNonExisting = false; + personSettings.id = id; + personSettings.compareFn = (options: FilterItem[], filter: string) => { + const f = filter.toLowerCase(); + return options.filter(m => m.title.toLowerCase() === f); + } + return personSettings; + } + + onPageChange(page: number) { this.pageChange.emit(this.pagination); } @@ -88,11 +312,88 @@ export class CardDetailLayoutComponent implements OnInit { } } - handleFilterChange(index: string) { - this.applyFilter.emit({ - filterItem: this.filters[parseInt(index, 10)], - action: FilterAction.Selected - }); + + updateFormatFilters(formats: FilterItem[]) { + this.filter.formats = formats.map(item => item.value) || []; + } + + updateLibraryFilters(libraries: FilterItem[]) { + this.filter.libraries = libraries.map(item => item.value.id) || []; + } + + updateGenreFilters(genres: FilterItem[]) { + this.filter.genres = genres.map(item => item.value.id) || []; + } + + updatePersonFilters(persons: FilterItem[], role: PersonRole) { + switch (role) { + case PersonRole.CoverArtist: + this.filter.coverArtist = persons.map(p => p.value.id); + break; + case PersonRole.Character: + this.filter.character = persons.map(p => p.value.id); + break; + case PersonRole.Colorist: + this.filter.colorist = persons.map(p => p.value.id); + break; + // case PersonRole.Artist: + // this.filter.artist = persons.map(p => p.value.id); + // break; + case PersonRole.Editor: + this.filter.editor = persons.map(p => p.value.id); + break; + case PersonRole.Inker: + this.filter.inker = persons.map(p => p.value.id); + break; + case PersonRole.Letterer: + this.filter.letterer = persons.map(p => p.value.id); + break; + case PersonRole.Penciller: + this.filter.penciller = persons.map(p => p.value.id); + break; + case PersonRole.Publisher: + this.filter.publisher = persons.map(p => p.value.id); + break; + case PersonRole.Writer: + this.filter.writers = persons.map(p => p.value.id); + break; + + } + } + + updateCollectionFilters(tags: FilterItem[]) { + this.filter.collectionTags = tags.map(item => item.value.id) || []; + } + + updateRating(rating: any) { + this.filter.rating = rating; + } + + updateReadStatus(status: string) { + console.log('readstatus: ', this.filter.readStatus); + if (status === 'read') { + this.filter.readStatus.read = !this.filter.readStatus.read; + } else if (status === 'inProgress') { + this.filter.readStatus.inProgress = !this.filter.readStatus.inProgress; + } else if (status === 'notRead') { + this.filter.readStatus.notRead = !this.filter.readStatus.notRead; + } + } + + getPersonsSettings(role: PersonRole) { + return this.peopleSettings[role]; + } + + clear() { + this.filter = this.seriesService.createSeriesFilter(); + this.resetTypeaheads.next(true); + this.applyFilter.emit(this.filter); + this.updateApplied++; + } + + apply() { + this.applyFilter.emit(this.filter); + this.updateApplied++; } } diff --git a/UI/Web/src/app/cards/cards.module.ts b/UI/Web/src/app/cards/cards.module.ts index 958bcd8d4..bd053352f 100644 --- a/UI/Web/src/app/cards/cards.module.ts +++ b/UI/Web/src/app/cards/cards.module.ts @@ -8,7 +8,7 @@ import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit import { ChangeCoverImageModalComponent } from './_modals/change-cover-image/change-cover-image-modal.component'; import { BookmarksModalComponent } from './_modals/bookmarks-modal/bookmarks-modal.component'; import { LazyLoadImageModule } from 'ng-lazyload-image'; -import { NgbTooltipModule, NgbCollapseModule, NgbPaginationModule, NgbDropdownModule, NgbProgressbarModule, NgbNavModule, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbTooltipModule, NgbCollapseModule, NgbPaginationModule, NgbDropdownModule, NgbProgressbarModule, NgbNavModule, NgbAccordionModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgxFileDropModule } from 'ngx-file-drop'; @@ -56,6 +56,7 @@ import { FileInfoComponent } from './file-info/file-info.component'; NgbNavModule, NgbTooltipModule, // Card item NgbCollapseModule, + NgbRatingModule, NgbNavModule, //Series Detail LazyLoadImageModule, diff --git a/UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.ts b/UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.ts index 7e4a711ca..2c203aed0 100644 --- a/UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.ts +++ b/UI/Web/src/app/cards/chapter-metadata-detail/chapter-metadata-detail.component.ts @@ -49,4 +49,5 @@ export class ChapterMetadataDetailComponent implements OnInit { // } } + } diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/collection-detail/collection-detail.component.html index 431680257..4957b8c62 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.html +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.html @@ -33,7 +33,6 @@ [items]="series" [pagination]="seriesPagination" (pageChange)="onPageChange($event)" - [filters]="filters" (applyFilter)="updateFilter($event)" > diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index 3799871c1..6a1831ee2 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -6,15 +6,13 @@ import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; -import { UpdateFilterEvent } from 'src/app/cards/card-detail-layout/card-detail-layout.component'; import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { CollectionTag } from 'src/app/_models/collection-tag'; import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event'; -import { SeriesRemovedEvent } from 'src/app/_models/events/series-removed-event'; import { Pagination } from 'src/app/_models/pagination'; import { Series } from 'src/app/_models/series'; -import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter'; +import { SeriesFilter } from 'src/app/_models/series-filter'; import { AccountService } from 'src/app/_services/account.service'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { ActionService } from 'src/app/_services/action.service'; @@ -39,10 +37,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { seriesPagination!: Pagination; collectionTagActions: ActionItem[] = []; isAdmin: boolean = false; - filters: Array = mangaFormatFilters; - filter: SeriesFilter = { - formats: [] - }; + filter: SeriesFilter | undefined = undefined; private onDestory: Subject = new Subject(); @@ -174,8 +169,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { }); } - updateFilter(data: UpdateFilterEvent) { - this.filter.formats = [data.filterItem.value]; + updateFilter(data: SeriesFilter) { + this.filter = data; if (this.seriesPagination !== undefined && this.seriesPagination !== null) { this.seriesPagination.currentPage = 1; this.onPageChange(this.seriesPagination); diff --git a/UI/Web/src/app/library-detail/library-detail.component.html b/UI/Web/src/app/library-detail/library-detail.component.html index d01cec915..65cc555dc 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.html +++ b/UI/Web/src/app/library-detail/library-detail.component.html @@ -4,7 +4,6 @@ [items]="series" [actions]="actions" [pagination]="pagination" - [filters]="filters" (applyFilter)="updateFilter($event)" (pageChange)="onPageChange($event)" > diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 374b7685e..cb356cfa1 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -4,13 +4,12 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs'; import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; -import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component'; import { KEY_CODES } from '../shared/_services/utility.service'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { Library } from '../_models/library'; import { Pagination } from '../_models/pagination'; import { Series } from '../_models/series'; -import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter'; +import { SeriesFilter } from '../_models/series-filter'; import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service'; import { ActionService } from '../_services/action.service'; import { LibraryService } from '../_services/library.service'; @@ -30,10 +29,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { loadingSeries = false; pagination!: Pagination; actions: ActionItem[] = []; - filters: Array = mangaFormatFilters; - filter: SeriesFilter = { - formats: [] - }; + filter: SeriesFilter | undefined = undefined; onDestroy: Subject = new Subject(); bulkActionCallback = (action: Action, data: any) => { @@ -134,8 +130,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { } } - updateFilter(data: UpdateFilterEvent) { - this.filter.formats = [data.filterItem.value]; + updateFilter(data: SeriesFilter) { + this.filter = data; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/on-deck/on-deck.component.html b/UI/Web/src/app/on-deck/on-deck.component.html index 2d8eded75..d03b4b696 100644 --- a/UI/Web/src/app/on-deck/on-deck.component.html +++ b/UI/Web/src/app/on-deck/on-deck.component.html @@ -2,11 +2,10 @@ diff --git a/UI/Web/src/app/on-deck/on-deck.component.ts b/UI/Web/src/app/on-deck/on-deck.component.ts index 7c8bb7fc2..7c661461b 100644 --- a/UI/Web/src/app/on-deck/on-deck.component.ts +++ b/UI/Web/src/app/on-deck/on-deck.component.ts @@ -3,11 +3,11 @@ import { Title } from '@angular/platform-browser'; import { Router, ActivatedRoute } from '@angular/router'; import { take } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; -import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component'; +import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component'; import { KEY_CODES } from '../shared/_services/utility.service'; import { Pagination } from '../_models/pagination'; import { Series } from '../_models/series'; -import { FilterItem, SeriesFilter, mangaFormatFilters } from '../_models/series-filter'; +import { SeriesFilter} from '../_models/series-filter'; import { Action } from '../_services/action-factory.service'; import { ActionService } from '../_services/action.service'; import { SeriesService } from '../_services/series.service'; @@ -23,10 +23,8 @@ export class OnDeckComponent implements OnInit { series: Series[] = []; pagination!: Pagination; libraryId!: number; - filters: Array = mangaFormatFilters; - filter: SeriesFilter = { - formats: [] - }; + filter: SeriesFilter | undefined = undefined; + filterSettings: FilterSettings = new FilterSettings(); constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title, private actionService: ActionService, public bulkSelectionService: BulkSelectionService) { @@ -35,6 +33,7 @@ export class OnDeckComponent implements OnInit { if (this.pagination === undefined || this.pagination === null) { this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1}; } + this.filterSettings.readProgressDisabled = true; this.loadPage(); } @@ -63,8 +62,8 @@ export class OnDeckComponent implements OnInit { this.loadPage(); } - updateFilter(data: UpdateFilterEvent) { - this.filter.formats = [data.filterItem.value]; + updateFilter(data: SeriesFilter) { + this.filter = data; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/recently-added/recently-added.component.html b/UI/Web/src/app/recently-added/recently-added.component.html index 93952f73e..27ef260d2 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.html +++ b/UI/Web/src/app/recently-added/recently-added.component.html @@ -3,8 +3,7 @@ [isLoading]="isLoading" [items]="series" [pagination]="pagination" -[filters]="filters" -(applyFilter)="updateFilter($event)" +(applyFilter)="applyFilter($event)" (pageChange)="onPageChange($event)" > diff --git a/UI/Web/src/app/recently-added/recently-added.component.ts b/UI/Web/src/app/recently-added/recently-added.component.ts index a517cbf30..b0e8fccd1 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.ts +++ b/UI/Web/src/app/recently-added/recently-added.component.ts @@ -4,12 +4,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs'; import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; -import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component'; import { KEY_CODES } from '../shared/_services/utility.service'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { Pagination } from '../_models/pagination'; import { Series } from '../_models/series'; -import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter'; +import { SeriesFilter } from '../_models/series-filter'; import { Action } from '../_services/action-factory.service'; import { ActionService } from '../_services/action.service'; import { MessageHubService } from '../_services/message-hub.service'; @@ -30,10 +29,7 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { pagination!: Pagination; libraryId!: number; - filters: Array = mangaFormatFilters; - filter: SeriesFilter = { - formats: [] - }; + filter: SeriesFilter | undefined = undefined; onDestroy: Subject = new Subject(); @@ -81,9 +77,8 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { this.loadPage(); } - updateFilter(data: UpdateFilterEvent) { - // TODO: Move this into card-layout component. It's the same except for callback - this.filter.formats = [data.filterItem.value]; + applyFilter(data: SeriesFilter) { + this.filter = data; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index 5784b3a9c..a5ec40dd4 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -49,22 +49,11 @@
+
-
- -
- -
diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 7e11541ee..e9a9bbd6a 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -63,7 +63,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { activeTabId = 2; hasNonSpecialVolumeChapters = true; - seriesSummary: string = ''; userReview: string = ''; libraryType: LibraryType = LibraryType.Manga; seriesMetadata: SeriesMetadata | null = null; @@ -148,7 +147,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { } constructor(private route: ActivatedRoute, private seriesService: SeriesService, - private ratingConfig: NgbRatingConfig, private router: Router, + private router: Router, public bulkSelectionService: BulkSelectionService, private modalService: NgbModal, public readerService: ReaderService, public utilityService: UtilityService, private toastr: ToastrService, private accountService: AccountService, public imageService: ImageService, @@ -156,8 +155,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { private confirmService: ConfirmService, private titleService: Title, private downloadService: DownloadService, private actionService: ActionService, public imageSerivce: ImageService, private messageHub: MessageHubService, - public bulkSelectionService: BulkSelectionService) { - ratingConfig.max = 5; + ) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { @@ -392,10 +390,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { } createHTML() { - if (this.seriesMetadata !== null) { - this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '
'); - } - this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '
'); } diff --git a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html index 3243a758f..e7863adc8 100644 --- a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html @@ -1,3 +1,7 @@ +
+ +
+
{{ageRatingName}} diff --git a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.ts index 7a3072fd2..ba1960a56 100644 --- a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.ts @@ -23,6 +23,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges { * String representation of AgeRating enum */ ageRatingName: string = ''; + /** + * Html representation of Series Summary + */ + seriesSummary: string = ''; get MangaFormat(): typeof MangaFormat { return MangaFormat; @@ -46,6 +50,11 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges { this.metadataService.getAgeRating(this.seriesMetadata.ageRating).subscribe(rating => { this.ageRatingName = rating; }); + + if (this.seriesMetadata !== null) { + this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '
'); + } + } ngOnInit(): void { diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 5bdaaedd0..e63d1b53c 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -88,6 +88,12 @@ export class UtilityService { return cleaned; } + filter(input: string, filter: string): boolean { + if (input === null || filter === null) return false; + const reg = /[_\.\-]/gi; + return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, '')); + } + mangaFormat(format: MangaFormat): string { switch (format) { case MangaFormat.EPUB: diff --git a/UI/Web/src/app/typeahead/typeahead.component.html b/UI/Web/src/app/typeahead/typeahead.component.html index 41b06ec5d..e14019981 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.html +++ b/UI/Web/src/app/typeahead/typeahead.component.html @@ -1,7 +1,5 @@
- -
@@ -13,6 +11,7 @@
Loading...
+