diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 34c41015c..78934be7d 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -169,6 +169,7 @@ namespace API.Tests.Parser [InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")] [InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")] [InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")] + [InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 39c7264d2..ea87456c0 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -99,12 +99,12 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all publication status /// [HttpGet("publication-status")] - public async Task>> GetAllPublicationStatus(string? libraryIds) + public ActionResult> GetAllPublicationStatus(string? libraryIds) { var ids = libraryIds?.Split(",").Select(int.Parse).ToList(); - if (ids != null && ids.Count > 0) + if (ids is {Count: > 0}) { - return Ok(await _unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); + return Ok(_unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); } return Ok(Enum.GetValues().Select(t => new PublicationStatusDto() diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 040d7f6b7..246caa04a 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -47,11 +47,11 @@ namespace API.Data.Metadata /// public float UserRating { get; set; } - public string AlternateSeries { get; set; } = string.Empty; public string StoryArc { get; set; } = string.Empty; public string SeriesGroup { get; set; } = string.Empty; - public string AlternativeSeries { get; set; } = string.Empty; - public string AlternativeNumber { get; set; } = string.Empty; + public string AlternateNumber { get; set; } = string.Empty; + public int AlternateCount { get; set; } = 0; + public string AlternateSeries { get; set; } = string.Empty; /// /// This is Epub only: calibre:title_sort diff --git a/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs b/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs new file mode 100644 index 000000000..dd2c6ce88 --- /dev/null +++ b/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs @@ -0,0 +1,1466 @@ +// +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("20220416211340_RemoveCustomIndex")] + partial class RemoveCustomIndex + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); + + 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("FileName") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .HasColumnType("TEXT"); + + 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("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("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("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.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.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("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .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("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("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("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.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("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + 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.HasKey("Id"); + + 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.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("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("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.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("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("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.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/20220416211340_RemoveCustomIndex.cs b/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs new file mode 100644 index 000000000..eb60f2349 --- /dev/null +++ b/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class RemoveCustomIndex : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format", + table: "Series"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format", + table: "Series", + columns: new[] { "Name", "NormalizedName", "LocalizedName", "LibraryId", "Format" }, + unique: true); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index c444976be..43b955788 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -744,9 +744,6 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); - b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format") - .IsUnique(); - b.ToTable("Series"); }); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 30163352f..fd9dcc52d 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -94,7 +94,7 @@ public interface ISeriesRepository Task> GetSeriesMetadataForIdsAsync(IEnumerable seriesIds); Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); Task> GetAllLanguagesForLibrariesAsync(List libraryIds); - Task> GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); + IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); Task> GetRecentlyUpdatedSeries(int userId, int pageSize = 30); } @@ -884,19 +884,19 @@ public class SeriesRepository : ISeriesRepository .ToList(); } - public async Task> GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) + public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) { - return await _context.Series + return _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.PublicationStatus) .Distinct() + .AsEnumerable() .Select(s => new PublicationStatusDto() { Value = s, Title = s.ToDescription() }) - .OrderBy(s => s.Title) - .ToListAsync(); + .OrderBy(s => s.Title); } diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index a6ae420b5..ef4b62eb4 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -3,78 +3,75 @@ using System.Collections.Generic; using API.Entities.Enums; using API.Entities.Interfaces; using API.Entities.Metadata; -using Microsoft.EntityFrameworkCore; -namespace API.Entities +namespace API.Entities; + +public class Series : IEntityDate { - [Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), nameof(LibraryId), nameof(Format), IsUnique = true)] - public class Series : IEntityDate - { - public int Id { get; set; } - /// - /// The UI visible Name of the Series. This may or may not be the same as the OriginalName - /// - public string Name { get; set; } - /// - /// Used internally for name matching. - /// - public string NormalizedName { get; set; } - /// - /// The name used to sort the Series. By default, will be the same as Name. - /// - public string SortName { get; set; } - /// - /// Name in original language (Japanese for Manga). By default, will be same as Name. - /// - public string LocalizedName { get; set; } - /// - /// Original Name on disk. Not exposed to UI. - /// - public string OriginalName { get; set; } - /// - /// Time of creation - /// - public DateTime Created { get; set; } - /// - /// Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc - /// - public DateTime LastModified { get; set; } - /// - /// Absolute path to the (managed) image file - /// - /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } - /// - /// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations. - /// - public bool CoverImageLocked { get; set; } - /// - /// Sum of all Volume page counts - /// - public int Pages { get; set; } + public int Id { get; set; } + /// + /// The UI visible Name of the Series. This may or may not be the same as the OriginalName + /// + public string Name { get; set; } + /// + /// Used internally for name matching. + /// + public string NormalizedName { get; set; } + /// + /// The name used to sort the Series. By default, will be the same as Name. + /// + public string SortName { get; set; } + /// + /// Name in original language (Japanese for Manga). By default, will be same as Name. + /// + public string LocalizedName { get; set; } + /// + /// Original Name on disk. Not exposed to UI. + /// + public string OriginalName { get; set; } + /// + /// Time of creation + /// + public DateTime Created { get; set; } + /// + /// Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc + /// + public DateTime LastModified { get; set; } + /// + /// Absolute path to the (managed) image file + /// + /// The file is managed internally to Kavita's APPDIR + public string CoverImage { get; set; } + /// + /// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations. + /// + public bool CoverImageLocked { get; set; } + /// + /// Sum of all Volume page counts + /// + public int Pages { get; set; } - /// - /// The type of all the files attached to this series - /// - public MangaFormat Format { get; set; } = MangaFormat.Unknown; + /// + /// The type of all the files attached to this series + /// + public MangaFormat Format { get; set; } = MangaFormat.Unknown; - public bool NameLocked { get; set; } - public bool SortNameLocked { get; set; } - public bool LocalizedNameLocked { get; set; } + public bool NameLocked { get; set; } + public bool SortNameLocked { get; set; } + public bool LocalizedNameLocked { get; set; } - /// - /// When a Chapter was last added onto the Series - /// - public DateTime LastChapterAdded { get; set; } + /// + /// When a Chapter was last added onto the Series + /// + public DateTime LastChapterAdded { get; set; } - public SeriesMetadata Metadata { get; set; } - public ICollection Ratings { get; set; } = new List(); - public ICollection Progress { get; set; } = new List(); + 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; } - public Library Library { get; set; } - public int LibraryId { get; set; } + // Relationships + public List Volumes { get; set; } + public Library Library { get; set; } + public int LibraryId { get; set; } - } } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 63db72658..bc4fb2f8a 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -132,9 +132,9 @@ namespace API.Parser new Regex( @"(?.*)(?:, Chapter )(?\d+)", MatchOptions, RegexTimeout), - // Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz + // Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz, My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras new Regex( - @"(?.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?\d+)", + @"(?.+?)(\s|_|-)(?!Vol)(\s|_|-)((?:Chapter)|(?:Ch\.))(\s|_|-)(?\d+)", MatchOptions, RegexTimeout), // [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz new Regex( diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index a43469c5e..a5b7e35b3 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -3730,9 +3730,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 141481a05..30c0f5971 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -195,6 +195,7 @@ export class SeriesService { } createSeriesFilter(filter?: SeriesFilter) { + if (filter !== undefined) return filter; const data: SeriesFilter = { formats: [], libraries: [], @@ -225,8 +226,6 @@ export class SeriesService { seriesNameQuery: '', }; - if (filter === undefined) return data; - - return filter; + return data; } } diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index f71101a89..625f061f6 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -40,7 +40,7 @@
-

Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.

+

Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect. See wiki for what is collected.

diff --git a/UI/Web/src/app/all-series/all-series.component.ts b/UI/Web/src/app/all-series/all-series.component.ts index fe81e17da..16aa80f92 100644 --- a/UI/Web/src/app/all-series/all-series.component.ts +++ b/UI/Web/src/app/all-series/all-series.component.ts @@ -31,6 +31,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy { onDestroy: Subject = new Subject(); filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); + filterActiveCheck!: SeriesFilter; filterActive: boolean = false; bulkActionCallback = (action: Action, data: any) => { @@ -79,8 +80,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - All Series'); - this.pagination = this.filterUtilityService.pagination(); - [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(); + this.pagination = this.filterUtilityService.pagination(this.route.snapshot); + [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); + this.filterActiveCheck = this.seriesService.createSeriesFilter(); } ngOnInit(): void { @@ -117,12 +119,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy { } loadPage() { - // The filter is out of sync with the presets from typeaheads on first load but syncs afterwards - if (this.filter == undefined) { - this.filter = this.seriesService.createSeriesFilter(); - } - - this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; this.pagination = series.pagination; 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 e0947f18d..83ee8a4c9 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 @@ -15,7 +15,6 @@ import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-adde import { Pagination } from 'src/app/_models/pagination'; import { Series } from 'src/app/_models/series'; import { FilterEvent, 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'; import { CollectionTagService } from 'src/app/_services/collection-tag.service'; @@ -41,6 +40,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { summary: string = ''; actionInProgress: boolean = false; + filterActiveCheck!: SeriesFilter; filterActive: boolean = false; filterOpen: EventEmitter = new EventEmitter(); @@ -98,9 +98,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { } const tagId = parseInt(routeId, 10); - this.seriesPagination = this.filterUtilityService.pagination(); - [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(); + this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot); + [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); this.filterSettings.presets.collectionTags = [tagId]; + this.filterActiveCheck = this.seriesService.createSeriesFilter(); + this.filterActiveCheck.collectionTags = [tagId]; this.updateTag(tagId); } @@ -161,7 +163,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { } loadPage() { - this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; this.seriesPagination = series.pagination; 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 60b659b9f..b76243b01 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -37,6 +37,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); filterActive: boolean = false; + filterActiveCheck!: SeriesFilter; tabs: Array<{title: string, fragment: string}> = [ {title: 'Library', fragment: ''}, @@ -101,9 +102,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { }); this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); - this.pagination = this.filterUtilityService.pagination(); - [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(); - this.filterSettings.presets.libraries = [this.libraryId]; + this.pagination = this.filterUtilityService.pagination(this.route.snapshot); + [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); + if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId]; + // Setup filterActiveCheck to check filter against + this.filterActiveCheck = this.seriesService.createSeriesFilter(); + this.filterActiveCheck.libraries = [this.libraryId]; + + this.filterSettings.libraryDisabled = true; } ngOnInit(): void { @@ -153,6 +159,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { updateFilter(data: FilterEvent) { this.filter = data.filter; + if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter); this.loadPage(); } @@ -165,7 +172,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { } this.loadingSeries = true; - this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; this.pagination = series.pagination; diff --git a/UI/Web/src/app/library/library.component.ts b/UI/Web/src/app/library/library.component.ts index 66086f306..a1ec41864 100644 --- a/UI/Web/src/app/library/library.component.ts +++ b/UI/Web/src/app/library/library.component.ts @@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { ReplaySubject, Subject } from 'rxjs'; import { debounceTime, filter, take, takeUntil } from 'rxjs/operators'; +import { FilterQueryParam } from '../shared/_services/filter-utilities.service'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { SeriesRemovedEvent } from '../_models/events/series-removed-event'; import { Library } from '../_models/library'; @@ -153,19 +154,19 @@ export class LibraryComponent implements OnInit, OnDestroy { handleSectionClick(sectionTitle: string) { if (sectionTitle.toLowerCase() === 'recently updated series') { const params: any = {}; - params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc - params['page'] = 1; + params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc + params[FilterQueryParam.Page] = 1; this.router.navigate(['all-series'], {queryParams: params}); } else if (sectionTitle.toLowerCase() === 'on deck') { const params: any = {}; - params['readStatus'] = 'true,false,false'; - params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc - params['page'] = 1; + params[FilterQueryParam.ReadStatus] = 'true,false,false'; + params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc + params[FilterQueryParam.Page] = 1; this.router.navigate(['all-series'], {queryParams: params}); }else if (sectionTitle.toLowerCase() === 'newly added series') { const params: any = {}; - params['sortBy'] = SortField.Created + ',false'; // sort by created, desc - params['page'] = 1; + params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc + params[FilterQueryParam.Page] = 1; this.router.navigate(['all-series'], {queryParams: params}); } } diff --git a/UI/Web/src/app/metadata-filter/filter-settings.ts b/UI/Web/src/app/metadata-filter/filter-settings.ts index 219e0eab5..28f7340e1 100644 --- a/UI/Web/src/app/metadata-filter/filter-settings.ts +++ b/UI/Web/src/app/metadata-filter/filter-settings.ts @@ -13,9 +13,11 @@ export class FilterSettings { tagsDisabled = false; languageDisabled = false; publicationStatusDisabled = false; + searchNameDisabled = false; presets: SeriesFilter | undefined; /** * Should the filter section be open by default + * @deprecated This is deprecated UX pattern. New style is to show highlight on filter button. */ openByDefault = false; } \ No newline at end of file diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index ae5883522..04fecaa25 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -19,13 +19,13 @@ This is library agnostic -
+
-
+
  - + {{item.title}} @@ -36,10 +36,10 @@
-
+
- + {{item.name}} @@ -50,11 +50,11 @@
-
+
  - + {{item.title}} @@ -65,10 +65,10 @@
-
+
- + {{item.title}} @@ -79,10 +79,10 @@
-
+
- + {{item.title}} @@ -95,10 +95,11 @@
-
+
- + {{item.name}} @@ -109,10 +110,11 @@
-
+
- + {{item.name}} @@ -123,10 +125,11 @@
-
+
- + {{item.name}} @@ -137,10 +140,11 @@
-
+
- + {{item.name}} @@ -151,10 +155,11 @@
-
+
- + {{item.name}} @@ -165,10 +170,11 @@
-
+
- + {{item.name}} @@ -179,10 +185,11 @@
-
+
- + {{item.name}} @@ -193,10 +200,11 @@
-
+
- + {{item.name}} @@ -207,10 +215,11 @@
-
+
- + {{item.name}} @@ -221,10 +230,11 @@
-
+
- + {{item.name}} @@ -236,7 +246,7 @@
-
+
@@ -254,7 +264,7 @@
-
+
@@ -265,9 +275,9 @@
-
+
- + {{item.title}} @@ -277,9 +287,10 @@
-
+
- + {{item.title}} @@ -289,9 +300,10 @@
-
+
- + {{item.title}} @@ -313,11 +325,11 @@
-
+
-
-
diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index f0a6fd3bd..268d14944 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -53,7 +53,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { tagsSettings: TypeaheadSettings = new TypeaheadSettings(); languageSettings: TypeaheadSettings = new TypeaheadSettings(); peopleSettings: {[PersonRole: string]: TypeaheadSettings} = {}; - resetTypeaheads: Subject = new ReplaySubject(1); + resetTypeaheads: ReplaySubject = new ReplaySubject(1); /** * Controls the visiblity of extended controls that sit below the main header. @@ -71,6 +71,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { updateApplied: number = 0; + fullyLoaded: boolean = false; + private onDestory: Subject = new Subject(); @@ -84,20 +86,32 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService, private utilityService: UtilityService, private collectionTagService: CollectionTagService) { + } + + ngOnInit(): void { + if (this.filterSettings === undefined) { + this.filterSettings = new FilterSettings(); + } + + if (this.filterOpen) { + this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => { + this.filteringCollapsed = !openState; + }); + } 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, []), + read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []), + notRead: new FormControl({value: this.filter.readStatus.notRead, disabled: this.filterSettings.readProgressDisabled}, []), + inProgress: new FormControl({value: this.filter.readStatus.inProgress, disabled: this.filterSettings.readProgressDisabled}, []), }); this.sortGroup = new FormGroup({ - sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []), + sortField: new FormControl({value: this.filter.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []), }); this.seriesNameGroup = new FormGroup({ - seriesNameQuery: new FormControl(this.filter.seriesNameQuery || '', []) + seriesNameQuery: new FormControl({value: this.filter.seriesNameQuery || '', disabled: this.filterSettings.searchNameDisabled}, []) }); this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => { @@ -138,19 +152,21 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { .subscribe(changes => { this.filter.seriesNameQuery = changes; }); + + this.loadFromPresetsAndSetup(); } - ngOnInit(): void { - if (this.filterSettings === undefined) { - this.filterSettings = new FilterSettings(); - } + ngOnDestroy() { + this.onDestory.next(); + this.onDestory.complete(); + } - if (this.filterOpen) { - this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => { - this.filteringCollapsed = !openState; - }); - } + getPersonsSettings(role: PersonRole) { + return this.peopleSettings[role]; + } + loadFromPresetsAndSetup() { + this.fullyLoaded = false; if (this.filterSettings.presets) { this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read); this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead); @@ -174,21 +190,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { } } - - this.setupTypeaheads(); - } - - ngOnDestroy() { - this.onDestory.next(); - this.onDestory.complete(); - } - - getPersonsSettings(role: PersonRole) { - return this.peopleSettings[role]; - } - - setupTypeaheads() { - this.setupFormatTypeahead(); forkJoin([ @@ -201,7 +202,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.setupGenreTypeahead(), this.setupPersonTypeahead(), ]).subscribe(results => { - this.resetTypeaheads.next(true); + this.fullyLoaded = true; + this.resetTypeaheads.next(false); // Pass false to ensure we reset to the preset and not to an empty typeahead if (this.filterSettings.openByDefault) { this.filteringCollapsed = false; } @@ -226,8 +228,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.formats && this.filterSettings.presets?.formats.length > 0) { this.formatSettings.savedData = mangaFormatFilters.filter(item => this.filterSettings.presets?.formats.includes(item.value)); - this.filter.formats = this.formatSettings.savedData.map(item => item.value); - this.resetTypeaheads.next(true); + this.updateFormatFilters(this.formatSettings.savedData); } } @@ -251,7 +252,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.libraries && this.filterSettings.presets?.libraries.length > 0) { return this.librarySettings.fetchFn('').pipe(map(libraries => { this.librarySettings.savedData = libraries.filter(item => this.filterSettings.presets?.libraries.includes(item.id)); - this.filter.libraries = this.librarySettings.savedData.map(item => item.id); + this.updateLibraryFilters(this.librarySettings.savedData); return of(true); })); } @@ -278,7 +279,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.genres && this.filterSettings.presets?.genres.length > 0) { return this.genreSettings.fetchFn('').pipe(map(genres => { this.genreSettings.savedData = genres.filter(item => this.filterSettings.presets?.genres.includes(item.id)); - this.filter.genres = this.genreSettings.savedData.map(item => item.id); + this.updateGenreFilters(this.genreSettings.savedData); return of(true); })); } @@ -305,7 +306,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.ageRating && this.filterSettings.presets?.ageRating.length > 0) { return this.ageRatingSettings.fetchFn('').pipe(map(rating => { this.ageRatingSettings.savedData = rating.filter(item => this.filterSettings.presets?.ageRating.includes(item.value)); - this.filter.ageRating = this.ageRatingSettings.savedData.map(item => item.value); + this.updateAgeRating(this.ageRatingSettings.savedData); return of(true); })); } @@ -332,7 +333,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.publicationStatus && this.filterSettings.presets?.publicationStatus.length > 0) { return this.publicationStatusSettings.fetchFn('').pipe(map(statuses => { this.publicationStatusSettings.savedData = statuses.filter(item => this.filterSettings.presets?.publicationStatus.includes(item.value)); - this.filter.publicationStatus = this.publicationStatusSettings.savedData.map(item => item.value); + this.updatePublicationStatus(this.publicationStatusSettings.savedData); return of(true); })); } @@ -358,7 +359,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.tags && this.filterSettings.presets?.tags.length > 0) { return this.tagsSettings.fetchFn('').pipe(map(tags => { this.tagsSettings.savedData = tags.filter(item => this.filterSettings.presets?.tags.includes(item.id)); - this.filter.tags = this.tagsSettings.savedData.map(item => item.id); + this.updateTagFilters(this.tagsSettings.savedData); return of(true); })); } @@ -384,7 +385,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.languages && this.filterSettings.presets?.languages.length > 0) { return this.languageSettings.fetchFn('').pipe(map(languages => { this.languageSettings.savedData = languages.filter(item => this.filterSettings.presets?.languages.includes(item.isoCode)); - this.filter.languages = this.languageSettings.savedData.map(item => item.isoCode); + this.updateLanguages(this.languageSettings.savedData); return of(true); })); } @@ -410,7 +411,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) { return this.collectionSettings.fetchFn('').pipe(map(tags => { this.collectionSettings.savedData = tags.filter(item => this.filterSettings.presets?.collectionTags.includes(item.id)); - this.filter.collectionTags = this.collectionSettings.savedData.map(item => item.id); + this.updateCollectionFilters(this.collectionSettings.savedData); return of(true); })); } @@ -423,16 +424,15 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { const fetch = personSettings.fetchFn as ((filter: string) => Observable); return fetch('').pipe(map(people => { personSettings.savedData = people.filter(item => presetField.includes(item.id)); - peopleFilterField = personSettings.savedData.map(item => item.id); - this.resetTypeaheads.next(true); this.peopleSettings[role] = personSettings; - this.updatePersonFilters(personSettings.savedData as Person[], role); + this.updatePersonFilters(personSettings.savedData, role); return true; })); - } else { - this.peopleSettings[role] = personSettings; - return of(true); } + + this.peopleSettings[role] = personSettings; + return of(true); + } setupPersonTypeahead() { @@ -449,8 +449,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.updateFromPreset('penciller', this.filter.penciller, this.filterSettings.presets?.penciller, PersonRole.Penciller), this.updateFromPreset('publisher', this.filter.publisher, this.filterSettings.presets?.publisher, PersonRole.Publisher), this.updateFromPreset('translators', this.filter.translators, this.filterSettings.presets?.translators, PersonRole.Translator) - ]).pipe(map(results => { - this.resetTypeaheads.next(true); + ]).pipe(map(_ => { return of(true); })); } @@ -537,6 +536,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { } updateRating(rating: any) { + if (this.filterSettings.ratingDisabled) return; this.filter.rating = rating; } @@ -548,7 +548,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.filter.publicationStatus = dtos.map(item => item.value) || []; } - updateLanguageRating(languages: Language[]) { + updateLanguages(languages: Language[]) { this.filter.languages = languages.map(item => item.isoCode) || []; } @@ -563,6 +563,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { } updateSortOrder() { + if (this.filterSettings.sortDisabled) return; this.isAscendingSort = !this.isAscendingSort; if (this.filter.sortOptions === null) { this.filter.sortOptions = { @@ -582,7 +583,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.sortGroup.get('sortField')?.setValue(SortField.SortName); this.isAscendingSort = true; // Apply any presets which will trigger the apply - this.setupTypeaheads(); + this.loadFromPresetsAndSetup(); } apply() { 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 5471b9573..584fc55dc 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -1,10 +1,10 @@ import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { NgbModal, NgbNavChangeEvent, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { forkJoin, Subject } from 'rxjs'; -import { finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators'; +import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component'; import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component'; 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 02a554d44..e8136aacc 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 @@ -4,12 +4,12 @@
- {{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}} + {{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}} {{seriesMetadata.releaseYear}} - {{seriesMetadata.language}} - {{seriesMetadata.publicationStatus | publicationStatus}} - + {{seriesMetadata.language}} + {{seriesMetadata.publicationStatus | publicationStatus}} + {{utilityService.mangaFormat(series.format)}} @@ -25,7 +25,7 @@
- {{item.title}} + {{item.title}}
@@ -70,7 +70,7 @@
- +
@@ -89,7 +89,7 @@
- +
@@ -102,7 +102,7 @@
- +
@@ -115,7 +115,7 @@
- +
@@ -128,7 +128,7 @@
- +
@@ -141,7 +141,7 @@
- +
@@ -154,7 +154,7 @@
- +
@@ -166,7 +166,7 @@
- {{item.title}} + {{item.title}}
@@ -178,7 +178,7 @@
- +
@@ -191,7 +191,7 @@
- +
@@ -204,7 +204,7 @@
- +
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 f5a505780..bd410d585 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 @@ -1,6 +1,7 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Router } from '@angular/router'; import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component'; +import { FilterQueryParam } from '../shared/_services/filter-utilities.service'; import { UtilityService } from '../shared/_services/utility.service'; import { MangaFormat } from '../_models/manga-format'; import { ReadingList } from '../_models/reading-list'; @@ -38,6 +39,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges { return TagBadgeCursor; } + get FilterQueryParam() { + return FilterQueryParam; + } + constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router) { } ngOnChanges(changes: SimpleChanges): void { @@ -64,10 +69,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges { this.isCollapsed = !this.isCollapsed; } - goTo(queryParamName: string, filter: any) { + goTo(queryParamName: FilterQueryParam, filter: any) { let params: any = {}; params[queryParamName] = filter; - params['page'] = 1; + params[FilterQueryParam.Page] = 1; this.router.navigate(['library', this.series.libraryId], {queryParams: params}); } 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 80656ad4c..77dd2910b 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -1,10 +1,42 @@ import { Injectable } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; -import { LibraryType } from 'src/app/_models/library'; import { Pagination } from 'src/app/_models/pagination'; import { SeriesFilter, SortField } from 'src/app/_models/series-filter'; import { SeriesService } from 'src/app/_services/series.service'; +/** + * Used to pass state between the filter and the url + */ +export enum FilterQueryParam { + Format = 'format', + Genres = 'genres', + AgeRating = 'ageRating', + PublicationStatus = 'publicationStatus', + Tags = 'tags', + Languages = 'languages', + CollectionTags = 'collectionTags', + Libraries = 'libraries', + Writers = 'writers', + Artists = 'artists', + Character = 'character', + Colorist = 'colorist', + CoverArtists = 'coverArtists', + Editor = 'editor', + Inker = 'inker', + Letterer = 'letterer', + Penciller = 'penciller', + Publisher = 'publisher', + Translator = 'translators', + ReadStatus = 'readStatus', + SortBy = 'sortBy', + Rating = 'rating', + Name = 'name', + /** + * This is a pagination control + */ + Page = 'page' +} + @Injectable({ providedIn: 'root' }) @@ -38,10 +70,11 @@ export class FilterUtilitiesService { /** * Will fetch current page from route if present + * @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data * @returns A default pagination object */ - pagination(): Pagination { - return {currentPage: parseInt(this.route.snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1}; + pagination(snapshot: ActivatedRouteSnapshot): Pagination { + return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1}; } @@ -54,46 +87,44 @@ export class FilterUtilitiesService { urlFromFilter(currentUrl: string, filter: SeriesFilter | undefined) { if (filter === undefined) return currentUrl; let params = ''; - - - params += this.joinFilter(filter.formats, 'format'); - params += this.joinFilter(filter.genres, 'genres'); - params += this.joinFilter(filter.ageRating, 'ageRating'); - params += this.joinFilter(filter.publicationStatus, 'publicationStatus'); - params += this.joinFilter(filter.tags, 'tags'); - params += this.joinFilter(filter.languages, 'languages'); - params += this.joinFilter(filter.collectionTags, 'collectionTags'); - params += this.joinFilter(filter.libraries, 'libraries'); + params += this.joinFilter(filter.formats, FilterQueryParam.Format); + params += this.joinFilter(filter.genres, FilterQueryParam.Genres); + params += this.joinFilter(filter.ageRating, FilterQueryParam.AgeRating); + params += this.joinFilter(filter.publicationStatus, FilterQueryParam.PublicationStatus); + params += this.joinFilter(filter.tags, FilterQueryParam.Tags); + params += this.joinFilter(filter.languages, FilterQueryParam.Languages); + params += this.joinFilter(filter.collectionTags, FilterQueryParam.CollectionTags); + params += this.joinFilter(filter.libraries, FilterQueryParam.Libraries); - params += this.joinFilter(filter.writers, 'writers'); - params += this.joinFilter(filter.artists, 'artists'); - params += this.joinFilter(filter.character, 'character'); - params += this.joinFilter(filter.colorist, 'colorist'); - params += this.joinFilter(filter.coverArtist, 'coverArtists'); - params += this.joinFilter(filter.editor, 'editor'); - params += this.joinFilter(filter.inker, 'inker'); - params += this.joinFilter(filter.letterer, 'letterer'); - params += this.joinFilter(filter.penciller, 'penciller'); - params += this.joinFilter(filter.publisher, 'publisher'); - params += this.joinFilter(filter.translators, 'translators'); + params += this.joinFilter(filter.writers, FilterQueryParam.Writers); + params += this.joinFilter(filter.artists, FilterQueryParam.Artists); + params += this.joinFilter(filter.character, FilterQueryParam.Character); + params += this.joinFilter(filter.colorist, FilterQueryParam.Colorist); + params += this.joinFilter(filter.coverArtist, FilterQueryParam.CoverArtists); + params += this.joinFilter(filter.editor, FilterQueryParam.Editor); + params += this.joinFilter(filter.inker, FilterQueryParam.Inker); + params += this.joinFilter(filter.letterer, FilterQueryParam.Letterer); + params += this.joinFilter(filter.penciller, FilterQueryParam.Penciller); + params += this.joinFilter(filter.publisher, FilterQueryParam.Publisher); + params += this.joinFilter(filter.translators, FilterQueryParam.Translator); // readStatus (we need to do an additonal check as there is a default case) if (filter.readStatus && filter.readStatus.inProgress !== true && filter.readStatus.notRead !== true && filter.readStatus.read !== true) { - params += '&readStatus=' + `${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`; + params += `&${FilterQueryParam.ReadStatus}=${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`; } // sortBy (additional check to not save to url if default case) if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) { - params += '&sortBy=' + filter.sortOptions.sortField + ',' + filter.sortOptions.isAscending; + params += `&${FilterQueryParam.SortBy}=${filter.sortOptions.sortField},${filter.sortOptions.isAscending}`; } if (filter.rating > 0) { - params += '&rating=' + filter.rating; + params += `&${FilterQueryParam.Rating}=${filter.rating}`; } if (filter.seriesNameQuery !== '') { - params += '&name=' + encodeURIComponent(filter.seriesNameQuery); + params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`; } return currentUrl + params; @@ -102,143 +133,143 @@ export class FilterUtilitiesService { private joinFilter(filterProp: Array, key: string) { let params = ''; if (filterProp.length > 0) { - params += `&${key}=` + filterProp.join(','); + params += `&${key}=${filterProp.join(',')}`; } return params; } /** * Returns a new instance of a filterSettings that is populated with filter presets from URL + * @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data * @returns The Preset filter and if something was set within */ - filterPresetsFromUrl(): [SeriesFilter, boolean] { - const snapshot = this.route.snapshot; + filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] { const filter = this.seriesService.createSeriesFilter(); let anyChanged = false; - const format = snapshot.queryParamMap.get('format'); + const format = snapshot.queryParamMap.get(FilterQueryParam.Format); if (format !== undefined && format !== null) { filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const genres = snapshot.queryParamMap.get('genres'); + const genres = snapshot.queryParamMap.get(FilterQueryParam.Genres); if (genres !== undefined && genres !== null) { filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const ageRating = snapshot.queryParamMap.get('ageRating'); + const ageRating = snapshot.queryParamMap.get(FilterQueryParam.AgeRating); if (ageRating !== undefined && ageRating !== null) { filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const publicationStatus = snapshot.queryParamMap.get('publicationStatus'); + const publicationStatus = snapshot.queryParamMap.get(FilterQueryParam.PublicationStatus); if (publicationStatus !== undefined && publicationStatus !== null) { filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const tags = snapshot.queryParamMap.get('tags'); + const tags = snapshot.queryParamMap.get(FilterQueryParam.Tags); if (tags !== undefined && tags !== null) { filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const languages = snapshot.queryParamMap.get('languages'); + const languages = snapshot.queryParamMap.get(FilterQueryParam.Languages); if (languages !== undefined && languages !== null) { filter.languages = [...filter.languages, ...languages.split(',')]; anyChanged = true; } - const writers = snapshot.queryParamMap.get('writers'); + const writers = snapshot.queryParamMap.get(FilterQueryParam.Writers); if (writers !== undefined && writers !== null) { filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const artists = snapshot.queryParamMap.get('artists'); + const artists = snapshot.queryParamMap.get(FilterQueryParam.Artists); if (artists !== undefined && artists !== null) { filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const character = snapshot.queryParamMap.get('character'); + const character = snapshot.queryParamMap.get(FilterQueryParam.Character); if (character !== undefined && character !== null) { filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const colorist = snapshot.queryParamMap.get('colorist'); + const colorist = snapshot.queryParamMap.get(FilterQueryParam.Colorist); if (colorist !== undefined && colorist !== null) { filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const coverArtists = snapshot.queryParamMap.get('coverArtists'); + const coverArtists = snapshot.queryParamMap.get(FilterQueryParam.CoverArtists); if (coverArtists !== undefined && coverArtists !== null) { filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const editor = snapshot.queryParamMap.get('editor'); + const editor = snapshot.queryParamMap.get(FilterQueryParam.Editor); if (editor !== undefined && editor !== null) { filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const inker = snapshot.queryParamMap.get('inker'); + const inker = snapshot.queryParamMap.get(FilterQueryParam.Inker); if (inker !== undefined && inker !== null) { filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const letterer = snapshot.queryParamMap.get('letterer'); + const letterer = snapshot.queryParamMap.get(FilterQueryParam.Letterer); if (letterer !== undefined && letterer !== null) { filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const penciller = snapshot.queryParamMap.get('penciller'); + const penciller = snapshot.queryParamMap.get(FilterQueryParam.Penciller); if (penciller !== undefined && penciller !== null) { filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const publisher = snapshot.queryParamMap.get('publisher'); + const publisher = snapshot.queryParamMap.get(FilterQueryParam.Publisher); if (publisher !== undefined && publisher !== null) { filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const translators = snapshot.queryParamMap.get('translators'); + const translators = snapshot.queryParamMap.get(FilterQueryParam.Translator); if (translators !== undefined && translators !== null) { filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const libraries = snapshot.queryParamMap.get('libraries'); + const libraries = snapshot.queryParamMap.get(FilterQueryParam.Libraries); if (libraries !== undefined && libraries !== null) { filter.libraries = [...filter.libraries, ...libraries.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } - const collectionTags = snapshot.queryParamMap.get('collectionTags'); + const collectionTags = snapshot.queryParamMap.get(FilterQueryParam.CollectionTags); if (collectionTags !== undefined && collectionTags !== null) { filter.collectionTags = [...filter.collectionTags, ...collectionTags.split(',').map(item => parseInt(item, 10))]; anyChanged = true; } // Rating, seriesName, - const rating = snapshot.queryParamMap.get('rating'); + const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating); if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) { filter.rating = parseInt(rating, 10); anyChanged = true; } /// Read status is encoded as true,true,true - const readStatus = snapshot.queryParamMap.get('readStatus'); + const readStatus = snapshot.queryParamMap.get(FilterQueryParam.ReadStatus); if (readStatus !== undefined && readStatus !== null) { const values = readStatus.split(',').map(i => i === 'true'); if (values.length === 3) { @@ -249,7 +280,7 @@ export class FilterUtilitiesService { } } - const sortBy = snapshot.queryParamMap.get('sortBy'); + const sortBy = snapshot.queryParamMap.get(FilterQueryParam.SortBy); if (sortBy !== undefined && sortBy !== null) { const values = sortBy.split(','); if (values.length === 1) { @@ -264,7 +295,7 @@ export class FilterUtilitiesService { } } - const searchNameQuery = snapshot.queryParamMap.get('name'); + const searchNameQuery = snapshot.queryParamMap.get(FilterQueryParam.Name); if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') { filter.seriesNameQuery = decodeURIComponent(searchNameQuery); anyChanged = true; diff --git a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html index d41997a73..6e59b9e6d 100644 --- a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html +++ b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html @@ -4,7 +4,7 @@
-
+
diff --git a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.scss b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.scss index 6e7f6dbda..6b20aa117 100644 --- a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.scss +++ b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.scss @@ -1,5 +1,5 @@ .hide-if-empty:empty { - display: none; + display: none !important; } ::ng-deep .companion-bar { diff --git a/UI/Web/src/app/typeahead/typeahead.component.html b/UI/Web/src/app/typeahead/typeahead.component.html index 635e11170..a0e8a9389 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.html +++ b/UI/Web/src/app/typeahead/typeahead.component.html @@ -5,17 +5,17 @@ Field is locked -
+
- + - +
Loading...
- +
diff --git a/UI/Web/src/app/typeahead/typeahead.component.scss b/UI/Web/src/app/typeahead/typeahead.component.scss index dbd61f1f2..d473c6e15 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.scss +++ b/UI/Web/src/app/typeahead/typeahead.component.scss @@ -43,6 +43,10 @@ input { border: 1px solid var(--input-border-color); color: var(--body-text-color); + &.disabled { + cursor: not-allowed !important; + } + input { outline: 0 !important; border-radius: .28571429rem; diff --git a/UI/Web/src/app/typeahead/typeahead.component.ts b/UI/Web/src/app/typeahead/typeahead.component.ts index 3aee75d7b..a46201f28 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/typeahead.component.ts @@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common'; import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Observable, of, ReplaySubject, Subject } from 'rxjs'; -import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { KEY_CODES } from '../shared/_services/utility.service'; import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings'; @@ -141,17 +141,22 @@ export class TypeaheadComponent implements OnInit, OnDestroy { */ @Input() settings!: TypeaheadSettings; /** - * When true, component will re-init and set back to false. + * When true, will reset field to no selections. When false, will reset to saved data */ - @Input() reset: Subject = new ReplaySubject(1); + @Input() reset: ReplaySubject = new ReplaySubject(1); /** * When a field is locked, we render custom css to indicate to the user. Does not affect functionality. */ @Input() locked: boolean = false; + /** + * If disabled, a user will not be able to interact with the typeahead + */ + @Input() disabled: boolean = false; @Output() selectedData = new EventEmitter(); @Output() newItemAdded = new EventEmitter(); @Output() onUnlock = new EventEmitter(); @Output() lockedChange = new EventEmitter(); + @ViewChild('input') inputElem!: ElementRef; @ContentChild('optionItem') optionTemplate!: TemplateRef; @@ -178,8 +183,8 @@ export class TypeaheadComponent implements OnInit, OnDestroy { ngOnInit() { - this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => { - this.clearSelections(); + this.reset.pipe(takeUntil(this.onDestroy)).subscribe((resetToEmpty: boolean) => { + this.clearSelections(resetToEmpty); this.init(); }); @@ -274,6 +279,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy { @HostListener('window:keydown', ['$event']) handleKeyPress(event: KeyboardEvent) { if (!this.hasFocus) { return; } + if (this.disabled) return; switch(event.key) { case KEY_CODES.DOWN_ARROW: @@ -347,15 +353,26 @@ export class TypeaheadComponent implements OnInit, OnDestroy { this.resetField(); } - clearSelections() { + clearSelections(untoggleAll: boolean = false) { if (this.optionSelection) { - this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false)); + if (!untoggleAll && this.settings.savedData) { + const isArray = this.settings.savedData.hasOwnProperty('length'); + if (isArray) { + this.optionSelection = new SelectionModel(true, this.settings.savedData); + } else { + this.optionSelection = new SelectionModel(true, [this.settings.savedData]); + } + } else { + this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false)); + } + this.selectedData.emit(this.optionSelection.selected()); this.resetField(); } } handleOptionClick(opt: any) { + if (this.disabled) return; if (!this.settings.multiple && this.optionSelection.selected().length > 0) { return; } @@ -402,6 +419,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy { event.stopPropagation(); event.preventDefault(); } + if (this.disabled) return; if (!this.settings.multiple && this.optionSelection.selected().length > 0) { return; @@ -452,6 +470,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy { } unlock(event: any) { + if (this.disabled) return; this.locked = !this.locked; this.onUnlock.emit(); this.lockedChange.emit(this.locked);