Lots of Filtering Fixes & New Fields (#2244)

* Added an id for komf userscript to help it inject into Kavita's UI without relying on strings, given localization.

* Still working the filter fields, there is a bug with selecting an input and it setting undefined like crazy. Path is coded but not tested or validated.

* Stashing changed. Really not sure what's happening. I'm seeing 2 constructor calls for one row. I'm seeing a field change trigger 400 events. Values aren't getting set correctly on default string.

I've made a ton of changes, when resuming this work, look at the diff. All of this can be reset excluding the Path work.

* Lots of comments but the double instantiation is due to the mobile drawer. Added an ngIf which seems to work.

* Fixed dropdown options triggering a ton of looped calls. Default limitTo to 0 when user empties blank or negative.

* Removed a ton of UserId db calls from a ton of apis. Added a new API to allow UI to query a specific role to lessen load on UI.

* Optimized the code on new filtering to only load people by a given role. This should speed up heavily tagged libraries.

Commented out a bunch of code that's no longer used. Will be cleaned up later.

* Fixed support so that library filter can handle multiple selections.

* Fixed a bug when hitting enter in an input, the statement would be removed.

* Fixed multi-select not resuming from url correctly.

* Restored the series/all api for Tachiyomi to continue using until I'm motivated enough to update the extension.

* Fixed some resuming of state with dropdowns, not always setting values in correct order.

* Added FilePath Filter which lets a user search on individual files (slow, may need index)

* Added a full filepath for new filtering.
This commit is contained in:
Joe Milazzo 2023-08-29 16:03:19 -07:00 committed by GitHub
parent 69b5530a93
commit cd84913fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 576 additions and 238 deletions

View File

@ -87,8 +87,7 @@ public class DeviceController : BaseApiController
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<DeviceDto>>> GetDevices() public async Task<ActionResult<IEnumerable<DeviceDto>>> GetDevices()
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId()));
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(userId));
} }
[HttpPost("send-to")] [HttpPost("send-to")]
@ -100,7 +99,7 @@ public class DeviceController : BaseApiController
if (await _emailService.IsDefaultEmailService()) if (await _emailService.IsDefaultEmailService())
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); 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, await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"started"), userId); "started"), userId);
@ -134,7 +133,7 @@ public class DeviceController : BaseApiController
if (await _emailService.IsDefaultEmailService()) if (await _emailService.IsDefaultEmailService())
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); 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, await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"started"), userId); "started"), userId);

View File

@ -161,8 +161,7 @@ public class LibraryController : BaseApiController
[HttpGet("jump-bar")] [HttpGet("jump-bar")]
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId) public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, User.GetUserId()))
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access"));
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));

View File

@ -37,17 +37,28 @@ public class MetadataController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds) public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0) 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()));
} }
/// <summary>
/// Fetches people from the instance by role
/// </summary>
/// <param name="role">role</param>
/// <returns></returns>
[HttpGet("people-by-role")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})]
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(PersonRole? role)
{
return role.HasValue ?
Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) :
Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
}
/// <summary> /// <summary>
/// Fetches people from the instance /// Fetches people from the instance
@ -58,13 +69,12 @@ public class MetadataController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds) public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0) 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()));
} }
/// <summary> /// <summary>
@ -76,13 +86,12 @@ public class MetadataController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds) public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0) 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()));
} }
/// <summary> /// <summary>

View File

@ -39,8 +39,7 @@ public class ReadingListController : BaseApiController
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId) public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()));
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
} }
/// <summary> /// <summary>
@ -54,8 +53,7 @@ public class ReadingListController : BaseApiController
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams,
bool includePromoted = true, bool sortByLastModified = false) bool includePromoted = true, bool sortByLastModified = false)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(User.GetUserId(), includePromoted,
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
userParams, sortByLastModified); userParams, sortByLastModified);
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages); Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
@ -70,10 +68,8 @@ public class ReadingListController : BaseApiController
[HttpGet("lists-for-series")] [HttpGet("lists-for-series")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId) public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(User.GetUserId(),
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true); seriesId, true));
return Ok(items);
} }
/// <summary> /// <summary>

View File

@ -62,7 +62,7 @@ public class ReviewController : BaseApiController
} }
var cacheKey = CacheKey + seriesId; var cacheKey = CacheKey + seriesId;
IEnumerable<UserReviewDto> externalReviews; IList<UserReviewDto> externalReviews;
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey); var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
if (result.HasValue) if (result.HasValue)
@ -74,7 +74,6 @@ public class ReviewController : BaseApiController
var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList(); var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList();
externalReviews = SelectSpectrumOfReviews(reviews); externalReviews = SelectSpectrumOfReviews(reviews);
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10)); await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
_logger.LogDebug("Caching external reviews for {Key}", cacheKey); _logger.LogDebug("Caching external reviews for {Key}", cacheKey);
} }
@ -87,7 +86,7 @@ public class ReviewController : BaseApiController
return Ok(userRatings); return Ok(userRatings);
} }
private static IList<UserReviewDto> SelectSpectrumOfReviews(List<UserReviewDto> reviews) private static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
{ {
IList<UserReviewDto> externalReviews; IList<UserReviewDto> externalReviews;
var totalReviews = reviews.Count; var totalReviews = reviews.Count;

View File

@ -33,8 +33,7 @@ public class SearchController : BaseApiController
[HttpGet("series-for-mangafile")] [HttpGet("series-for-mangafile")]
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId) public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, User.GetUserId()));
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
} }
/// <summary> /// <summary>
@ -46,8 +45,7 @@ public class SearchController : BaseApiController
[HttpGet("series-for-chapter")] [HttpGet("series-for-chapter")]
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId) public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, User.GetUserId()));
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
} }
[HttpGet("search")] [HttpGet("search")]

View File

@ -67,7 +67,7 @@ public class SeriesController : BaseApiController
[Obsolete("use v2")] [Obsolete("use v2")]
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto) public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
@ -90,7 +90,7 @@ public class SeriesController : BaseApiController
[HttpPost("v2")] [HttpPost("v2")]
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto) public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto);
@ -114,8 +114,7 @@ public class SeriesController : BaseApiController
[HttpGet("{seriesId:int}")] [HttpGet("{seriesId:int}")]
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId) public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, User.GetUserId());
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
if (series == null) return NoContent(); if (series == null) return NoContent();
return Ok(series); return Ok(series);
} }
@ -150,15 +149,13 @@ public class SeriesController : BaseApiController
[HttpGet("volumes")] [HttpGet("volumes")]
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId) public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, User.GetUserId()));
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
} }
[HttpGet("volume")] [HttpGet("volume")]
public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId) public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId());
var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId);
if (vol == null) return NoContent(); if (vol == null) return NoContent();
return Ok(vol); return Ok(vol);
} }
@ -253,7 +250,7 @@ public class SeriesController : BaseApiController
[Obsolete("use recently-added-v2")] [Obsolete("use recently-added-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
@ -277,7 +274,7 @@ public class SeriesController : BaseApiController
[HttpPost("recently-added-v2")] [HttpPost("recently-added-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAddedV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAddedV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto);
@ -299,8 +296,7 @@ public class SeriesController : BaseApiController
[HttpPost("recently-updated-series")] [HttpPost("recently-updated-series")]
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters() public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(User.GetUserId(), 20));
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, 20));
} }
/// <summary> /// <summary>
@ -310,10 +306,10 @@ public class SeriesController : BaseApiController
/// <param name="userParams"></param> /// <param name="userParams"></param>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("all")] [HttpPost("all-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto);
@ -327,6 +323,31 @@ public class SeriesController : BaseApiController
return Ok(series); return Ok(series);
} }
/// <summary>
/// Returns all series for the library. Obsolete, use all-v2
/// </summary>
/// <param name="filterDto"></param>
/// <param name="userParams"></param>
/// <param name="libraryId"></param>
/// <returns></returns>
[HttpPost("all")]
[Obsolete("User all-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> 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);
}
/// <summary> /// <summary>
/// Fetches series that are on deck aka have progress on them. /// Fetches series that are on deck aka have progress on them.
/// </summary> /// </summary>
@ -337,7 +358,7 @@ public class SeriesController : BaseApiController
[HttpPost("on-deck")] [HttpPost("on-deck")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck([FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) public async Task<ActionResult<IEnumerable<SeriesDto>>> 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); var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, null);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList); await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
@ -419,25 +440,24 @@ public class SeriesController : BaseApiController
[HttpPost("metadata")] [HttpPost("metadata")]
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) public async Task<ActionResult> 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 _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}");
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}");
}
} }
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"));
} }
/// <summary> /// <summary>
@ -449,7 +469,7 @@ public class SeriesController : BaseApiController
[HttpGet("series-by-collection")] [HttpGet("series-by-collection")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var userId = User.GetUserId();
var series = var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
@ -472,8 +492,7 @@ public class SeriesController : BaseApiController
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{ {
if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload")); 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, User.GetUserId()));
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
} }
/// <summary> /// <summary>
@ -503,10 +522,9 @@ public class SeriesController : BaseApiController
[HttpGet("series-detail")] [HttpGet("series-detail")]
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId) public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
try try
{ {
return await _seriesService.GetSeriesDetail(seriesId, userId); return await _seriesService.GetSeriesDetail(seriesId, User.GetUserId());
} }
catch (KavitaException ex) catch (KavitaException ex)
{ {
@ -525,9 +543,7 @@ public class SeriesController : BaseApiController
[HttpGet("related")] [HttpGet("related")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
{ {
// Send back a custom DTO with each type or maybe sorted in some way return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(User.GetUserId(), seriesId, relation));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
} }
/// <summary> /// <summary>
@ -538,8 +554,7 @@ public class SeriesController : BaseApiController
[HttpGet("all-related")] [HttpGet("all-related")]
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId) public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _seriesService.GetRelatedSeries(User.GetUserId(), seriesId));
return Ok(await _seriesService.GetRelatedSeries(userId, seriesId));
} }

View File

@ -68,10 +68,9 @@ public class UsersController : BaseApiController
[HttpGet("has-reading-progress")] [HttpGet("has-reading-progress")]
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId) public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
{ {
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist")); 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")] [HttpGet("has-library-access")]

View File

@ -28,5 +28,13 @@ public enum FilterField
ReadProgress = 20, ReadProgress = 20,
Formats = 21, Formats = 21,
ReleaseYear = 22, ReleaseYear = 22,
ReadTime = 23 ReadTime = 23,
/// <summary>
/// Series Folder
/// </summary>
Path = 24,
/// <summary>
/// File path
/// </summary>
FilePath = 25
} }

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Extensions.QueryExtensions; using API.Extensions.QueryExtensions;
using AutoMapper; using AutoMapper;
@ -17,9 +18,11 @@ public interface IPersonRepository
void Remove(Person person); void Remove(Person person);
Task<IList<Person>> GetAllPeople(); Task<IList<Person>> GetAllPeople();
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId); Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false); Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId); Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
Task<int> GetCountAsync(); Task<int> GetCountAsync();
} }
public class PersonRepository : IPersonRepository public class PersonRepository : IPersonRepository
@ -94,4 +97,15 @@ public class PersonRepository : IPersonRepository
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider) .ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
} }
public async Task<IList<PersonDto>> 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<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
} }

View File

@ -840,7 +840,6 @@ public class SeriesRepository : ISeriesRepository
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext) private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext)
{ {
// NOTE: Why do we even have libraryId when the filter has the actual libraryIds? // 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 userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == 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!) .HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!)
.HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0) .HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0)
// This needs different treatment // TODO: This needs different treatment
.HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds) .HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds)
.WhereIf(onlyParentSeries, .WhereIf(onlyParentSeries,
@ -979,11 +978,12 @@ public class SeriesRepository : ISeriesRepository
{ {
if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains) if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains)
{ {
filterIncludeLibs.Add(int.Parse(stmt.Value));
filterIncludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse));
} }
else 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.Summary => query.HasSummary(true, statement.Comparison, (string) value),
FilterField.SeriesName => query.HasName(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, FilterField.PublicationStatus => query.HasPublicationStatus(true, statement.Comparison,
(IList<PublicationStatus>) value), (IList<PublicationStatus>) value),
FilterField.Languages => query.HasLanguage(true, statement.Comparison, (IList<string>) value), FilterField.Languages => query.HasLanguage(true, statement.Comparison, (IList<string>) value),

View File

@ -6,6 +6,7 @@ using System.Linq.Expressions;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common; using Kavita.Common;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -512,4 +513,116 @@ public static class SeriesFilter
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
} }
} }
public static IQueryable<Series> HasPath(this IQueryable<Series> 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<Series> HasFilePath(this IQueryable<Series> 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");
}
}
} }

View File

@ -13,6 +13,8 @@ public static class FilterFieldValueConverter
return field switch return field switch
{ {
FilterField.SeriesName => (value, typeof(string)), FilterField.SeriesName => (value, typeof(string)),
FilterField.Path => (value, typeof(string)),
FilterField.FilePath => (value, typeof(string)),
FilterField.ReleaseYear => (int.Parse(value), typeof(int)), FilterField.ReleaseYear => (int.Parse(value), typeof(int)),
FilterField.Languages => (value.Split(',').ToList(), typeof(IList<string>)), FilterField.Languages => (value.Split(',').ToList(), typeof(IList<string>)),
FilterField.PublicationStatus => (value.Split(',') FilterField.PublicationStatus => (value.Split(',')

View File

@ -37,7 +37,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lazysizes": "^5.3.2", "lazysizes": "^5.3.2",
"ng-circle-progress": "^1.7.1", "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-color-picker": "^14.0.0",
"ngx-extended-pdf-viewer": "^16.2.16", "ngx-extended-pdf-viewer": "^16.2.16",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
@ -10558,9 +10558,9 @@
} }
}, },
"node_modules/ng-select2-component": { "node_modules/ng-select2-component": {
"version": "13.0.2", "version": "13.0.6",
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.2.tgz", "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.6.tgz",
"integrity": "sha512-8Tms5p0V/0J0vCWOf2Vrk6tJlwbaf3D3As3iigcjRncYlfXN130agniBcZ007C3zK2KyLXJJRkEWzlCls8/TVQ==", "integrity": "sha512-CiAelglSz2aeYy0BiXRi32zc49Mq27+J1eDzTrXmf2o50MvNo3asS3NRVQcnSldo/zLcJafWCMueVfjVaV1etw==",
"dependencies": { "dependencies": {
"ngx-infinite-scroll": ">=16.0.0", "ngx-infinite-scroll": ">=16.0.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"

View File

@ -42,7 +42,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lazysizes": "^5.3.2", "lazysizes": "^5.3.2",
"ng-circle-progress": "^1.7.1", "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-color-picker": "^14.0.0",
"ngx-extended-pdf-viewer": "^16.2.16", "ngx-extended-pdf-viewer": "^16.2.16",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",

View File

@ -24,7 +24,9 @@ export enum FilterField
ReadProgress = 20, ReadProgress = 20,
Formats = 21, Formats = 21,
ReleaseYear = 22, ReleaseYear = 22,
ReadTime = 23 ReadTime = 23,
Path = 24,
FilePath = 25
} }
export const allFields = Object.keys(FilterField) export const allFields = Object.keys(FilterField)

View File

@ -8,7 +8,7 @@ import {AgeRating} from '../_models/metadata/age-rating';
import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto';
import {Language} from '../_models/metadata/language'; import {Language} from '../_models/metadata/language';
import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; 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 {Tag} from '../_models/tag';
import {TextResonse} from '../_types/text-response'; import {TextResonse} from '../_types/text-response';
import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison';
@ -33,44 +33,44 @@ export class MetadataService {
constructor(private httpClient: HttpClient, private router: Router) { } constructor(private httpClient: HttpClient, private router: Router) { }
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) { // applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
const dto: SeriesFilterV2 = { // const dto: SeriesFilterV2 = {
statements: [this.createDefaultFilterStatement(filter, comparison, value + '')], // statements: [this.createDefaultFilterStatement(filter, comparison, value + '')],
combination: FilterCombination.Or, // combination: FilterCombination.Or,
limitTo: 0 // limitTo: 0
}; // };
// // //
// console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto)); // // console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto));
// this.router.navigateByUrl(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<string>(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 // getFilter(filterName: string) {
this.httpClient.post<string>(this.baseUrl + 'filter/create-temp', dto, TextResonse).pipe(map(name => { // return this.httpClient.get<SeriesFilterV2>(this.baseUrl + 'filter?name=' + filterName);
dto.name = name; // }
}), switchMap((_) => {
let params: any = {};
params['filterName'] = dto.name;
return this.router.navigate(page, {queryParams: params});
})).subscribe();
} // getAgeRating(ageRating: AgeRating) {
// if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
getFilter(filterName: string) { // return of(this.ageRatingTypes[ageRating]);
return this.httpClient.get<SeriesFilterV2>(this.baseUrl + 'filter?name=' + filterName); // }
} // return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => {
// if (this.ageRatingTypes === undefined) {
getAgeRating(ageRating: AgeRating) { // this.ageRatingTypes = {};
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) { // }
return of(this.ageRatingTypes[ageRating]); //
} // this.ageRatingTypes[ageRating] = ratingString;
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => { // return this.ageRatingTypes[ageRating];
if (this.ageRatingTypes === undefined) { // }));
this.ageRatingTypes = {}; // }
}
this.ageRatingTypes[ageRating] = ratingString;
return this.ageRatingTypes[ageRating];
}));
}
getAllAgeRatings(libraries?: Array<number>) { getAllAgeRatings(libraries?: Array<number>) {
let method = 'metadata/age-ratings' let method = 'metadata/age-ratings'
@ -132,10 +132,14 @@ export class MetadataService {
return this.httpClient.get<Array<Person>>(this.baseUrl + method); return this.httpClient.get<Array<Person>>(this.baseUrl + method);
} }
getChapterSummary(chapterId: number) { getAllPeopleByRole(role: PersonRole) {
return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse); return this.httpClient.get<Array<Person>>(this.baseUrl + 'metadata/people-by-role?role=' + role);
} }
// getChapterSummary(chapterId: number) {
// return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse);
// }
createDefaultFilterDto(): SeriesFilterV2 { createDefaultFilterDto(): SeriesFilterV2 {
return { return {
statements: [] as FilterStatement[], statements: [] as FilterStatement[],
@ -159,6 +163,6 @@ export class MetadataService {
updateFilter(arr: Array<FilterStatement>, index: number, filterStmt: FilterStatement) { updateFilter(arr: Array<FilterStatement>, index: number, filterStmt: FilterStatement) {
arr[index].comparison = filterStmt.comparison; arr[index].comparison = filterStmt.comparison;
arr[index].field = filterStmt.field; arr[index].field = filterStmt.field;
arr[index].value = filterStmt.value + ''; arr[index].value = filterStmt.value ? filterStmt.value + '' : '';
} }
} }

View File

@ -39,7 +39,7 @@ export class SeriesService {
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
const data = filter || {}; const data = filter || {};
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe( return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all-v2', data, {observe: 'response', params}).pipe(
map((response: any) => { map((response: any) => {
return this.utilityService.createPaginatedResult(response, this.paginatedResults); return this.utilityService.createPaginatedResult(response, this.paginatedResults);
}) })

View File

@ -115,12 +115,14 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
} }
ngOnInit(): void { ngOnInit(): void {
console.log('[card-detail-layout] ngOnInit')
if (this.trackByIdentity === undefined) { if (this.trackByIdentity === undefined) {
this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
} }
if (this.filterSettings === undefined) { if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings(); this.filterSettings = new FilterSettings();
console.log('[card-detail-layout] creating blank FilterSettings');
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -178,6 +180,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
this.applyFilter.emit(event); this.applyFilter.emit(event);
this.updateApplied++; this.updateApplied++;
this.filter = event.filterV2; this.filter = event.filterV2;
console.log('[card-detail-layout] apply filter')
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -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, constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,

View File

@ -1,9 +1,8 @@
<ng-container *transloco="let t; read: 'metadata-builder'"> <ng-container *transloco="let t; read: 'metadata-builder'">
<ng-container *ngIf="filter"> <ng-container *ngIf="filter">
<form [formGroup]="formGroup">
<ng-container *ngIf="utilityService.getActiveBreakpoint() === Breakpoint.Desktop; else mobileView"> <ng-container *ngIf="utilityService.getActiveBreakpoint() === Breakpoint.Desktop; else mobileView">
<div class="container-fluid"> <div class="container-fluid">
<form [formGroup]="formGroup">
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-2"> <div class="col-md-2">
<select class="form-select" formControlName="comparison"> <select class="form-select" formControlName="comparison">
@ -12,33 +11,30 @@
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<button class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')" [disabled]="statementLimit === -1 || (statementLimit > 0 && filter.statements.length >= statementLimit)"> <button type="button" class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')" [disabled]="statementLimit === -1 || (statementLimit > 0 && filter.statements.length >= statementLimit)">
<i class="fa fa-solid fa-plus" aria-hidden="true"></i> <i class="fa fa-solid fa-plus" aria-hidden="true"></i>
<span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}</span> <span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}</span>
</button> </button>
</div> </div>
</div> </div>
</form> <div class="row mb-2" *ngFor="let filterStmt of filter.statements; let i = index">
<div class="row mb-2" *ngFor="let filterStmt of filter.statements; let i = index"> <div class="col-md-10">
<div class="col-md-10"> <app-metadata-row-filter [index]="i + 100" [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
<app-metadata-row-filter [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)"> <div class="col-md-1 ms-2">
<div class="col-md-1 ms-2"> <button type="button" class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule', {num: i})" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule', {num: i})" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1"> <i class="fa-solid fa-minus" aria-hidden="true"></i>
<i class="fa-solid fa-minus" aria-hidden="true"></i> <span class="visually-hidden">{{t('remove-rule', {num: i})}}</span>
<span class="visually-hidden">{{t('remove-rule', {num: i})}}</span> </button>
</button> </div>
</div> </app-metadata-row-filter>
</app-metadata-row-filter> </div>
</div> </div>
</div> </div>
</div> </ng-container>
</ng-container>
<ng-template #mobileView> <ng-template #mobileView>
<!-- TODO: Robbie please help me style this drawer only view --> <div class="container-fluid">
<div class="container-fluid">
<form [formGroup]="formGroup">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-2 col-10"> <div class="col-md-2 col-10">
<select class="form-select" formControlName="comparison"> <select class="form-select" formControlName="comparison">
@ -53,22 +49,21 @@
</button> </button>
</div> </div>
</div> </div>
</form> <div class="row mb-3" *ngFor="let filterStmt of filter.statements; let i = index">
<div class="row mb-3" *ngFor="let filterStmt of filter.statements; let i = index"> <div class="col-md-12">
<div class="col-md-12"> <app-metadata-row-filter [index]="i" [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
<app-metadata-row-filter [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)"> <div class="col-md-1 ms-2 col-1">
<div class="col-md-1 ms-2 col-1"> <button type="button" class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule')" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule')" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1"> <i class="fa-solid fa-minus" aria-hidden="true"></i>
<i class="fa-solid fa-minus" aria-hidden="true"></i> <span class="visually-hidden">{{t('remove-rule')}}</span>
<span class="visually-hidden">{{t('remove-rule')}}</span> </button>
</button> </div>
</div> </app-metadata-row-filter>
</app-metadata-row-filter> </div>
</div> </div>
</div> </div>
</div> </ng-template>
</ng-template> </form>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@ -19,10 +19,9 @@ import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {FilterCombination} from "../../../_models/metadata/v2/filter-combination"; import {FilterCombination} from "../../../_models/metadata/v2/filter-combination";
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {allFields} from "../../../_models/metadata/v2/filter-field";
import {allFields, FilterField} from "../../../_models/metadata/v2/filter-field";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {tap} from "rxjs/operators"; import {distinctUntilChanged, tap} from "rxjs/operators";
import {translate, TranslocoDirective} from "@ngneat/transloco"; import {translate, TranslocoDirective} from "@ngneat/transloco";
@Component({ @Component({
@ -52,12 +51,14 @@ export class MetadataBuilderComponent implements OnInit {
@Input() statementLimit = 0; @Input() statementLimit = 0;
@Input() availableFilterFields = allFields; @Input() availableFilterFields = allFields;
@Output() update: EventEmitter<SeriesFilterV2> = new EventEmitter<SeriesFilterV2>(); @Output() update: EventEmitter<SeriesFilterV2> = new EventEmitter<SeriesFilterV2>();
@Output() apply: EventEmitter<void> = new EventEmitter<void>();
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly metadataService = inject(MetadataService); private readonly metadataService = inject(MetadataService);
protected readonly utilityService = inject(UtilityService); protected readonly utilityService = inject(UtilityService);
protected readonly filterUtilityService = inject(FilterUtilitiesService); protected readonly filterUtilityService = inject(FilterUtilitiesService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
protected readonly Breakpoint = Breakpoint;
formGroup: FormGroup = new FormGroup({}); formGroup: FormGroup = new FormGroup({});
@ -66,39 +67,30 @@ export class MetadataBuilderComponent implements OnInit {
{value: FilterCombination.And, title: translate('metadata-builder.and')}, {value: FilterCombination.And, title: translate('metadata-builder.and')},
]; ];
get Breakpoint() { return Breakpoint; }
ngOnInit() { ngOnInit() {
if (this.filter === undefined) { console.log('[builder] ngOnInit');
// 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
});
}
this.formGroup.addControl('comparison', new FormControl<FilterCombination>(this.filter?.combination || FilterCombination.Or, [])); this.formGroup.addControl('comparison', new FormControl<FilterCombination>(this.filter?.combination || FilterCombination.Or, []));
this.formGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(values => { this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap(values => {
this.filter.combination = parseInt(this.formGroup.get('comparison')?.value, 10); 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); this.update.emit(this.filter);
})).subscribe() })).subscribe();
} }
addFilter() { addFilter() {
console.log('[builder] Adding Filter')
this.filter.statements = [this.metadataService.createDefaultFilterStatement(), ...this.filter.statements]; this.filter.statements = [this.metadataService.createDefaultFilterStatement(), ...this.filter.statements];
this.cdRef.markForCheck();
} }
removeFilter(index: number) { 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.filter.statements = this.filter.statements.slice(0, index).concat(this.filter.statements.slice(index + 1))
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
updateFilter(index: number, filterStmt: FilterStatement) { updateFilter(index: number, filterStmt: FilterStatement) {
console.log('[builder] updating filter: ', this.filter.statements);
this.metadataService.updateFilter(this.filter.statements, index, filterStmt); this.metadataService.updateFilter(this.filter.statements, index, filterStmt);
this.update.emit(this.filter); this.update.emit(this.filter);
} }

View File

@ -2,11 +2,9 @@
<form [formGroup]="formGroup"> <form [formGroup]="formGroup">
<div class="row g-0"> <div class="row g-0">
<div class="col-md-3 me-2 col-10 mb-2"> <div class="col-md-3 me-2 col-10 mb-2">
<ng-container *ngIf="formGroup.get('input') as control"> <select class="form-select me-2" formControlName="input">
<select class="form-select me-2" formControlName="input"> <option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option> </select>
</select>
</ng-container>
</div> </div>
<div class="col-md-2 me-2 col-10 mb-2"> <div class="col-md-2 me-2 col-10 mb-2">
@ -22,7 +20,7 @@
<input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue"> <input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue">
</ng-container> </ng-container>
<ng-container *ngSwitchCase="PredicateType.Number"> <ng-container *ngSwitchCase="PredicateType.Number">
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue"> <input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
</ng-container> </ng-container>
<ng-container *ngSwitchCase="PredicateType.Dropdown"> <ng-container *ngSwitchCase="PredicateType.Dropdown">
<ng-container *ngIf="dropdownOptions$ | async as opts"> <ng-container *ngIf="dropdownOptions$ | async as opts">

View File

@ -3,15 +3,23 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
EventEmitter, EventEmitter, inject,
inject,
Input, Input,
OnInit, OnInit,
Output Output,
} from '@angular/core'; } from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {FilterStatement} from '../../../_models/metadata/v2/filter-statement'; 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 {MetadataService} from 'src/app/_services/metadata.service';
import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter'; import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter';
import {PersonRole} from 'src/app/_models/metadata/person'; import {PersonRole} from 'src/app/_models/metadata/person';
@ -32,7 +40,7 @@ enum PredicateType {
Dropdown = 3, 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 NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating];
const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating, const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating,
FilterField.Translators, FilterField.Characters, FilterField.Publisher, FilterField.Translators, FilterField.Characters, FilterField.Publisher,
@ -81,6 +89,7 @@ const DropdownComparisons = [FilterComparison.Equal,
}) })
export class MetadataFilterRowComponent implements OnInit { 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 * 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<Select2Option[]>([]); dropdownOptions$ = of<Select2Option[]>([]);
loaded: boolean = false; loaded: boolean = false;
protected readonly PredicateType = PredicateType;
get PredicateType() { return PredicateType };
get MultipleDropdownAllowed() { get MultipleDropdownAllowed() {
const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison; const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison;
@ -113,38 +120,37 @@ export class MetadataFilterRowComponent implements OnInit {
private readonly collectionTagService: CollectionTagService) {} private readonly collectionTagService: CollectionTagService) {}
ngOnInit() { ngOnInit() {
console.log('[ngOnInit] creating stmt (' + this.index + '): ', this.preset)
this.formGroup.addControl('input', new FormControl<FilterField>(FilterField.SeriesName, [])); this.formGroup.addControl('input', new FormControl<FilterField>(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.populateFromPreset();
this.formGroup.get('filterValue')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(v => console.log('filterValue: ', v))).subscribe();
// Dropdown dynamic option selection // Dropdown dynamic option selection
this.dropdownOptions$ = this.formGroup.get('input')!.valueChanges.pipe( this.dropdownOptions$ = this.formGroup.get('input')!.valueChanges.pipe(
startWith(this.preset.value), startWith(this.preset.value),
switchMap((_) => this.getDropdownObservable()), distinctUntilChanged(),
tap((opts) => { filter(() => {
if (!this.formGroup.get('filterValue')?.value) { const inputVal = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
this. populateFromPreset(); return DropdownFields.includes(inputVal);
return;
}
if (this.MultipleDropdownAllowed) {
this.formGroup.get('filterValue')?.setValue((opts[0].value + '').split(','));
} else {
this.formGroup.get('filterValue')?.setValue(opts[0].value);
}
}), }),
switchMap((_) => this.getDropdownObservable()),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
); );
this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => { this.formGroup!.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
this.filterStatement.emit({ const stmt = {
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison, comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField, field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField,
value: this.formGroup.get('filterValue')?.value! 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; this.loaded = true;
@ -152,30 +158,37 @@ export class MetadataFilterRowComponent implements OnInit {
} }
populateFromPreset() { 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)) { 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)) { } else if (DropdownFields.includes(this.preset.field)) {
if (this.MultipleDropdownAllowed) { if (this.MultipleDropdownAllowed || val.includes(',')) {
this.formGroup.get('filterValue')?.setValue(this.preset.value.split(',')); 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 { } else {
if (this.preset.field === FilterField.Languages) { if (this.preset.field === FilterField.Languages) {
this.formGroup.get('filterValue')?.setValue(this.preset.value); this.formGroup.get('filterValue')?.patchValue(val);
} else { } else {
this.formGroup.get('filterValue')?.setValue(parseInt(this.preset.value, 10)); this.formGroup.get('filterValue')?.patchValue(parseInt(val, 10));
} }
} }
} else { } 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(); this.cdRef.markForCheck();
} }
getDropdownObservable(): Observable<Select2Option[]> { getDropdownObservable(): Observable<Select2Option[]> {
const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
console.log('Getting dropdown observable: ', filterField);
switch (filterField) { switch (filterField) {
case FilterField.PublicationStatus: case FilterField.PublicationStatus:
return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => {
@ -224,41 +237,46 @@ export class MetadataFilterRowComponent implements OnInit {
} }
getPersonOptions(role: PersonRole) { 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} return {value: person.id, label: person.name}
}))) })));
} }
handleFieldChange(val: string) { handleFieldChange(val: string) {
const inputVal = parseInt(val, 10) as FilterField; const inputVal = parseInt(val, 10) as FilterField;
console.log('HandleFieldChange: ', val);
if (StringFields.includes(inputVal)) { if (StringFields.includes(inputVal)) {
this.validComparisons$.next(StringComparisons); this.validComparisons$.next(StringComparisons);
this.predicateType$.next(PredicateType.Text); 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; return;
} }
if (NumberFields.includes(inputVal)) { if (NumberFields.includes(inputVal)) {
let comps = [...NumberComparisons]; const comps = [...NumberComparisons];
if (inputVal === FilterField.ReleaseYear) { if (inputVal === FilterField.ReleaseYear) {
comps.push(...DateComparisons); comps.push(...DateComparisons);
} }
this.validComparisons$.next(comps); this.validComparisons$.next(comps);
this.predicateType$.next(PredicateType.Number); this.predicateType$.next(PredicateType.Number);
if (this.loaded) this.formGroup.get('filterValue')?.setValue(''); if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0);
return; return;
} }
if (DropdownFields.includes(inputVal)) { if (DropdownFields.includes(inputVal)) {
let comps = [...DropdownComparisons]; const comps = [...DropdownComparisons];
if (inputVal === FilterField.AgeRating) { if (inputVal === FilterField.AgeRating) {
comps.push(...NumberComparisons); comps.push(...NumberComparisons);
} }
this.validComparisons$.next(comps); this.validComparisons$.next(comps);
this.predicateType$.next(PredicateType.Dropdown); this.predicateType$.next(PredicateType.Dropdown);
return;
} }
} }

View File

@ -58,6 +58,10 @@ export class FilterFieldPipe implements PipeTransform {
return translate('filter-field-pipe.user-rating'); return translate('filter-field-pipe.user-rating');
case FilterField.Writers: case FilterField.Writers:
return translate('filter-field-pipe.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: default:
throw new Error(`Invalid FilterField value: ${value}`); throw new Error(`Invalid FilterField value: ${value}`);
} }

View File

@ -1,17 +1,18 @@
<ng-container *transloco="let t; read: 'metadata-filter'"> <ng-container *transloco="let t; read: 'metadata-filter'">
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen"> <ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
<div class="phone-hidden"> <div class="phone-hidden" *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet">
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)"> <div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container> <ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div> </div>
</div> </div>
<div class="not-phone-hidden"> <div class="not-phone-hidden" *ngIf="utilityService.getActiveBreakpoint() < Breakpoint.Desktop">
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)"> <app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)">
<h5 header> <h5 header>
{{t('filter-title')}} {{t('filter-title')}}
</h5> </h5>
<div body class="drawer-body"> <div body class="drawer-body">
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
<ng-container [ngTemplateOutlet]="filterSection"></ng-container> <ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div> </div>
</app-drawer> </app-drawer>
@ -19,9 +20,13 @@
</ng-container> </ng-container>
<ng-template #filterSection> <ng-template #filterSection>
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded"> <div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded && filterV2">
<div class="row justify-content-center g-0"> <div class="row justify-content-center g-0">
<app-metadata-builder [filter]="filterV2!" [availableFilterFields]="allFilterFields" (update)="handleFilters($event)" [statementLimit]="filterSettings.statementLimit"></app-metadata-builder> <app-metadata-builder [filter]="filterV2"
[availableFilterFields]="allFilterFields"
[statementLimit]="filterSettings.statementLimit"
(update)="handleFilters($event)">
</app-metadata-builder>
</div> </div>
<form [formGroup]="sortGroup" class="container-fluid"> <form [formGroup]="sortGroup" class="container-fluid">
<div class="row mb-3"> <div class="row mb-3">

View File

@ -76,6 +76,7 @@ export class MetadataFilterComponent implements OnInit {
allFilterFields = allFields; allFilterFields = allFields;
handleFilters(filter: SeriesFilterV2) { handleFilters(filter: SeriesFilterV2) {
console.log('[metadata-filter] updating filter');
this.filterV2 = filter; this.filterV2 = filter;
} }
@ -86,6 +87,7 @@ export class MetadataFilterComponent implements OnInit {
constructor(public toggleService: ToggleService) {} constructor(public toggleService: ToggleService) {}
ngOnInit(): void { ngOnInit(): void {
console.log('[metadata-filter] ngOnInit')
if (this.filterSettings === undefined) { if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings(); this.filterSettings = new FilterSettings();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -137,6 +139,7 @@ export class MetadataFilterComponent implements OnInit {
loadFromPresetsAndSetup() { loadFromPresetsAndSetup() {
this.fullyLoaded = false; this.fullyLoaded = false;
console.log('[metadata-filter] loading from preset and setting up');
this.filterV2 = this.deepClone(this.filterSettings.presetsV2); this.filterV2 = this.deepClone(this.filterSettings.presetsV2);
this.sortGroup = new FormGroup({ this.sortGroup = new FormGroup({
@ -145,6 +148,7 @@ export class MetadataFilterComponent implements OnInit {
}); });
this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
console.log('[metadata-filter] sortGroup value change');
if (this.filterV2?.sortOptions === null) { if (this.filterV2?.sortOptions === null) {
this.filterV2.sortOptions = { this.filterV2.sortOptions = {
isAscending: this.isAscendingSort, 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!.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.cdRef.markForCheck();
}); });
this.fullyLoaded = true; this.fullyLoaded = true;
this.cdRef.markForCheck();
this.apply(); this.apply();
} }
@ -173,6 +176,7 @@ export class MetadataFilterComponent implements OnInit {
} }
this.filterV2!.sortOptions!.isAscending = this.isAscendingSort; this.filterV2!.sortOptions!.isAscending = this.isAscendingSort;
console.log('[metadata-filter] updated filter sort order')
} }
clear() { clear() {
@ -181,7 +185,6 @@ export class MetadataFilterComponent implements OnInit {
} }
apply() { apply() {
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!}); this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!});
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) { if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {

View File

@ -1731,7 +1731,9 @@
"tags": "Tags", "tags": "Tags",
"translators": "Translators", "translators": "Translators",
"user-rating": "User Rating", "user-rating": "User Rating",
"writers": "Writers" "writers": "Writers",
"path": "Path",
"file-path": "File Path"
}, },
"filter-comparison-pipe": { "filter-comparison-pipe": {

View File

@ -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": { "/api/Metadata/people": {
"get": { "get": {
"tags": [ "tags": [
@ -8012,7 +8075,7 @@
} }
} }
}, },
"/api/Series/all": { "/api/Series/all-v2": {
"post": { "post": {
"tags": [ "tags": [
"Series" "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": { "/api/Series/on-deck": {
"post": { "post": {
"tags": [ "tags": [
@ -13610,7 +13762,9 @@
20, 20,
21, 21,
22, 22,
23 23,
24,
25
], ],
"type": "integer", "type": "integer",
"description": "Represents the field which will dictate the value type and the Extension used for filtering", "description": "Represents the field which will dictate the value type and the Extension used for filtering",