mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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
This commit is contained in:
parent
ca893930d3
commit
28688ada8e
@ -7,10 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="14.0.3" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="14.0.13" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -47,29 +47,29 @@
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.38" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
||||
<PackageReference Include="NetVips" Version="2.0.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.11.4" />
|
||||
<PackageReference Include="NetVips" Version="2.1.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.12.1" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.32.0.39516">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.33.0.40503">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.14.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="14.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="14.0.13" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -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]+)");
|
||||
|
31
API/Controllers/MetadataController.cs
Normal file
31
API/Controllers/MetadataController.cs
Normal file
@ -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<ActionResult<IList<GenreTagDto>>> GetAllGenres()
|
||||
{
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("people")]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople()
|
||||
{
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeople());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public IList<MangaFormat> Formats { get; init; } = new List<MangaFormat>();
|
||||
|
||||
/// <summary>
|
||||
/// The progress you want to be returned. This can be bitwise manipulated. Defaults to all applicable states.
|
||||
/// </summary>
|
||||
public ReadStatus ReadStatus { get; init; } = new ReadStatus();
|
||||
|
||||
/// <summary>
|
||||
/// A list of library ids to restrict search to. Defaults to all libraries by passing empty list
|
||||
/// </summary>
|
||||
public IList<int> Libraries { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Genre ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Genres { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Writers to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Writers { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Penciller ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Penciller { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Inker ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Inker { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Colorist ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Colorist { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Letterer ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Letterer { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of CoverArtist ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> CoverArtist { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Editor ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Editor { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Publisher ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Publisher { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Character ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> Character { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// A list of Collection Tag ids to restrict search to. Defaults to all genres by passing an empty list
|
||||
/// </summary>
|
||||
public IList<int> CollectionTags { get; init; } = new List<int>();
|
||||
/// <summary>
|
||||
/// Will return back everything with the rating and above
|
||||
/// <see cref="AppUserRating.Rating"/>
|
||||
/// </summary>
|
||||
public int Rating { get; init; }
|
||||
|
||||
}
|
||||
}
|
||||
|
13
API/DTOs/Filtering/ReadStatus.cs
Normal file
13
API/DTOs/Filtering/ReadStatus.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace API.DTOs.Filtering;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the Reading Status. This is a flag and allows multiple statues
|
||||
/// </summary>
|
||||
public class ReadStatus
|
||||
{
|
||||
public bool NotRead { get; set; } = false;
|
||||
public bool InProgress { get; set; } = false;
|
||||
public bool Read { get; set; } = false;
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1228
API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs
generated
Normal file
1228
API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
API/Data/Migrations/20211214000230_SeriesIncludes.cs
Normal file
57
API/Data/Migrations/20211214000230_SeriesIncludes.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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<Genre> FindByNameAsync(string genreName);
|
||||
Task<IList<Genre>> GetAllGenres();
|
||||
Task<IList<Genre>> GetAllGenresAsync();
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
||||
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
||||
}
|
||||
|
||||
@ -57,8 +60,15 @@ public class GenreRepository : IGenreRepository
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Genre>> GetAllGenres()
|
||||
public async Task<IList<Genre>> GetAllGenresAsync()
|
||||
{
|
||||
return await _context.Genre.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync()
|
||||
{
|
||||
return await _context.Genre
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
@ -177,17 +177,15 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<PagedList<SeriesDto>> 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<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
private async Task<List<int>> GetUserLibraries(int libraryId, int userId)
|
||||
@ -247,7 +245,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
public async Task<bool> 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
|
||||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> 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<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
private IList<MangaFormat> ExtractFilters(int libraryId, int userId, FilterDto filter, ref List<int> userLibraries,
|
||||
out List<int> allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter,
|
||||
out bool hasRatingFilter, out bool hasProgressFilter, out IList<int> seriesIds)
|
||||
{
|
||||
var formats = filter.GetSqlFilter();
|
||||
|
||||
if (filter.Libraries.Count > 0)
|
||||
{
|
||||
userLibraries = userLibraries.Where(l => filter.Libraries.Contains(l)).ToList();
|
||||
}
|
||||
|
||||
allPeopleIds = new List<int>();
|
||||
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<int>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -401,24 +465,23 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<SeriesDto>> 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<IQueryable<Series>> 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<FilterableQuery> 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<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
|
||||
{
|
||||
var metadataDto = await _context.SeriesMetadata
|
||||
|
@ -53,6 +53,8 @@ namespace API.Entities
|
||||
public MangaFormat Format { get; set; } = MangaFormat.Unknown;
|
||||
|
||||
public SeriesMetadata Metadata { get; set; }
|
||||
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
|
||||
public ICollection<AppUserProgress> Progress { get; set; } = new List<AppUserProgress>();
|
||||
|
||||
// Relationships
|
||||
public List<Volume> Volumes { get; set; }
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using API.DTOs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Helpers
|
||||
|
@ -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)(?<Series>.+?)(?<!Vol)\.?\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
|
||||
@"^(?!Vol)(?<Series>.+?)(?<!Vol)(?<!Vol.)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Tower Of God S01 014 (CBT) (digital).cbz
|
||||
new Regex(
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -11,7 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.32.0.39516">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.33.0.40503">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -13,6 +13,7 @@ export enum PersonRole {
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
id: number;
|
||||
name: string;
|
||||
role: PersonRole;
|
||||
}
|
@ -1,38 +1,53 @@
|
||||
import { MangaFormat } from "./manga-format";
|
||||
|
||||
export interface FilterItem {
|
||||
export interface FilterItem<T> {
|
||||
title: string;
|
||||
value: any;
|
||||
value: T;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface SeriesFilter {
|
||||
formats: Array<MangaFormat>;
|
||||
libraries: Array<number>,
|
||||
readStatus: ReadStatus;
|
||||
genres: Array<number>;
|
||||
writers: Array<number>;
|
||||
penciller: Array<number>;
|
||||
inker: Array<number>;
|
||||
colorist: Array<number>;
|
||||
letterer: Array<number>;
|
||||
coverArtist: Array<number>;
|
||||
editor: Array<number>;
|
||||
publisher: Array<number>;
|
||||
character: Array<number>;
|
||||
collectionTags: Array<number>;
|
||||
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
|
||||
}
|
||||
|
@ -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<Genre[]>(this.baseUrl + 'metadata/genres');
|
||||
}
|
||||
|
||||
getAllPeople() {
|
||||
return this.httpClient.get<Person[]>(this.baseUrl + 'metadata/people');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -10,24 +10,264 @@
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button *ngIf="filters !== undefined && filters.length > 0" class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<button class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||
<span class="sr-only">Sort / Filter</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row no-gutters filter-section" #collapse="ngbCollapse" [(ngbCollapse)]="filteringCollapsed">
|
||||
<div class="col">
|
||||
<form class="ml-2" [formGroup]="filterForm">
|
||||
<div class="form-group" *ngIf="filters.length > 0">
|
||||
<label for="series-filter">Filter</label>
|
||||
<select class="form-control" id="series-filter" formControlName="filter" (ngModelChange)="handleFilterChange($event)" style="max-width: 200px;">
|
||||
<option [value]="i" *ngFor="let opt of filters; let i = index">{{opt.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="phone-hidden">
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="filteringCollapsed">
|
||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="not-phone-hidden">
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||
<div header>
|
||||
<h2 style="margin-top: 0.5rem">Book Settings
|
||||
<button type="button" class="close" aria-label="Close" (click)="commentDrawer.close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
<div body class="drawer-body">
|
||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||
</div>
|
||||
</app-drawer>
|
||||
</div>
|
||||
|
||||
<ng-template #filterSection>
|
||||
<div class="filter-section">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-3" *ngIf="!filterSettings.formatDisabled">
|
||||
<div class="form-group">
|
||||
<label for="format">Format</label>
|
||||
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3"*ngIf="!filterSettings.libraryDisabled">
|
||||
<div class="form-group">
|
||||
<label for="libraries">Libraries</label>
|
||||
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="!filterSettings.collectionDisabled">
|
||||
<div class="form-group">
|
||||
<label for="collections">Collections</label>
|
||||
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="!filterSettings.genresDisabled">
|
||||
<div class="form-group">
|
||||
<label for="genres">Genres</label>
|
||||
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- The People row -->
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
|
||||
<div class="form-group">
|
||||
<label for="cover-artist">(Cover) Artists</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
|
||||
<div class="form-group">
|
||||
<label for="writers">Writers</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
|
||||
<div class="form-group">
|
||||
<label for="publisher">Publisher</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
|
||||
<div class="form-group">
|
||||
<label for="Penciller">Penciller</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
|
||||
<div class="form-group">
|
||||
<label for="letterer">Letterer</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
|
||||
<div class="form-group">
|
||||
<label for="inker">Inker</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
|
||||
<div class="form-group">
|
||||
<label for="editor">Editor</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
|
||||
<div class="form-group">
|
||||
<label for="colorist">Colorist</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
|
||||
<div class="form-group">
|
||||
<label for="character">Character</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- Rating/Review/Progress -->
|
||||
<div class="col" *ngIf="!filterSettings.readProgressDisabled">
|
||||
<!-- Not sure how to do this on the backend, might have to be a UI control -->
|
||||
<label>Read Progress</label>
|
||||
<form [formGroup]="readProgressGroup" class="ml-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="notread" formControlName="notRead">
|
||||
<label class="form-check-label" for="notread">Unread</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="inprogress" formControlName="inProgress">
|
||||
<label class="form-check-label" for="inprogress">In Progress</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="read" formControlName="read">
|
||||
<label class="form-check-label" for="read">Read</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col" *ngIf="!filterSettings.ratingDisabled">
|
||||
<label for="ratings">Rating</label>
|
||||
<form class="form-inline ml-2">
|
||||
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
|
||||
<ng-template let-fill="fill" let-index="index">
|
||||
<span class="star" [class.filled]="(index >= (filter.rating - 1)) && filter.rating > 0" [ngbTooltip]="(index + 1) + ' and up'">★</span>
|
||||
</ng-template>
|
||||
</ngb-rating>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<button class="btn btn-secondary mr-2" (click)="clear()">Clear</button>
|
||||
<button class="btn btn-primary" (click)="apply()">Apply</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- Sort by functionalities -->
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
@use '../../../theme/colors';
|
||||
|
||||
.star {
|
||||
font-size: 1.5rem;
|
||||
color: colors.$rating-empty;
|
||||
}
|
||||
.filled {
|
||||
color: colors.$rating-filled;
|
||||
}
|
@ -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<any>[] = [];
|
||||
/**
|
||||
* A list of Filters which can filter the data of the page. If nothing is passed, the control will not show.
|
||||
*/
|
||||
@Input() filters: Array<FilterItem> = [];
|
||||
@Input() trackByIdentity!: (index: number, item: any) => string;
|
||||
@Input() filterSettings!: FilterSettings;
|
||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
||||
@Output() applyFilter: EventEmitter<UpdateFilterEvent> = new EventEmitter();
|
||||
@Output() applyFilter: EventEmitter<SeriesFilter> = new EventEmitter();
|
||||
|
||||
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
filterForm: FormGroup = new FormGroup({
|
||||
filter: new FormControl(0, []),
|
||||
});
|
||||
|
||||
formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings();
|
||||
librarySettings: TypeaheadSettings<FilterItem<Library>> = new TypeaheadSettings();
|
||||
genreSettings: TypeaheadSettings<FilterItem<Genre>> = new TypeaheadSettings();
|
||||
collectionSettings: TypeaheadSettings<FilterItem<CollectionTag>> = new TypeaheadSettings();
|
||||
peopleSettings: {[PersonRole: string]: TypeaheadSettings<FilterItem<Person>>} = {};
|
||||
resetTypeaheads: Subject<boolean> = new ReplaySubject(1);
|
||||
|
||||
/**
|
||||
* Controls the visiblity of extended controls that sit below the main header.
|
||||
*/
|
||||
filteringCollapsed: boolean = true;
|
||||
|
||||
constructor() { }
|
||||
filter!: SeriesFilter;
|
||||
libraries: Array<FilterItem<Library>> = [];
|
||||
genres: Array<FilterItem<Genre>> = [];
|
||||
persons: Array<FilterItem<Person>> = [];
|
||||
collectionTags: Array<FilterItem<CollectionTag>> = [];
|
||||
|
||||
readProgressGroup!: FormGroup;
|
||||
|
||||
updateApplied: number = 0;
|
||||
|
||||
private onDestory: Subject<void> = 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<MangaFormat>[], 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<Library>[], 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<Genre>[], 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<CollectionTag>[], 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<FilterItem<Person>>();
|
||||
personSettings.minCharacters = 0;
|
||||
personSettings.multiple = true;
|
||||
personSettings.unique = true;
|
||||
personSettings.addIfNonExisting = false;
|
||||
personSettings.id = id;
|
||||
personSettings.compareFn = (options: FilterItem<Person>[], 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<MangaFormat>[]) {
|
||||
this.filter.formats = formats.map(item => item.value) || [];
|
||||
}
|
||||
|
||||
updateLibraryFilters(libraries: FilterItem<Library>[]) {
|
||||
this.filter.libraries = libraries.map(item => item.value.id) || [];
|
||||
}
|
||||
|
||||
updateGenreFilters(genres: FilterItem<Genre>[]) {
|
||||
this.filter.genres = genres.map(item => item.value.id) || [];
|
||||
}
|
||||
|
||||
updatePersonFilters(persons: FilterItem<Person>[], 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<CollectionTag>[]) {
|
||||
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++;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -49,4 +49,5 @@ export class ChapterMetadataDetailComponent implements OnInit {
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -33,7 +33,6 @@
|
||||
[items]="series"
|
||||
[pagination]="seriesPagination"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
@ -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<CollectionTag>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
filters: Array<FilterItem> = mangaFormatFilters;
|
||||
filter: SeriesFilter = {
|
||||
formats: []
|
||||
};
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
|
||||
@ -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);
|
||||
|
@ -4,7 +4,6 @@
|
||||
[items]="series"
|
||||
[actions]="actions"
|
||||
[pagination]="pagination"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
|
@ -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<Library>[] = [];
|
||||
filters: Array<FilterItem> = mangaFormatFilters;
|
||||
filter: SeriesFilter = {
|
||||
formats: []
|
||||
};
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
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);
|
||||
|
@ -2,11 +2,10 @@
|
||||
<app-card-detail-layout header="On Deck"
|
||||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[filters]="filters"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
|
@ -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<FilterItem> = 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);
|
||||
|
@ -3,8 +3,7 @@
|
||||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(applyFilter)="applyFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
@ -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<FilterItem> = mangaFormatFilters;
|
||||
filter: SeriesFilter = {
|
||||
formats: []
|
||||
};
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
|
||||
onDestroy: Subject<void> = 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);
|
||||
|
@ -49,22 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- TODO: This will be the first of reviews section. Reviews will show your plus other peoples reviews in media cards like Plex does and this will be below metadata -->
|
||||
<app-read-more class="user-review {{userReview ? 'mt-1' : ''}}" [text]="series?.userReview || ''" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
<div class="row no-gutters {{series?.userReview ? '' : 'mt-2'}}">
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
<div *ngIf="seriesMetadata" class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [series]="series"></app-series-metadata-detail>
|
||||
|
||||
<!-- <div class="row no-gutters mt-1" *ngIf="series.format != MangaFormat.UNKNOWN">
|
||||
<div class="col-md-4">
|
||||
<h5>Type</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed"><app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format></app-tag-badge>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -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, '<br>');
|
||||
}
|
||||
|
||||
this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
<div class="row no-gutters {{series?.userReview ? '' : 'mt-2'}}">
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<div class="row no-gutters" *ngIf="seriesMetadata.ageRating">
|
||||
<app-tag-badge title="Age Rating">{{ageRatingName}}</app-tag-badge>
|
||||
|
@ -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, '<br>');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -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:
|
||||
|
@ -1,7 +1,5 @@
|
||||
<form [formGroup]="typeaheadForm">
|
||||
<ng-container *ngIf="settings.multiple" >
|
||||
|
||||
|
||||
<div class="typeahead-input" (click)="onInputFocus($event)">
|
||||
<div>
|
||||
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
|
||||
@ -13,6 +11,7 @@
|
||||
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoadingOptions">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<!-- TODO: Add a clear all button -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown" *ngIf="hasFocus">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from './typeahead-settings';
|
||||
@ -143,6 +143,10 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
@Input() settings!: TypeaheadSettings<any>;
|
||||
/**
|
||||
* When true, component will re-init and set back to false.
|
||||
*/
|
||||
@Input() reset: Subject<boolean> = new ReplaySubject(1);
|
||||
@Output() selectedData = new EventEmitter<any[] | any>();
|
||||
@Output() newItemAdded = new EventEmitter<any[] | any>();
|
||||
|
||||
@ -167,6 +171,14 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => {
|
||||
this.init();
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.settings.compareFn === undefined && this.settings.multiple) {
|
||||
console.error('A compare function must be defined');
|
||||
return;
|
||||
|
@ -14,6 +14,20 @@ $dark-form-border: rgba(239, 239, 239, 0.125);
|
||||
$dark-form-readonly: #434648;
|
||||
$dark-item-accent-bg: #292d32;
|
||||
|
||||
|
||||
//=========================
|
||||
// Ratings
|
||||
//=========================
|
||||
$rating-filled: $primary-color;
|
||||
$rating-empty: #b0c4de;
|
||||
|
||||
//=========================
|
||||
// Drawers
|
||||
//=========================
|
||||
// :root {
|
||||
// --drawer-background-color: #FFF;
|
||||
// }
|
||||
|
||||
$theme-colors: (
|
||||
"primary": $primary-color,
|
||||
"danger": $error-color
|
||||
|
Loading…
x
Reference in New Issue
Block a user