mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-01 04:34:49 -04:00
Must Contains Filter (#2249)
* Removed docker-compose.yml as it's not used and may confuse users. * Added ability to delete single collections from card actions. Updated transloco library which fixes older iOS browsers not being able to load Kavita. * Added a Must Contains comparison which will make so all values must exist. * Fixed up multiselect dropdowns not reseting value when changing filter field
This commit is contained in:
parent
b5540e58e0
commit
072fadf1de
@ -32,7 +32,7 @@ public class CollectionController : BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return a list of all collection tags on the server
|
/// Return a list of all collection tags on the server for the logged in user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -130,7 +130,6 @@ public class CollectionController : BaseApiController
|
|||||||
{
|
{
|
||||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
|
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
|
||||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
|
||||||
|
|
||||||
if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove))
|
if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove))
|
||||||
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated"));
|
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated"));
|
||||||
@ -142,4 +141,29 @@ public class CollectionController : BaseApiController
|
|||||||
|
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the collection tag from all Series it was attached to
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<ActionResult> DeleteTag(int tagId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata);
|
||||||
|
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||||
|
|
||||||
|
if (await _collectionService.DeleteTag(tag))
|
||||||
|
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-deleted"));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,20 @@ public enum FilterComparison
|
|||||||
LessThan = 3,
|
LessThan = 3,
|
||||||
LessThanEqual = 4,
|
LessThanEqual = 4,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// value is within any of the series. This is inheritently an OR, even if combinator is an AND
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Only works with IList</remarks>
|
/// <remarks>Only works with IList</remarks>
|
||||||
Contains = 5,
|
Contains = 5,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// value is within All of the series. This is an AND, even if combinator ORs the different statements
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Only works with IList</remarks>
|
||||||
|
MustContains = 6,
|
||||||
|
/// <summary>
|
||||||
/// Performs a LIKE %value%
|
/// Performs a LIKE %value%
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Matches = 6,
|
Matches = 7,
|
||||||
NotContains = 7,
|
NotContains = 8,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not Equal to
|
/// Not Equal to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -976,14 +976,15 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries))
|
foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries))
|
||||||
{
|
{
|
||||||
|
var libIds = stmt.Value.Split(',').Select(int.Parse);
|
||||||
if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains)
|
if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains)
|
||||||
{
|
{
|
||||||
|
|
||||||
filterIncludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse));
|
filterIncludeLibs.AddRange(libIds);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
filterExcludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse));
|
filterExcludeLibs.AddRange(libIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
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;
|
||||||
@ -28,6 +26,8 @@ public static class SeriesFilter
|
|||||||
return queryable.Where(s => s.Metadata.Language.Equals(languages.First()));
|
return queryable.Where(s => s.Metadata.Language.Equals(languages.First()));
|
||||||
case FilterComparison.Contains:
|
case FilterComparison.Contains:
|
||||||
return queryable.Where(s => languages.Contains(s.Metadata.Language));
|
return queryable.Where(s => languages.Contains(s.Metadata.Language));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language)));
|
||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
return queryable.Where(s => !languages.Contains(s.Metadata.Language));
|
return queryable.Where(s => !languages.Contains(s.Metadata.Language));
|
||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
@ -78,6 +78,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
case FilterComparison.BeginsWith:
|
case FilterComparison.BeginsWith:
|
||||||
case FilterComparison.EndsWith:
|
case FilterComparison.EndsWith:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear");
|
throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
@ -112,6 +113,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.Rating");
|
throw new KavitaException($"{comparison} not applicable for Series.Rating");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
@ -149,6 +151,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.AgeRating");
|
throw new KavitaException($"{comparison} not applicable for Series.AgeRating");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
@ -182,6 +185,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime");
|
throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
@ -204,6 +208,7 @@ public static class SeriesFilter
|
|||||||
return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus));
|
return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus));
|
||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus);
|
return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus);
|
||||||
|
case FilterComparison.MustContains:
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -273,6 +278,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.ReadProgress");
|
throw new KavitaException($"{comparison} not applicable for Series.ReadProgress");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
@ -295,6 +301,15 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
return queryable.Where(s => s.Metadata.Tags.Any(t => !tags.Contains(t.Id)));
|
return queryable.Where(s => s.Metadata.Tags.Any(t => !tags.Contains(t.Id)));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
// Deconstruct and do a Union of a bunch of where statements since this doesn't translate
|
||||||
|
var queries = new List<IQueryable<Series>>()
|
||||||
|
{
|
||||||
|
queryable
|
||||||
|
};
|
||||||
|
queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId))));
|
||||||
|
|
||||||
|
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -325,6 +340,15 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
return queryable.Where(s => s.Metadata.People.Any(t => !people.Contains(t.Id)));
|
return queryable.Where(s => s.Metadata.People.Any(t => !people.Contains(t.Id)));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
// Deconstruct and do a Union of a bunch of where statements since this doesn't translate
|
||||||
|
var queries = new List<IQueryable<Series>>()
|
||||||
|
{
|
||||||
|
queryable
|
||||||
|
};
|
||||||
|
queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.Id == gId))));
|
||||||
|
|
||||||
|
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -355,6 +379,15 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id)));
|
return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id)));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
// Deconstruct and do a Union of a bunch of where statements since this doesn't translate
|
||||||
|
var queries = new List<IQueryable<Series>>()
|
||||||
|
{
|
||||||
|
queryable
|
||||||
|
};
|
||||||
|
queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId))));
|
||||||
|
|
||||||
|
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -385,6 +418,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
return queryable.Where(s => !formats.Contains(s.Format));
|
return queryable.Where(s => !formats.Contains(s.Format));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -407,7 +441,6 @@ public static class SeriesFilter
|
|||||||
{
|
{
|
||||||
if (!condition || collectionTags.Count == 0) return queryable;
|
if (!condition || collectionTags.Count == 0) return queryable;
|
||||||
|
|
||||||
//var first = collectionTags.First();
|
|
||||||
switch (comparison)
|
switch (comparison)
|
||||||
{
|
{
|
||||||
case FilterComparison.Equal:
|
case FilterComparison.Equal:
|
||||||
@ -416,6 +449,15 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
return queryable.Where(s => !s.Metadata.CollectionTags.Any(t => collectionTags.Contains(t.Id)));
|
return queryable.Where(s => !s.Metadata.CollectionTags.Any(t => collectionTags.Contains(t.Id)));
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
// Deconstruct and do a Union of a bunch of where statements since this doesn't translate
|
||||||
|
var queries = new List<IQueryable<Series>>()
|
||||||
|
{
|
||||||
|
queryable
|
||||||
|
};
|
||||||
|
queries.AddRange(collectionTags.Select(gId => queryable.Where(s => s.Metadata.CollectionTags.Any(p => p.Id == gId))));
|
||||||
|
|
||||||
|
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||||
case FilterComparison.GreaterThan:
|
case FilterComparison.GreaterThan:
|
||||||
case FilterComparison.GreaterThanEqual:
|
case FilterComparison.GreaterThanEqual:
|
||||||
case FilterComparison.LessThan:
|
case FilterComparison.LessThan:
|
||||||
@ -475,6 +517,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.Name");
|
throw new KavitaException($"{comparison} not applicable for Series.Name");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||||
@ -508,6 +551,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary");
|
throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||||
@ -543,6 +587,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||||
@ -618,6 +663,7 @@ public static class SeriesFilter
|
|||||||
case FilterComparison.IsAfter:
|
case FilterComparison.IsAfter:
|
||||||
case FilterComparison.IsInLast:
|
case FilterComparison.IsInLast:
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"file-missing": "File was not found in book",
|
"file-missing": "File was not found in book",
|
||||||
|
|
||||||
"collection-updated": "Collection updated successfully",
|
"collection-updated": "Collection updated successfully",
|
||||||
|
"collection-deleted": "Collection deleted",
|
||||||
"generic-error": "Something went wrong, please try again",
|
"generic-error": "Something went wrong, please try again",
|
||||||
"collection-doesnt-exist": "Collection does not exist",
|
"collection-doesnt-exist": "Collection does not exist",
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ namespace API.Services;
|
|||||||
public interface ICollectionTagService
|
public interface ICollectionTagService
|
||||||
{
|
{
|
||||||
Task<bool> TagExistsByName(string name);
|
Task<bool> TagExistsByName(string name);
|
||||||
|
Task<bool> DeleteTag(CollectionTag tag);
|
||||||
Task<bool> UpdateTag(CollectionTagDto dto);
|
Task<bool> UpdateTag(CollectionTagDto dto);
|
||||||
Task<bool> AddTagToSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
Task<bool> AddTagToSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||||
Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||||
@ -49,6 +50,12 @@ public class CollectionTagService : ICollectionTagService
|
|||||||
return await _unitOfWork.CollectionTagRepository.TagExists(name);
|
return await _unitOfWork.CollectionTagRepository.TagExists(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteTag(CollectionTag tag)
|
||||||
|
{
|
||||||
|
_unitOfWork.CollectionTagRepository.Remove(tag);
|
||||||
|
return await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateTag(CollectionTagDto dto)
|
public async Task<bool> UpdateTag(CollectionTagDto dto)
|
||||||
{
|
{
|
||||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
|
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
|
||||||
@ -130,6 +137,7 @@ public class CollectionTagService : ICollectionTagService
|
|||||||
public async Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds)
|
public async Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds)
|
||||||
{
|
{
|
||||||
if (tag == null) return false;
|
if (tag == null) return false;
|
||||||
|
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||||
foreach (var seriesIdToRemove in seriesIds)
|
foreach (var seriesIdToRemove in seriesIds)
|
||||||
{
|
{
|
||||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||||
|
8
UI/Web/package-lock.json
generated
8
UI/Web/package-lock.json
generated
@ -23,7 +23,7 @@
|
|||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.1",
|
||||||
"@microsoft/signalr": "^7.0.10",
|
"@microsoft/signalr": "^7.0.10",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||||
"@ngneat/transloco": "^5.0.6",
|
"@ngneat/transloco": "^5.0.7",
|
||||||
"@ngneat/transloco-locale": "^5.1.1",
|
"@ngneat/transloco-locale": "^5.1.1",
|
||||||
"@ngneat/transloco-persist-lang": "^5.0.0",
|
"@ngneat/transloco-persist-lang": "^5.0.0",
|
||||||
"@ngneat/transloco-persist-translations": "^5.0.0",
|
"@ngneat/transloco-persist-translations": "^5.0.0",
|
||||||
@ -3170,9 +3170,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ngneat/transloco": {
|
"node_modules/@ngneat/transloco": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-5.0.7.tgz",
|
||||||
"integrity": "sha512-pt0jiU0co0nT72bhodT9ervBvSgl1jVUrTbLsHwjtP3WoJZxfOmXN21j5MSA/GJFRkolceI8+yWqtG7jux+WDg==",
|
"integrity": "sha512-x1c2e+7cOYPPVFPgqGcN3R6d7f18a4sMHzxsCamcxS2w7vWXcEzWKZ8JcI1TdpxrM+RKuj2NRfEEcr1HjAI/4w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ngneat/transloco-utils": "^5.0.0",
|
"@ngneat/transloco-utils": "^5.0.0",
|
||||||
"flat": "5.0.2",
|
"flat": "5.0.2",
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.1",
|
||||||
"@microsoft/signalr": "^7.0.10",
|
"@microsoft/signalr": "^7.0.10",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||||
"@ngneat/transloco": "^5.0.6",
|
"@ngneat/transloco": "^5.0.7",
|
||||||
"@ngneat/transloco-locale": "^5.1.1",
|
"@ngneat/transloco-locale": "^5.1.1",
|
||||||
"@ngneat/transloco-persist-lang": "^5.0.0",
|
"@ngneat/transloco-persist-lang": "^5.0.0",
|
||||||
"@ngneat/transloco-persist-translations": "^5.0.0",
|
"@ngneat/transloco-persist-translations": "^5.0.0",
|
||||||
|
@ -9,11 +9,12 @@ export enum FilterComparison {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Only works with IList</remarks>
|
/// <remarks>Only works with IList</remarks>
|
||||||
Contains = 5,
|
Contains = 5,
|
||||||
|
MustContains = 6,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a LIKE %value%
|
/// Performs a LIKE %value%
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Matches = 6,
|
Matches = 7,
|
||||||
NotContains = 7,
|
NotContains = 8,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not Equal to
|
/// Not Equal to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -42,4 +43,4 @@ export enum FilterComparison {
|
|||||||
/// Is Date not between now and X seconds ago
|
/// Is Date not between now and X seconds ago
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IsNotInLast = 15,
|
IsNotInLast = 15,
|
||||||
}
|
}
|
||||||
|
@ -247,6 +247,14 @@ export class ActionFactoryService {
|
|||||||
requiresAdmin: true,
|
requiresAdmin: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: Action.Delete,
|
||||||
|
title: 'delete',
|
||||||
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false,
|
||||||
|
class: 'danger',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.seriesActions = [
|
this.seriesActions = [
|
||||||
|
@ -41,4 +41,8 @@ export class CollectionTagService {
|
|||||||
tagNameExists(name: string) {
|
tagNameExists(name: string) {
|
||||||
return this.httpClient.get<boolean>(this.baseUrl + 'collection/name-exists?name=' + name);
|
return this.httpClient.get<boolean>(this.baseUrl + 'collection/name-exists?name=' + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTag(tagId: number) {
|
||||||
|
return this.httpClient.delete<string>(this.baseUrl + 'collection?tagId=' + tagId, TextResonse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,8 +166,8 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasCustomSort() {
|
hasCustomSort() {
|
||||||
return this.filter.sortOptions?.sortField != SortField.SortName || !this.filter.sortOptions.isAscending
|
return this.filter?.sortOptions?.sortField != SortField.SortName || !this.filter?.sortOptions.isAscending
|
||||||
|| this.filterSettings.presetsV2?.sortOptions?.sortField != SortField.SortName || !this.filterSettings.presetsV2?.sortOptions?.isAscending;
|
|| this.filterSettings?.presetsV2?.sortOptions?.sortField != SortField.SortName || !this.filterSettings?.presetsV2?.sortOptions?.isAscending;
|
||||||
}
|
}
|
||||||
|
|
||||||
performAction(action: ActionItem<any>) {
|
performAction(action: ActionItem<any>) {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
|
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
|
||||||
[imageUrl]="imageSerivce.getCollectionCoverImage(item.id)"
|
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
||||||
(clicked)="loadCollection(item)"></app-card-item>
|
(clicked)="loadCollection(item)"></app-card-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -1,31 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, DestroyRef,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
inject,
|
inject,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import {Title} from '@angular/platform-browser';
|
||||||
import { Router } from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { map, of} from 'rxjs';
|
import {map, of} from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import {Observable} from 'rxjs/internal/Observable';
|
||||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
import {CollectionTag} from 'src/app/_models/collection-tag';
|
||||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
|
||||||
import { Tag } from 'src/app/_models/tag';
|
import {Tag} from 'src/app/_models/tag';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import {AccountService} from 'src/app/_services/account.service';
|
||||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
||||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
import {CollectionTagService} from 'src/app/_services/collection-tag.service';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import {ImageService} from 'src/app/_services/image.service';
|
||||||
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
import {JumpbarService} from 'src/app/_services/jumpbar.service';
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import { NgIf, AsyncPipe, DecimalPipe } from '@angular/common';
|
import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common';
|
||||||
import { CardItemComponent } from '../../../cards/card-item/card-item.component';
|
import {CardItemComponent} from '../../../cards/card-item/card-item.component';
|
||||||
import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component';
|
import {CardDetailLayoutComponent} from '../../../cards/card-detail-layout/card-detail-layout.component';
|
||||||
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
import {
|
||||||
|
SideNavCompanionBarComponent
|
||||||
|
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||||
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||||
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -49,11 +53,12 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly translocoService = inject(TranslocoService);
|
private readonly translocoService = inject(TranslocoService);
|
||||||
|
private readonly toastr = inject(ToastrService);
|
||||||
|
|
||||||
constructor(private collectionService: CollectionTagService, private router: Router,
|
constructor(private collectionService: CollectionTagService, private router: Router,
|
||||||
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
|
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
|
||||||
private titleService: Title, private jumpbarService: JumpbarService,
|
private titleService: Title, private jumpbarService: JumpbarService,
|
||||||
private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService,
|
private readonly cdRef: ChangeDetectorRef, public imageService: ImageService,
|
||||||
public accountService: AccountService) {
|
public accountService: AccountService) {
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
this.titleService.setTitle('Kavita - ' + this.translocoService.translate('all-collections.title'));
|
this.titleService.setTitle('Kavita - ' + this.translocoService.translate('all-collections.title'));
|
||||||
@ -87,6 +92,11 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
|
|
||||||
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
|
case(Action.Delete):
|
||||||
|
this.collectionService.deleteTag(collectionTag.id).subscribe(res => {
|
||||||
|
this.toastr.success(res);
|
||||||
|
});
|
||||||
|
break;
|
||||||
case(Action.Edit):
|
case(Action.Edit):
|
||||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
||||||
modalRef.componentInstance.tag = collectionTag;
|
modalRef.componentInstance.tag = collectionTag;
|
||||||
|
@ -3,23 +3,15 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
EventEmitter, inject,
|
EventEmitter,
|
||||||
|
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 {
|
import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs';
|
||||||
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';
|
||||||
@ -50,6 +42,16 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
|
|||||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DropdownFieldsWithoutMustContains = [
|
||||||
|
FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus
|
||||||
|
];
|
||||||
|
const DropdownFieldsThatIncludeNumberComparisons = [
|
||||||
|
FilterField.AgeRating
|
||||||
|
];
|
||||||
|
const NumberFieldsThatIncludeDateComparisons = [
|
||||||
|
FilterField.ReleaseYear
|
||||||
|
];
|
||||||
|
|
||||||
const StringComparisons = [FilterComparison.Equal,
|
const StringComparisons = [FilterComparison.Equal,
|
||||||
FilterComparison.NotEqual,
|
FilterComparison.NotEqual,
|
||||||
FilterComparison.BeginsWith,
|
FilterComparison.BeginsWith,
|
||||||
@ -65,7 +67,8 @@ const NumberComparisons = [FilterComparison.Equal,
|
|||||||
const DropdownComparisons = [FilterComparison.Equal,
|
const DropdownComparisons = [FilterComparison.Equal,
|
||||||
FilterComparison.NotEqual,
|
FilterComparison.NotEqual,
|
||||||
FilterComparison.Contains,
|
FilterComparison.Contains,
|
||||||
FilterComparison.NotContains];
|
FilterComparison.NotContains,
|
||||||
|
FilterComparison.MustContains];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-metadata-row-filter',
|
selector: 'app-metadata-row-filter',
|
||||||
@ -113,7 +116,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
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;
|
||||||
return comp === FilterComparison.Contains || comp === FilterComparison.NotContains;
|
return comp === FilterComparison.Contains || comp === FilterComparison.NotContains || comp === FilterComparison.MustContains;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly metadataService: MetadataService, private readonly libraryService: LibraryService,
|
constructor(private readonly metadataService: MetadataService, private readonly libraryService: LibraryService,
|
||||||
@ -148,6 +151,12 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
value: this.formGroup.get('filterValue')?.value!
|
value: this.formGroup.get('filterValue')?.value!
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Some ids can get through and be numbers, convert them to strings for the backend
|
||||||
|
if (typeof stmt.value === 'number' && !Number.isNaN(stmt.value)) {
|
||||||
|
stmt.value = stmt.value + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Trying to update parent with new stmt: ', stmt.value);
|
||||||
if (!stmt.value && stmt.field !== FilterField.SeriesName) return;
|
if (!stmt.value && stmt.field !== FilterField.SeriesName) return;
|
||||||
console.log('updating parent with new statement: ', stmt.value)
|
console.log('updating parent with new statement: ', stmt.value)
|
||||||
this.filterStatement.emit(stmt);
|
this.filterStatement.emit(stmt);
|
||||||
@ -188,7 +197,6 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
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 => {
|
||||||
@ -252,15 +260,15 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
this.predicateType$.next(PredicateType.Text);
|
this.predicateType$.next(PredicateType.Text);
|
||||||
|
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.formGroup.get('filterValue')?.patchValue('',{emitEvent: false});
|
this.formGroup.get('filterValue')?.patchValue('');
|
||||||
console.log('setting filterValue to empty string', this.formGroup.get('filterValue')?.value)
|
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)) {
|
||||||
const comps = [...NumberComparisons];
|
const comps = [...NumberComparisons];
|
||||||
if (inputVal === FilterField.ReleaseYear) {
|
if (NumberFieldsThatIncludeDateComparisons.includes(inputVal)) {
|
||||||
comps.push(...DateComparisons);
|
comps.push(...DateComparisons);
|
||||||
}
|
}
|
||||||
this.validComparisons$.next(comps);
|
this.validComparisons$.next(comps);
|
||||||
@ -270,12 +278,16 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DropdownFields.includes(inputVal)) {
|
if (DropdownFields.includes(inputVal)) {
|
||||||
const comps = [...DropdownComparisons];
|
let comps = [...DropdownComparisons];
|
||||||
if (inputVal === FilterField.AgeRating) {
|
if (DropdownFieldsThatIncludeNumberComparisons.includes(inputVal)) {
|
||||||
comps.push(...NumberComparisons);
|
comps.push(...NumberComparisons);
|
||||||
}
|
}
|
||||||
|
if (DropdownFieldsWithoutMustContains.includes(inputVal)) {
|
||||||
|
comps = comps.filter(c => c !== FilterComparison.MustContains);
|
||||||
|
}
|
||||||
this.validComparisons$.next(comps);
|
this.validComparisons$.next(comps);
|
||||||
this.predicateType$.next(PredicateType.Dropdown);
|
this.predicateType$.next(PredicateType.Dropdown);
|
||||||
|
if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ export class FilterComparisonPipe implements PipeTransform {
|
|||||||
return translate('filter-comparison-pipe.is-in-last');
|
return translate('filter-comparison-pipe.is-in-last');
|
||||||
case FilterComparison.IsNotInLast:
|
case FilterComparison.IsNotInLast:
|
||||||
return translate('filter-comparison-pipe.is-not-in-last');
|
return translate('filter-comparison-pipe.is-not-in-last');
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
return translate('filter-comparison-pipe.must-contains');
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid FilterComparison value: ${value}`);
|
throw new Error(`Invalid FilterComparison value: ${value}`);
|
||||||
}
|
}
|
||||||
|
@ -1751,7 +1751,8 @@
|
|||||||
"is-before": "Is before",
|
"is-before": "Is before",
|
||||||
"is-after": "Is after",
|
"is-after": "Is after",
|
||||||
"is-in-last": "Is in last",
|
"is-in-last": "Is in last",
|
||||||
"is-not-in-last": "Is not in last"
|
"is-not-in-last": "Is not in last",
|
||||||
|
"must-contains": "Must Contains"
|
||||||
},
|
},
|
||||||
|
|
||||||
"toasts": {
|
"toasts": {
|
||||||
|
@ -66,7 +66,3 @@
|
|||||||
/* hint */
|
/* hint */
|
||||||
--select2-hint-text-color: #888;
|
--select2-hint-text-color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
//.select2-selection__rendered {
|
|
||||||
// padding-top: 4px;
|
|
||||||
//}
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
kavita:
|
|
||||||
image: kizaing/kavita:latest
|
|
||||||
container_name: kavita
|
|
||||||
volumes:
|
|
||||||
- ./manga:/manga
|
|
||||||
- ./config:/kavita/config
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
#Uncomment if you want to implement healthchecks
|
|
||||||
#healthcheck:
|
|
||||||
# test: curl --fail http://localhost:5000 || exit 1
|
|
||||||
# interval: 300s
|
|
||||||
# retries: 3
|
|
||||||
# start_period: 30s
|
|
||||||
# timeout: 15s
|
|
29
openapi.json
29
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.7.14"
|
"version": "0.7.7.17"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -1268,7 +1268,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Collection"
|
"Collection"
|
||||||
],
|
],
|
||||||
"summary": "Return a list of all collection tags on the server",
|
"summary": "Return a list of all collection tags on the server for the logged in user.",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success",
|
"description": "Success",
|
||||||
@ -1300,6 +1300,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Collection"
|
||||||
|
],
|
||||||
|
"summary": "Removes the collection tag from all Series it was attached to",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tagId",
|
||||||
|
"in": "query",
|
||||||
|
"description": "",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/Collection/search": {
|
"/api/Collection/search": {
|
||||||
@ -13726,6 +13748,7 @@
|
|||||||
5,
|
5,
|
||||||
6,
|
6,
|
||||||
7,
|
7,
|
||||||
|
8,
|
||||||
9,
|
9,
|
||||||
10,
|
10,
|
||||||
11,
|
11,
|
||||||
@ -18400,4 +18423,4 @@
|
|||||||
"description": "Responsible for all things Want To Read"
|
"description": "Responsible for all things Want To Read"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user