diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index 90b3723d7..fa5bc34fa 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -87,8 +87,7 @@ public class DeviceController : BaseApiController [HttpGet] public async Task>> GetDevices() { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(userId)); + return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId())); } [HttpPost("send-to")] @@ -100,7 +99,7 @@ public class DeviceController : BaseApiController if (await _emailService.IsDefaultEmailService()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "started"), userId); @@ -134,7 +133,7 @@ public class DeviceController : BaseApiController if (await _emailService.IsDefaultEmailService()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "started"), userId); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 55890c13e..fc3965577 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -161,8 +161,7 @@ public class LibraryController : BaseApiController [HttpGet("jump-bar")] public async Task>> GetJumpBar(int libraryId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) + if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, User.GetUserId())) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access")); return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index e950bc1d5..0abf032af 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -37,17 +37,28 @@ public class MetadataController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllGenres(string? libraryIds) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids != null && ids.Count > 0) { - return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId)); + return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(userId)); + return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId())); } - + /// + /// Fetches people from the instance by role + /// + /// role + /// + [HttpGet("people-by-role")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})] + public async Task>> GetAllPeople(PersonRole? role) + { + return role.HasValue ? + Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) : + Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); + } /// /// Fetches people from the instance @@ -58,13 +69,12 @@ public class MetadataController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllPeople(string? libraryIds) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids != null && ids.Count > 0) { - return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId)); + return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(userId)); + return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); } /// @@ -76,13 +86,12 @@ public class MetadataController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllTags(string? libraryIds) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids != null && ids.Count > 0) { - return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId)); + return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(userId)); + return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId())); } /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 0e8899783..329fed1e2 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -39,8 +39,7 @@ public class ReadingListController : BaseApiController [HttpGet] public async Task>> GetList(int readingListId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId)); + return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId())); } /// @@ -54,8 +53,7 @@ public class ReadingListController : BaseApiController public async Task>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true, bool sortByLastModified = false) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted, + var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(User.GetUserId(), includePromoted, userParams, sortByLastModified); Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages); @@ -70,10 +68,8 @@ public class ReadingListController : BaseApiController [HttpGet("lists-for-series")] public async Task>> GetListsForSeries(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true); - - return Ok(items); + return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(User.GetUserId(), + seriesId, true)); } /// diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index d14f81edc..e2424a6dc 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -62,7 +62,7 @@ public class ReviewController : BaseApiController } var cacheKey = CacheKey + seriesId; - IEnumerable externalReviews; + IList externalReviews; var result = await _cacheProvider.GetAsync>(cacheKey); if (result.HasValue) @@ -74,7 +74,6 @@ public class ReviewController : BaseApiController var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList(); externalReviews = SelectSpectrumOfReviews(reviews); - await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10)); _logger.LogDebug("Caching external reviews for {Key}", cacheKey); } @@ -87,7 +86,7 @@ public class ReviewController : BaseApiController return Ok(userRatings); } - private static IList SelectSpectrumOfReviews(List reviews) + private static IList SelectSpectrumOfReviews(IList reviews) { IList externalReviews; var totalReviews = reviews.Count; diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 03ba05bed..98c969800 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -33,8 +33,7 @@ public class SearchController : BaseApiController [HttpGet("series-for-mangafile")] public async Task> GetSeriesForMangaFile(int mangaFileId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, User.GetUserId())); } /// @@ -46,8 +45,7 @@ public class SearchController : BaseApiController [HttpGet("series-for-chapter")] public async Task> GetSeriesForChapter(int chapterId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, User.GetUserId())); } [HttpGet("search")] diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index a8c1a82d1..a86d9626a 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -67,7 +67,7 @@ public class SeriesController : BaseApiController [Obsolete("use v2")] public async Task>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); @@ -90,7 +90,7 @@ public class SeriesController : BaseApiController [HttpPost("v2")] public async Task>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); @@ -114,8 +114,7 @@ public class SeriesController : BaseApiController [HttpGet("{seriesId:int}")] public async Task> GetSeries(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, User.GetUserId()); if (series == null) return NoContent(); return Ok(series); } @@ -150,15 +149,13 @@ public class SeriesController : BaseApiController [HttpGet("volumes")] public async Task>> GetVolumes(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)); + return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, User.GetUserId())); } [HttpGet("volume")] public async Task> GetVolume(int volumeId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId); + var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId()); if (vol == null) return NoContent(); return Ok(vol); } @@ -253,7 +250,7 @@ public class SeriesController : BaseApiController [Obsolete("use recently-added-v2")] public async Task>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); @@ -277,7 +274,7 @@ public class SeriesController : BaseApiController [HttpPost("recently-added-v2")] public async Task>> GetRecentlyAddedV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); @@ -299,8 +296,7 @@ public class SeriesController : BaseApiController [HttpPost("recently-updated-series")] public async Task>> GetRecentlyAddedChapters() { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, 20)); + return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(User.GetUserId(), 20)); } /// @@ -310,10 +306,10 @@ public class SeriesController : BaseApiController /// /// /// - [HttpPost("all")] - public async Task>> GetAllSeries(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) + [HttpPost("all-v2")] + public async Task>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); @@ -327,6 +323,31 @@ public class SeriesController : BaseApiController return Ok(series); } + /// + /// Returns all series for the library. Obsolete, use all-v2 + /// + /// + /// + /// + /// + [HttpPost("all")] + [Obsolete("User all-v2")] + public async Task>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) + { + var userId = User.GetUserId(); + var series = + await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + + // Apply progress/rating information (I can't work out how to do this in initial query) + if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series")); + + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); + + Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); + + return Ok(series); + } + /// /// Fetches series that are on deck aka have progress on them. /// @@ -337,7 +358,7 @@ public class SeriesController : BaseApiController [HttpPost("on-deck")] public async Task>> GetOnDeck([FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, null); await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList); @@ -419,25 +440,24 @@ public class SeriesController : BaseApiController [HttpPost("metadata")] public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) { - if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) + if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail")); + + if (await _licenseService.HasActiveLicense()) { - if (await _licenseService.HasActiveLicense()) + _logger.LogDebug("Clearing cache as series weblinks may have changed"); + await _reviewCacheProvider.RemoveAsync(ReviewController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); + await _ratingCacheProvider.RemoveAsync(RatingController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); + + var allUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(s => s.Id); + foreach (var userId in allUsers) { - _logger.LogDebug("Clearing cache as series weblinks may have changed"); - await _reviewCacheProvider.RemoveAsync(ReviewController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); - await _ratingCacheProvider.RemoveAsync(RatingController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); - - var allUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(s => s.Id); - foreach (var userId in allUsers) - { - await _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}"); - } + await _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}"); } - - return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated")); } - return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail")); + return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated")); + } /// @@ -449,7 +469,7 @@ public class SeriesController : BaseApiController [HttpGet("series-by-collection")] public async Task>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); @@ -472,8 +492,7 @@ public class SeriesController : BaseApiController public async Task>> GetAllSeriesById(SeriesByIdsDto dto) { if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload")); - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, User.GetUserId())); } /// @@ -503,10 +522,9 @@ public class SeriesController : BaseApiController [HttpGet("series-detail")] public async Task> GetSeriesDetailBreakdown(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); try { - return await _seriesService.GetSeriesDetail(seriesId, userId); + return await _seriesService.GetSeriesDetail(seriesId, User.GetUserId()); } catch (KavitaException ex) { @@ -525,9 +543,7 @@ public class SeriesController : BaseApiController [HttpGet("related")] public async Task>> GetRelatedSeries(int seriesId, RelationKind relation) { - // Send back a custom DTO with each type or maybe sorted in some way - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(User.GetUserId(), seriesId, relation)); } /// @@ -538,8 +554,7 @@ public class SeriesController : BaseApiController [HttpGet("all-related")] public async Task> GetAllRelatedSeries(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _seriesService.GetRelatedSeries(userId, seriesId)); + return Ok(await _seriesService.GetRelatedSeries(User.GetUserId(), seriesId)); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 755646c33..9358bd406 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -68,10 +68,9 @@ public class UsersController : BaseApiController [HttpGet("has-reading-progress")] public async Task> HasReadingProgress(int libraryId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist")); - return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId)); + return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, User.GetUserId())); } [HttpGet("has-library-access")] diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index a780013ce..73fef1c37 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -28,5 +28,13 @@ public enum FilterField ReadProgress = 20, Formats = 21, ReleaseYear = 22, - ReadTime = 23 + ReadTime = 23, + /// + /// Series Folder + /// + Path = 24, + /// + /// File path + /// + FilePath = 25 } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 0e05a6672..a6329f887 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; @@ -17,9 +18,11 @@ public interface IPersonRepository void Remove(Person person); Task> GetAllPeople(); Task> GetAllPersonDtosAsync(int userId); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false); Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId); Task GetCountAsync(); + } public class PersonRepository : IPersonRepository @@ -94,4 +97,15 @@ public class PersonRepository : IPersonRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } + + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role) + { + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + return await _context.Person + .Where(p => p.Role == role) + .OrderBy(p => p.Name) + .RestrictAgainstAgeRestriction(ageRating) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 2dd3fa972..aab679315 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -840,7 +840,6 @@ public class SeriesRepository : ISeriesRepository private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext) { // NOTE: Why do we even have libraryId when the filter has the actual libraryIds? - // TODO: Remove this method var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) @@ -869,7 +868,7 @@ public class SeriesRepository : ISeriesRepository .HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!) .HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0) - // This needs different treatment + // TODO: This needs different treatment .HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds) .WhereIf(onlyParentSeries, @@ -979,11 +978,12 @@ public class SeriesRepository : ISeriesRepository { if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains) { - filterIncludeLibs.Add(int.Parse(stmt.Value)); + + filterIncludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse)); } else { - filterExcludeLibs.Add(int.Parse(stmt.Value)); + filterExcludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse)); } } @@ -1036,6 +1036,8 @@ public class SeriesRepository : ISeriesRepository { FilterField.Summary => query.HasSummary(true, statement.Comparison, (string) value), FilterField.SeriesName => query.HasName(true, statement.Comparison, (string) value), + FilterField.Path => query.HasPath(true, statement.Comparison, (string) value), + FilterField.FilePath => query.HasFilePath(true, statement.Comparison, (string) value), FilterField.PublicationStatus => query.HasPublicationStatus(true, statement.Comparison, (IList) value), FilterField.Languages => query.HasLanguage(true, statement.Comparison, (IList) value), diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index b69c7594e..fa5e7b633 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using API.DTOs.Filtering.v2; using API.Entities; using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; using Kavita.Common; using Microsoft.EntityFrameworkCore; @@ -512,4 +513,116 @@ public static class SeriesFilter throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); } } + + public static IQueryable HasPath(this IQueryable queryable, bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = Parser.NormalizePath(queryString); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalizedPath)); + case FilterComparison.BeginsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"{normalizedPath}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}")); + case FilterComparison.Matches: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.FolderPath != null && s.FolderPath != normalizedPath); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public static IQueryable HasFilePath(this IQueryable queryable, bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = Parser.NormalizePath(queryString); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.BeginsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.EndsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}") + ) + ) + ) + ); + case FilterComparison.Matches: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.NotEqual: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath == null || !f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + } diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index 226a2d99f..36ab4913b 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -13,6 +13,8 @@ public static class FilterFieldValueConverter return field switch { FilterField.SeriesName => (value, typeof(string)), + FilterField.Path => (value, typeof(string)), + FilterField.FilePath => (value, typeof(string)), FilterField.ReleaseYear => (int.Parse(value), typeof(int)), FilterField.Languages => (value.Split(',').ToList(), typeof(IList)), FilterField.PublicationStatus => (value.Split(',') diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index dc34bcada..45c6d57ea 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -37,7 +37,7 @@ "file-saver": "^2.0.5", "lazysizes": "^5.3.2", "ng-circle-progress": "^1.7.1", - "ng-select2-component": "^13.0.2", + "ng-select2-component": "^13.0.6", "ngx-color-picker": "^14.0.0", "ngx-extended-pdf-viewer": "^16.2.16", "ngx-file-drop": "^16.0.0", @@ -10558,9 +10558,9 @@ } }, "node_modules/ng-select2-component": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.2.tgz", - "integrity": "sha512-8Tms5p0V/0J0vCWOf2Vrk6tJlwbaf3D3As3iigcjRncYlfXN130agniBcZ007C3zK2KyLXJJRkEWzlCls8/TVQ==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.6.tgz", + "integrity": "sha512-CiAelglSz2aeYy0BiXRi32zc49Mq27+J1eDzTrXmf2o50MvNo3asS3NRVQcnSldo/zLcJafWCMueVfjVaV1etw==", "dependencies": { "ngx-infinite-scroll": ">=16.0.0", "tslib": "^2.3.0" diff --git a/UI/Web/package.json b/UI/Web/package.json index d259d45c7..6e1f1b48c 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -42,7 +42,7 @@ "file-saver": "^2.0.5", "lazysizes": "^5.3.2", "ng-circle-progress": "^1.7.1", - "ng-select2-component": "^13.0.2", + "ng-select2-component": "^13.0.6", "ngx-color-picker": "^14.0.0", "ngx-extended-pdf-viewer": "^16.2.16", "ngx-file-drop": "^16.0.0", diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index 0c62af3e4..27335b7ca 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -24,7 +24,9 @@ export enum FilterField ReadProgress = 20, Formats = 21, ReleaseYear = 22, - ReadTime = 23 + ReadTime = 23, + Path = 24, + FilePath = 25 } export const allFields = Object.keys(FilterField) diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index d5dfd05ff..61eb56a76 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -8,7 +8,7 @@ import {AgeRating} from '../_models/metadata/age-rating'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {Language} from '../_models/metadata/language'; import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; -import {Person} from '../_models/metadata/person'; +import {Person, PersonRole} from '../_models/metadata/person'; import {Tag} from '../_models/tag'; import {TextResonse} from '../_types/text-response'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; @@ -33,44 +33,44 @@ export class MetadataService { constructor(private httpClient: HttpClient, private router: Router) { } - applyFilter(page: Array, filter: FilterField, comparison: FilterComparison, value: string) { - const dto: SeriesFilterV2 = { - statements: [this.createDefaultFilterStatement(filter, comparison, value + '')], - combination: FilterCombination.Or, - limitTo: 0 - }; - // - // console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto)); - // this.router.navigateByUrl(this.filterUtilityService.urlFromFilterV2(page.join('/'), dto)); + // applyFilter(page: Array, filter: FilterField, comparison: FilterComparison, value: string) { + // const dto: SeriesFilterV2 = { + // statements: [this.createDefaultFilterStatement(filter, comparison, value + '')], + // combination: FilterCombination.Or, + // limitTo: 0 + // }; + // // + // // console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto)); + // // this.router.navigateByUrl(this.filterUtilityService.urlFromFilterV2(page.join('/'), dto)); + // + // // Creates a temp name for the filter + // this.httpClient.post(this.baseUrl + 'filter/create-temp', dto, TextResonse).pipe(map(name => { + // dto.name = name; + // }), switchMap((_) => { + // let params: any = {}; + // params['filterName'] = dto.name; + // return this.router.navigate(page, {queryParams: params}); + // })).subscribe(); + // + // } - // Creates a temp name for the filter - this.httpClient.post(this.baseUrl + 'filter/create-temp', dto, TextResonse).pipe(map(name => { - dto.name = name; - }), switchMap((_) => { - let params: any = {}; - params['filterName'] = dto.name; - return this.router.navigate(page, {queryParams: params}); - })).subscribe(); + // getFilter(filterName: string) { + // return this.httpClient.get(this.baseUrl + 'filter?name=' + filterName); + // } - } - - getFilter(filterName: string) { - return this.httpClient.get(this.baseUrl + 'filter?name=' + filterName); - } - - getAgeRating(ageRating: AgeRating) { - if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) { - return of(this.ageRatingTypes[ageRating]); - } - return this.httpClient.get(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => { - if (this.ageRatingTypes === undefined) { - this.ageRatingTypes = {}; - } - - this.ageRatingTypes[ageRating] = ratingString; - return this.ageRatingTypes[ageRating]; - })); - } + // getAgeRating(ageRating: AgeRating) { + // if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) { + // return of(this.ageRatingTypes[ageRating]); + // } + // return this.httpClient.get(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => { + // if (this.ageRatingTypes === undefined) { + // this.ageRatingTypes = {}; + // } + // + // this.ageRatingTypes[ageRating] = ratingString; + // return this.ageRatingTypes[ageRating]; + // })); + // } getAllAgeRatings(libraries?: Array) { let method = 'metadata/age-ratings' @@ -132,10 +132,14 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } - getChapterSummary(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse); + getAllPeopleByRole(role: PersonRole) { + return this.httpClient.get>(this.baseUrl + 'metadata/people-by-role?role=' + role); } + // getChapterSummary(chapterId: number) { + // return this.httpClient.get(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse); + // } + createDefaultFilterDto(): SeriesFilterV2 { return { statements: [] as FilterStatement[], @@ -159,6 +163,6 @@ export class MetadataService { updateFilter(arr: Array, index: number, filterStmt: FilterStatement) { arr[index].comparison = filterStmt.comparison; arr[index].field = filterStmt.field; - arr[index].value = filterStmt.value + ''; + arr[index].value = filterStmt.value ? filterStmt.value + '' : ''; } } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 34f17b45f..dd021dbe0 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -39,7 +39,7 @@ export class SeriesService { params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; - return this.httpClient.post>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + 'series/all-v2', data, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response, this.paginatedResults); }) diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index d1f2a9acd..0847daa3a 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -115,12 +115,14 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges { } ngOnInit(): void { + console.log('[card-detail-layout] ngOnInit') if (this.trackByIdentity === undefined) { this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; } if (this.filterSettings === undefined) { this.filterSettings = new FilterSettings(); + console.log('[card-detail-layout] creating blank FilterSettings'); this.cdRef.markForCheck(); } @@ -178,6 +180,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges { this.applyFilter.emit(event); this.updateApplied++; this.filter = event.filterV2; + console.log('[card-detail-layout] apply filter') this.cdRef.markForCheck(); } 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 f2f71fc71..40bf3a81f 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -135,6 +135,11 @@ export class LibraryDetailComponent implements OnInit { } } + get Debug() { + console.log('rendered section '); + return 0; + } + constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService, private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html index f93851d37..b783ba1a1 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html @@ -1,9 +1,8 @@ - - -
-
+ + +
@@ -53,22 +49,21 @@
- -
-
- -
- -
-
+
+
+ +
+ +
+
+
-
- - + + diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts index f25c9392d..23306dd91 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts @@ -19,10 +19,9 @@ import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {FilterCombination} from "../../../_models/metadata/v2/filter-combination"; import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; -import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; -import {allFields, FilterField} from "../../../_models/metadata/v2/filter-field"; +import {allFields} from "../../../_models/metadata/v2/filter-field"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {tap} from "rxjs/operators"; +import {distinctUntilChanged, tap} from "rxjs/operators"; import {translate, TranslocoDirective} from "@ngneat/transloco"; @Component({ @@ -52,12 +51,14 @@ export class MetadataBuilderComponent implements OnInit { @Input() statementLimit = 0; @Input() availableFilterFields = allFields; @Output() update: EventEmitter = new EventEmitter(); + @Output() apply: EventEmitter = new EventEmitter(); private readonly cdRef = inject(ChangeDetectorRef); private readonly metadataService = inject(MetadataService); protected readonly utilityService = inject(UtilityService); protected readonly filterUtilityService = inject(FilterUtilitiesService); private readonly destroyRef = inject(DestroyRef); + protected readonly Breakpoint = Breakpoint; formGroup: FormGroup = new FormGroup({}); @@ -66,39 +67,30 @@ export class MetadataBuilderComponent implements OnInit { {value: FilterCombination.And, title: translate('metadata-builder.and')}, ]; - get Breakpoint() { return Breakpoint; } - - ngOnInit() { - if (this.filter === undefined) { - // I've left this in to see if it ever happens or not - console.error('No filter, creating one in metadata-builder') - // If there is no default preset, let's open with series name - this.filter = this.filterUtilityService.createSeriesV2Filter(); - this.filter.statements.push({ - value: '', - comparison: FilterComparison.Equal, - field: FilterField.SeriesName - }); - } - + console.log('[builder] ngOnInit'); this.formGroup.addControl('comparison', new FormControl(this.filter?.combination || FilterCombination.Or, [])); - this.formGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(values => { - this.filter.combination = parseInt(this.formGroup.get('comparison')?.value, 10); + this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap(values => { + this.filter.combination = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterCombination; + console.log('[builder] emitting filter from comparison change'); this.update.emit(this.filter); - })).subscribe() + })).subscribe(); } addFilter() { + console.log('[builder] Adding Filter') this.filter.statements = [this.metadataService.createDefaultFilterStatement(), ...this.filter.statements]; + this.cdRef.markForCheck(); } removeFilter(index: number) { + console.log('[builder] Removing filter') this.filter.statements = this.filter.statements.slice(0, index).concat(this.filter.statements.slice(index + 1)) this.cdRef.markForCheck(); } updateFilter(index: number, filterStmt: FilterStatement) { + console.log('[builder] updating filter: ', this.filter.statements); this.metadataService.updateFilter(this.filter.statements, index, filterStmt); this.update.emit(this.filter); } diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html index 2e3e6fcb6..36fa209f0 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html @@ -2,11 +2,9 @@
- - - +
@@ -22,7 +20,7 @@ - + diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts index 7f044daee..6cf3d9236 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts @@ -3,15 +3,23 @@ import { ChangeDetectorRef, Component, DestroyRef, - EventEmitter, - inject, + EventEmitter, inject, Input, OnInit, - Output + Output, } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FilterStatement} from '../../../_models/metadata/v2/filter-statement'; -import {BehaviorSubject, distinctUntilChanged, map, Observable, of, startWith, switchMap, tap} from 'rxjs'; +import { + BehaviorSubject, + distinctUntilChanged, filter, + map, + Observable, + of, + startWith, + switchMap, + tap +} from 'rxjs'; import {MetadataService} from 'src/app/_services/metadata.service'; import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter'; import {PersonRole} from 'src/app/_models/metadata/person'; @@ -32,7 +40,7 @@ enum PredicateType { Dropdown = 3, } -const StringFields = [FilterField.SeriesName, FilterField.Summary]; +const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath]; const NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating]; const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating, FilterField.Translators, FilterField.Characters, FilterField.Publisher, @@ -81,6 +89,7 @@ const DropdownComparisons = [FilterComparison.Equal, }) export class MetadataFilterRowComponent implements OnInit { + @Input() index: number = 0; // This is only for debugging /** * Slightly misleading as this is the initial state and will be updated on the filterStatement event emitter */ @@ -100,9 +109,7 @@ export class MetadataFilterRowComponent implements OnInit { dropdownOptions$ = of([]); loaded: boolean = false; - - - get PredicateType() { return PredicateType }; + protected readonly PredicateType = PredicateType; get MultipleDropdownAllowed() { const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison; @@ -113,38 +120,37 @@ export class MetadataFilterRowComponent implements OnInit { private readonly collectionTagService: CollectionTagService) {} ngOnInit() { + console.log('[ngOnInit] creating stmt (' + this.index + '): ', this.preset) this.formGroup.addControl('input', new FormControl(FilterField.SeriesName, [])); - this.formGroup.get('input')?.valueChanges.subscribe((val: string) => this.handleFieldChange(val)); + this.formGroup.get('input')?.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe((val: string) => this.handleFieldChange(val)); this.populateFromPreset(); + this.formGroup.get('filterValue')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(v => console.log('filterValue: ', v))).subscribe(); // Dropdown dynamic option selection this.dropdownOptions$ = this.formGroup.get('input')!.valueChanges.pipe( startWith(this.preset.value), - switchMap((_) => this.getDropdownObservable()), - tap((opts) => { - if (!this.formGroup.get('filterValue')?.value) { - this. populateFromPreset(); - return; - } - - if (this.MultipleDropdownAllowed) { - this.formGroup.get('filterValue')?.setValue((opts[0].value + '').split(',')); - } else { - this.formGroup.get('filterValue')?.setValue(opts[0].value); - } + distinctUntilChanged(), + filter(() => { + const inputVal = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; + return DropdownFields.includes(inputVal); }), + switchMap((_) => this.getDropdownObservable()), takeUntilDestroyed(this.destroyRef) ); - this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => { - this.filterStatement.emit({ + this.formGroup!.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => { + const stmt = { comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison, field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField, value: this.formGroup.get('filterValue')?.value! - }); + }; + + if (!stmt.value && stmt.field !== FilterField.SeriesName) return; + console.log('updating parent with new statement: ', stmt.value) + this.filterStatement.emit(stmt); }); this.loaded = true; @@ -152,30 +158,37 @@ export class MetadataFilterRowComponent implements OnInit { } + populateFromPreset() { + const val = this.preset.value === "undefined" || !this.preset.value ? '' : this.preset.value; + console.log('populating preset: ', val); + this.formGroup.get('comparison')?.patchValue(this.preset.comparison); + this.formGroup.get('input')?.patchValue(this.preset.field); + if (StringFields.includes(this.preset.field)) { - this.formGroup.get('filterValue')?.patchValue(this.preset.value); + this.formGroup.get('filterValue')?.patchValue(val); } else if (DropdownFields.includes(this.preset.field)) { - if (this.MultipleDropdownAllowed) { - this.formGroup.get('filterValue')?.setValue(this.preset.value.split(',')); + if (this.MultipleDropdownAllowed || val.includes(',')) { + console.log('setting multiple values: ', val.split(',').map(d => parseInt(d, 10))); + this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10))); } else { if (this.preset.field === FilterField.Languages) { - this.formGroup.get('filterValue')?.setValue(this.preset.value); + this.formGroup.get('filterValue')?.patchValue(val); } else { - this.formGroup.get('filterValue')?.setValue(parseInt(this.preset.value, 10)); + this.formGroup.get('filterValue')?.patchValue(parseInt(val, 10)); } } } else { - this.formGroup.get('filterValue')?.patchValue(parseInt(this.preset.value, 10)); + this.formGroup.get('filterValue')?.patchValue(parseInt(val, 10)); } - this.formGroup.get('comparison')?.patchValue(this.preset.comparison); - this.formGroup.get('input')?.setValue(this.preset.field); + this.cdRef.markForCheck(); } getDropdownObservable(): Observable { const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; + console.log('Getting dropdown observable: ', filterField); switch (filterField) { case FilterField.PublicationStatus: return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { @@ -224,41 +237,46 @@ export class MetadataFilterRowComponent implements OnInit { } getPersonOptions(role: PersonRole) { - return this.metadataService.getAllPeople().pipe(map(people => people.filter(p2 => p2.role === role).map(person => { + return this.metadataService.getAllPeopleByRole(role).pipe(map(people => people.map(person => { return {value: person.id, label: person.name} - }))) + }))); } handleFieldChange(val: string) { const inputVal = parseInt(val, 10) as FilterField; + console.log('HandleFieldChange: ', val); if (StringFields.includes(inputVal)) { this.validComparisons$.next(StringComparisons); - this.predicateType$.next(PredicateType.Text); - if (this.loaded) this.formGroup.get('filterValue')?.setValue(''); + + if (this.loaded) { + this.formGroup.get('filterValue')?.patchValue('',{emitEvent: false}); + console.log('setting filterValue to empty string', this.formGroup.get('filterValue')?.value) + } // BUG: undefined is getting set and the input value isn't updating and emitting to the backend return; } if (NumberFields.includes(inputVal)) { - let comps = [...NumberComparisons]; + const comps = [...NumberComparisons]; if (inputVal === FilterField.ReleaseYear) { comps.push(...DateComparisons); } this.validComparisons$.next(comps); this.predicateType$.next(PredicateType.Number); - if (this.loaded) this.formGroup.get('filterValue')?.setValue(''); + if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0); return; } if (DropdownFields.includes(inputVal)) { - let comps = [...DropdownComparisons]; + const comps = [...DropdownComparisons]; if (inputVal === FilterField.AgeRating) { comps.push(...NumberComparisons); } this.validComparisons$.next(comps); this.predicateType$.next(PredicateType.Dropdown); + return; } } diff --git a/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts b/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts index bbd96d334..d8e67b04d 100644 --- a/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts +++ b/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts @@ -58,6 +58,10 @@ export class FilterFieldPipe implements PipeTransform { return translate('filter-field-pipe.user-rating'); case FilterField.Writers: return translate('filter-field-pipe.writers'); + case FilterField.Path: + return translate('filter-field-pipe.path'); + case FilterField.FilePath: + return translate('filter-field-pipe.file-path'); default: throw new Error(`Invalid FilterField value: ${value}`); } 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 835af8492..95628e33e 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -1,17 +1,18 @@ -
+
-
+
{{t('filter-title')}}
+
@@ -19,9 +20,13 @@ -
+
- + +
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 542a19a38..22ac3f5c6 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -76,6 +76,7 @@ export class MetadataFilterComponent implements OnInit { allFilterFields = allFields; handleFilters(filter: SeriesFilterV2) { + console.log('[metadata-filter] updating filter'); this.filterV2 = filter; } @@ -86,6 +87,7 @@ export class MetadataFilterComponent implements OnInit { constructor(public toggleService: ToggleService) {} ngOnInit(): void { + console.log('[metadata-filter] ngOnInit') if (this.filterSettings === undefined) { this.filterSettings = new FilterSettings(); this.cdRef.markForCheck(); @@ -137,6 +139,7 @@ export class MetadataFilterComponent implements OnInit { loadFromPresetsAndSetup() { this.fullyLoaded = false; + console.log('[metadata-filter] loading from preset and setting up'); this.filterV2 = this.deepClone(this.filterSettings.presetsV2); this.sortGroup = new FormGroup({ @@ -145,6 +148,7 @@ export class MetadataFilterComponent implements OnInit { }); this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + console.log('[metadata-filter] sortGroup value change'); if (this.filterV2?.sortOptions === null) { this.filterV2.sortOptions = { isAscending: this.isAscendingSort, @@ -152,12 +156,11 @@ export class MetadataFilterComponent implements OnInit { }; } this.filterV2!.sortOptions!.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10); - this.filterV2!.limitTo = parseInt(this.sortGroup.get('limitTo')?.value, 10); + this.filterV2!.limitTo = Math.max(parseInt(this.sortGroup.get('limitTo')?.value || '0', 10), 0); this.cdRef.markForCheck(); }); this.fullyLoaded = true; - this.cdRef.markForCheck(); this.apply(); } @@ -173,6 +176,7 @@ export class MetadataFilterComponent implements OnInit { } this.filterV2!.sortOptions!.isAscending = this.isAscendingSort; + console.log('[metadata-filter] updated filter sort order') } clear() { @@ -181,7 +185,6 @@ export class MetadataFilterComponent implements OnInit { } apply() { - this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!}); if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 4176677b8..e3939739a 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1731,7 +1731,9 @@ "tags": "Tags", "translators": "Translators", "user-rating": "User Rating", - "writers": "Writers" + "writers": "Writers", + "path": "Path", + "file-path": "File Path" }, "filter-comparison-pipe": { diff --git a/openapi.json b/openapi.json index 98d40bc46..8efb4a059 100644 --- a/openapi.json +++ b/openapi.json @@ -3031,6 +3031,69 @@ } } }, + "/api/Metadata/people-by-role": { + "get": { + "tags": [ + "Metadata" + ], + "summary": "Fetches people from the instance by role", + "parameters": [ + { + "name": "role", + "in": "query", + "description": "role", + "schema": { + "enum": [ + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + } + } + } + } + } + }, "/api/Metadata/people": { "get": { "tags": [ @@ -8012,7 +8075,7 @@ } } }, - "/api/Series/all": { + "/api/Series/all-v2": { "post": { "tags": [ "Series" @@ -8100,6 +8163,95 @@ } } }, + "/api/Series/all": { + "post": { + "tags": [ + "Series" + ], + "summary": "Returns all series for the library. Obsolete, use all-v2", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "description": "If set to 0, will set as MaxInt", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "libraryId", + "in": "query", + "description": "", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/FilterDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/FilterDto" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesDto" + } + } + } + } + } + }, + "deprecated": true + } + }, "/api/Series/on-deck": { "post": { "tags": [ @@ -13610,7 +13762,9 @@ 20, 21, 22, - 23 + 23, + 24, + 25 ], "type": "integer", "description": "Represents the field which will dictate the value type and the Extension used for filtering",