mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Smart Filter Polish & New Filters (#2283)
This commit is contained in:
parent
0d8c081093
commit
45f6fb67d4
@ -40,6 +40,10 @@ public enum FilterField
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// On Want To Read or Not
|
/// On Want To Read or Not
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WantToRead = 26
|
WantToRead = 26,
|
||||||
|
/// <summary>
|
||||||
|
/// Last time User Read
|
||||||
|
/// </summary>
|
||||||
|
ReadingDate = 27
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -868,8 +868,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres)
|
.HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres)
|
||||||
.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)
|
||||||
|
|
||||||
// TODO: This needs different treatment
|
|
||||||
.HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds)
|
.HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds)
|
||||||
|
|
||||||
.WhereIf(onlyParentSeries,
|
.WhereIf(onlyParentSeries,
|
||||||
@ -917,6 +915,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderBy(s => s.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderBy(s => s.Metadata.ReleaseYear),
|
||||||
|
SortField.ReadProgress => query.OrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -930,6 +929,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderByDescending(s => s.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderByDescending(s => s.Metadata.ReleaseYear),
|
||||||
|
SortField.ReadProgress => query.OrderByDescending(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1089,6 +1089,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList<MangaFormat>) value),
|
FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList<MangaFormat>) value),
|
||||||
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),
|
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),
|
||||||
FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value),
|
FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value),
|
||||||
|
FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId),
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -439,8 +439,8 @@ public class UserRepository : IUserRepository
|
|||||||
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id,
|
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id,
|
||||||
(bookmark, series) => new BookmarkSeriesPair()
|
(bookmark, series) => new BookmarkSeriesPair()
|
||||||
{
|
{
|
||||||
bookmark = bookmark,
|
Bookmark = bookmark,
|
||||||
series = series
|
Series = series
|
||||||
});
|
});
|
||||||
|
|
||||||
var filterStatement = filter.Statements.FirstOrDefault(f => f.Field == FilterField.SeriesName);
|
var filterStatement = filter.Statements.FirstOrDefault(f => f.Field == FilterField.SeriesName);
|
||||||
@ -457,34 +457,34 @@ public class UserRepository : IUserRepository
|
|||||||
switch (filterStatement.Comparison)
|
switch (filterStatement.Comparison)
|
||||||
{
|
{
|
||||||
case FilterComparison.Equal:
|
case FilterComparison.Equal:
|
||||||
filterSeriesQuery = filterSeriesQuery.Where(s => s.series.Name.Equals(queryString)
|
filterSeriesQuery = filterSeriesQuery.Where(s => s.Series.Name.Equals(queryString)
|
||||||
|| s.series.OriginalName.Equals(queryString)
|
|| s.Series.OriginalName.Equals(queryString)
|
||||||
|| s.series.LocalizedName.Equals(queryString)
|
|| s.Series.LocalizedName.Equals(queryString)
|
||||||
|| s.series.SortName.Equals(queryString));
|
|| s.Series.SortName.Equals(queryString));
|
||||||
break;
|
break;
|
||||||
case FilterComparison.BeginsWith:
|
case FilterComparison.BeginsWith:
|
||||||
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"{queryString}%")
|
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"{queryString}%")
|
||||||
||EF.Functions.Like(s.series.OriginalName, $"{queryString}%")
|
||EF.Functions.Like(s.Series.OriginalName, $"{queryString}%")
|
||||||
|| EF.Functions.Like(s.series.LocalizedName, $"{queryString}%")
|
|| EF.Functions.Like(s.Series.LocalizedName, $"{queryString}%")
|
||||||
|| EF.Functions.Like(s.series.SortName, $"{queryString}%"));
|
|| EF.Functions.Like(s.Series.SortName, $"{queryString}%"));
|
||||||
break;
|
break;
|
||||||
case FilterComparison.EndsWith:
|
case FilterComparison.EndsWith:
|
||||||
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"%{queryString}")
|
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"%{queryString}")
|
||||||
||EF.Functions.Like(s.series.OriginalName, $"%{queryString}")
|
||EF.Functions.Like(s.Series.OriginalName, $"%{queryString}")
|
||||||
|| EF.Functions.Like(s.series.LocalizedName, $"%{queryString}")
|
|| EF.Functions.Like(s.Series.LocalizedName, $"%{queryString}")
|
||||||
|| EF.Functions.Like(s.series.SortName, $"%{queryString}"));
|
|| EF.Functions.Like(s.Series.SortName, $"%{queryString}"));
|
||||||
break;
|
break;
|
||||||
case FilterComparison.Matches:
|
case FilterComparison.Matches:
|
||||||
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"%{queryString}%")
|
filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"%{queryString}%")
|
||||||
||EF.Functions.Like(s.series.OriginalName, $"%{queryString}%")
|
||EF.Functions.Like(s.Series.OriginalName, $"%{queryString}%")
|
||||||
|| EF.Functions.Like(s.series.LocalizedName, $"%{queryString}%")
|
|| EF.Functions.Like(s.Series.LocalizedName, $"%{queryString}%")
|
||||||
|| EF.Functions.Like(s.series.SortName, $"%{queryString}%"));
|
|| EF.Functions.Like(s.Series.SortName, $"%{queryString}%"));
|
||||||
break;
|
break;
|
||||||
case FilterComparison.NotEqual:
|
case FilterComparison.NotEqual:
|
||||||
filterSeriesQuery = filterSeriesQuery.Where(s => s.series.Name != queryString
|
filterSeriesQuery = filterSeriesQuery.Where(s => s.Series.Name != queryString
|
||||||
|| s.series.OriginalName != queryString
|
|| s.Series.OriginalName != queryString
|
||||||
|| s.series.LocalizedName != queryString
|
|| s.Series.LocalizedName != queryString
|
||||||
|| s.series.SortName != queryString);
|
|| s.Series.SortName != queryString);
|
||||||
break;
|
break;
|
||||||
case FilterComparison.MustContains:
|
case FilterComparison.MustContains:
|
||||||
case FilterComparison.NotContains:
|
case FilterComparison.NotContains:
|
||||||
@ -504,7 +504,7 @@ public class UserRepository : IUserRepository
|
|||||||
return await ApplyLimit(filterSeriesQuery
|
return await ApplyLimit(filterSeriesQuery
|
||||||
.Sort(filter.SortOptions)
|
.Sort(filter.SortOptions)
|
||||||
.AsSplitQuery(), filter.LimitTo)
|
.AsSplitQuery(), filter.LimitTo)
|
||||||
.Select(o => o.bookmark)
|
.Select(o => o.Bookmark)
|
||||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ namespace API.Extensions.QueryExtensions.Filtering;
|
|||||||
|
|
||||||
public class BookmarkSeriesPair
|
public class BookmarkSeriesPair
|
||||||
{
|
{
|
||||||
public AppUserBookmark bookmark { get; set; }
|
public AppUserBookmark Bookmark { get; set; }
|
||||||
public Series series { get; set; }
|
public Series Series { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BookmarkSort
|
public static class BookmarkSort
|
||||||
@ -31,12 +31,13 @@ public static class BookmarkSort
|
|||||||
{
|
{
|
||||||
query = sortOptions.SortField switch
|
query = sortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderBy(s => s.series.SortName.ToLower()),
|
SortField.SortName => query.OrderBy(s => s.Series.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderBy(s => s.series.Created),
|
SortField.CreatedDate => query.OrderBy(s => s.Series.Created),
|
||||||
SortField.LastModifiedDate => query.OrderBy(s => s.series.LastModified),
|
SortField.LastModifiedDate => query.OrderBy(s => s.Series.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderBy(s => s.series.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.Series.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderBy(s => s.series.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderBy(s => s.Series.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderBy(s => s.series.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderBy(s => s.Series.Metadata.ReleaseYear),
|
||||||
|
SortField.ReadProgress => query.OrderBy(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -44,12 +45,13 @@ public static class BookmarkSort
|
|||||||
{
|
{
|
||||||
query = sortOptions.SortField switch
|
query = sortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderByDescending(s => s.series.SortName.ToLower()),
|
SortField.SortName => query.OrderByDescending(s => s.Series.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderByDescending(s => s.series.Created),
|
SortField.CreatedDate => query.OrderByDescending(s => s.Series.Created),
|
||||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.series.LastModified),
|
SortField.LastModifiedDate => query.OrderByDescending(s => s.Series.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.series.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.Series.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderByDescending(s => s.series.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderByDescending(s => s.Series.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderByDescending(s => s.series.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderByDescending(s => s.Series.Metadata.ReleaseYear),
|
||||||
|
SortField.ReadProgress => query.OrderByDescending(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -288,6 +288,64 @@ public static class SeriesFilter
|
|||||||
return queryable.Where(s => ids.Contains(s.Id));
|
return queryable.Where(s => ids.Contains(s.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<Series> HasReadingDate(this IQueryable<Series> queryable, bool condition,
|
||||||
|
FilterComparison comparison, DateTime? date, int userId)
|
||||||
|
{
|
||||||
|
if (!condition || !date.HasValue) return queryable;
|
||||||
|
|
||||||
|
var subQuery = queryable
|
||||||
|
.Include(s => s.Progress)
|
||||||
|
.Where(s => s.Progress != null)
|
||||||
|
.Select(s => new
|
||||||
|
{
|
||||||
|
Series = s,
|
||||||
|
MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId)
|
||||||
|
.Select(p => (DateTime?) p.LastModified)
|
||||||
|
.DefaultIfEmpty()
|
||||||
|
.Max()
|
||||||
|
})
|
||||||
|
.Where(s => s.MaxDate != null)
|
||||||
|
.AsEnumerable();
|
||||||
|
|
||||||
|
switch (comparison)
|
||||||
|
{
|
||||||
|
case FilterComparison.Equal:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date));
|
||||||
|
break;
|
||||||
|
case FilterComparison.IsAfter:
|
||||||
|
case FilterComparison.GreaterThan:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date);
|
||||||
|
break;
|
||||||
|
case FilterComparison.GreaterThanEqual:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date);
|
||||||
|
break;
|
||||||
|
case FilterComparison.IsBefore:
|
||||||
|
case FilterComparison.LessThan:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date);
|
||||||
|
break;
|
||||||
|
case FilterComparison.LessThanEqual:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date);
|
||||||
|
break;
|
||||||
|
case FilterComparison.NotEqual:
|
||||||
|
subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date));
|
||||||
|
break;
|
||||||
|
case FilterComparison.Matches:
|
||||||
|
case FilterComparison.Contains:
|
||||||
|
case FilterComparison.NotContains:
|
||||||
|
case FilterComparison.BeginsWith:
|
||||||
|
case FilterComparison.EndsWith:
|
||||||
|
case FilterComparison.IsInLast:
|
||||||
|
case FilterComparison.IsNotInLast:
|
||||||
|
case FilterComparison.MustContains:
|
||||||
|
throw new KavitaException($"{comparison} not applicable for Series.ReadProgress");
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = subQuery.Select(s => s.Series.Id).ToList();
|
||||||
|
return queryable.Where(s => ids.Contains(s.Id));
|
||||||
|
}
|
||||||
|
|
||||||
public static IQueryable<Series> HasTags(this IQueryable<Series> queryable, bool condition,
|
public static IQueryable<Series> HasTags(this IQueryable<Series> queryable, bool condition,
|
||||||
FilterComparison comparison, IList<int> tags)
|
FilterComparison comparison, IList<int> tags)
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ public static class SeriesSort
|
|||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderBy(s => s.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderBy(s => s.Metadata.ReleaseYear),
|
||||||
//SortField.ReadProgress => query.OrderBy()
|
SortField.ReadProgress => query.OrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -45,6 +45,7 @@ public static class SeriesSort
|
|||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
||||||
SortField.ReleaseYear => query.OrderByDescending(s => s.Metadata.ReleaseYear),
|
SortField.ReleaseYear => query.OrderByDescending(s => s.Metadata.ReleaseYear),
|
||||||
|
SortField.ReadProgress => query.OrderByDescending(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max()),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,12 +36,12 @@ public class AutoMapperProfiles : Profile
|
|||||||
public AutoMapperProfiles()
|
public AutoMapperProfiles()
|
||||||
{
|
{
|
||||||
CreateMap<BookmarkSeriesPair, BookmarkDto>()
|
CreateMap<BookmarkSeriesPair, BookmarkDto>()
|
||||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.bookmark.Id))
|
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Bookmark.Id))
|
||||||
.ForMember(dest => dest.Page, opt => opt.MapFrom(src => src.bookmark.Page))
|
.ForMember(dest => dest.Page, opt => opt.MapFrom(src => src.Bookmark.Page))
|
||||||
.ForMember(dest => dest.VolumeId, opt => opt.MapFrom(src => src.bookmark.VolumeId))
|
.ForMember(dest => dest.VolumeId, opt => opt.MapFrom(src => src.Bookmark.VolumeId))
|
||||||
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.bookmark.SeriesId))
|
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Bookmark.SeriesId))
|
||||||
.ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.bookmark.ChapterId))
|
.ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.Bookmark.ChapterId))
|
||||||
.ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.series));
|
.ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series));
|
||||||
CreateMap<LibraryDto, Library>();
|
CreateMap<LibraryDto, Library>();
|
||||||
CreateMap<Volume, VolumeDto>();
|
CreateMap<Volume, VolumeDto>();
|
||||||
CreateMap<MangaFile, MangaFileDto>();
|
CreateMap<MangaFile, MangaFileDto>();
|
||||||
|
@ -69,6 +69,7 @@ public static class FilterFieldValueConverter
|
|||||||
.ToList(), typeof(IList<int>)),
|
.ToList(), typeof(IList<int>)),
|
||||||
FilterField.WantToRead => (bool.Parse(value), typeof(bool)),
|
FilterField.WantToRead => (bool.Parse(value), typeof(bool)),
|
||||||
FilterField.ReadProgress => (int.Parse(value), typeof(int)),
|
FilterField.ReadProgress => (int.Parse(value), typeof(int)),
|
||||||
|
FilterField.ReadingDate => (DateTime.Parse(value), typeof(DateTime?)),
|
||||||
FilterField.Formats => (value.Split(',')
|
FilterField.Formats => (value.Split(',')
|
||||||
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
||||||
.ToList(), typeof(IList<MangaFormat>)),
|
.ToList(), typeof(IList<MangaFormat>)),
|
||||||
|
@ -20,6 +20,7 @@ export enum SortField {
|
|||||||
LastChapterAdded = 4,
|
LastChapterAdded = 4,
|
||||||
TimeToRead = 5,
|
TimeToRead = 5,
|
||||||
ReleaseYear = 6,
|
ReleaseYear = 6,
|
||||||
|
ReadProgress = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allSortFields = Object.keys(SortField)
|
export const allSortFields = Object.keys(SortField)
|
||||||
|
@ -27,7 +27,8 @@ export enum FilterField
|
|||||||
ReadTime = 23,
|
ReadTime = 23,
|
||||||
Path = 24,
|
Path = 24,
|
||||||
FilePath = 25,
|
FilePath = 25,
|
||||||
WantToRead = 26
|
WantToRead = 26,
|
||||||
|
ReadingDate = 27
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allFields = Object.keys(FilterField)
|
export const allFields = Object.keys(FilterField)
|
||||||
|
@ -205,7 +205,7 @@ export class ActionFactoryService {
|
|||||||
action: Action.Scan,
|
action: Action.Scan,
|
||||||
title: 'scan-library',
|
title: 'scan-library',
|
||||||
callback: this.dummyCallback,
|
callback: this.dummyCallback,
|
||||||
requiresAdmin: false,
|
requiresAdmin: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,47 +1,73 @@
|
|||||||
|
<ng-container *transloco="let t; read: 'metadata-filter-row'">
|
||||||
|
<form [formGroup]="formGroup">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-3 me-2 col-10 mb-2">
|
||||||
|
<select class="form-select me-2" formControlName="input">
|
||||||
|
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="formGroup">
|
<div class="col-md-2 me-2 col-10 mb-2">
|
||||||
<div class="row g-0">
|
<select class="col-auto form-select" formControlName="comparison">
|
||||||
<div class="col-md-3 me-2 col-10 mb-2">
|
<option *ngFor="let comparison of validComparisons$ | async" [value]="comparison">{{comparison | filterComparison}}</option>
|
||||||
<select class="form-select me-2" formControlName="input">
|
</select>
|
||||||
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2 me-2 col-10 mb-2">
|
<div class="col-md-4 col-10 mb-2">
|
||||||
<select class="col-auto form-select" formControlName="comparison">
|
<ng-container *ngIf="predicateType$ | async as predicateType">
|
||||||
<option *ngFor="let comparison of validComparisons$ | async" [value]="comparison">{{comparison | filterComparison}}</option>
|
<ng-container [ngSwitch]="predicateType">
|
||||||
</select>
|
<ng-container *ngSwitchCase="PredicateType.Text">
|
||||||
</div>
|
<input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue">
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="PredicateType.Number">
|
||||||
|
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="PredicateType.Boolean">
|
||||||
|
<input type="checkbox" class="form-check-input mt-2 me-2" style="font-size: 1.5rem" formControlName="filterValue">
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="PredicateType.Date">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
placeholder="yyyy-mm-dd"
|
||||||
|
name="dp"
|
||||||
|
formControlName="filterValue"
|
||||||
|
(dateSelect)="onDateSelect($event)"
|
||||||
|
(blur)="updateIfDateFilled()"
|
||||||
|
ngbDatepicker
|
||||||
|
#d="ngbDatepicker"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-outline-secondary fa-solid fa-calendar-days" (click)="d.toggle()" type="button"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 col-10 mb-2">
|
|
||||||
<ng-container *ngIf="predicateType$ | async as predicateType">
|
</ng-container>
|
||||||
<ng-container [ngSwitch]="predicateType">
|
<ng-container *ngSwitchCase="PredicateType.Dropdown">
|
||||||
<ng-container *ngSwitchCase="PredicateType.Text">
|
<ng-container *ngIf="dropdownOptions$ | async as opts">
|
||||||
<input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue">
|
<ng-container *ngTemplateOutlet="dropdown; context: { options: opts, multipleAllowed: MultipleDropdownAllowed }"></ng-container>
|
||||||
</ng-container>
|
<ng-template #dropdown let-options="options" let-multipleAllowed="multipleAllowed">
|
||||||
<ng-container *ngSwitchCase="PredicateType.Number">
|
<select2 [data]="options"
|
||||||
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
|
formControlName="filterValue"
|
||||||
</ng-container>
|
[hideSelectedItems]="true"
|
||||||
<ng-container *ngSwitchCase="PredicateType.Boolean">
|
[multiple]="multipleAllowed"
|
||||||
<input type="checkbox" class="form-check-input mt-2 me-2" style="font-size: 1.5rem" formControlName="filterValue">
|
[infiniteScroll]="true"
|
||||||
</ng-container>
|
[resettable]="true">
|
||||||
<ng-container *ngSwitchCase="PredicateType.Dropdown">
|
</select2>
|
||||||
<ng-container *ngIf="dropdownOptions$ | async as opts">
|
</ng-template>
|
||||||
<ng-container *ngTemplateOutlet="dropdown; context: { options: opts, multipleAllowed: MultipleDropdownAllowed }"></ng-container>
|
</ng-container>
|
||||||
<ng-template #dropdown let-options="options" let-multipleAllowed="multipleAllowed">
|
</ng-container>
|
||||||
<select2 [data]="options"
|
|
||||||
formControlName="filterValue"
|
|
||||||
[multiple]="multipleAllowed"
|
|
||||||
[infiniteScroll]="true"
|
|
||||||
[resettable]="true">
|
|
||||||
</select2>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-content #removeBtn></ng-content>
|
<div class="col pt-2 ms-2">
|
||||||
</div>
|
<ng-container *ngIf="UiLabel !== null">
|
||||||
</form>
|
<span class="text-muted">{{t(UiLabel.unit)}}</span>
|
||||||
|
<i *ngIf="UiLabel.tooltip" class="fa fa-info-circle ms-1" aria-hidden="true" [ngbTooltip]="t(UiLabel.tooltip)"></i>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-content #removeBtn></ng-content>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
|
@ -1,3 +1,23 @@
|
|||||||
::ng-deep .select2-selection__rendered {
|
::ng-deep .select2-selection__rendered {
|
||||||
padding-top: 4px !important;
|
padding-top: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header, ::ng-deep .dropdown-menu{
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
color: var(--body-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .ngb-dp-header, ::ng-deep .ngb-dp-weekdays {
|
||||||
|
background-color: var(--bs-body-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .ngb-dp-day .btn-light, ::ng-deep .ngb-dp-weekday {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
color: var(--body-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep [ngbDatepickerDayView]:hover:not(.bg-primary), [ngbDatepickerDayView].active:not(.bg-primary) {
|
||||||
|
background: var(--primary-color-dark-shade) !important;
|
||||||
|
outline: 1px solid var(--primary-color-dark-shade) !important;
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output, ViewChild,
|
||||||
} 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';
|
||||||
@ -25,14 +25,38 @@ import {FilterComparisonPipe} from "../../_pipes/filter-comparison.pipe";
|
|||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {Select2Module, Select2Option} from "ng-select2-component";
|
import {Select2Module, Select2Option} from "ng-select2-component";
|
||||||
import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component";
|
import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component";
|
||||||
|
import {
|
||||||
|
NgbDate,
|
||||||
|
NgbDateParserFormatter,
|
||||||
|
NgbDatepicker,
|
||||||
|
NgbDateStruct,
|
||||||
|
NgbInputDatepicker,
|
||||||
|
NgbTooltip
|
||||||
|
} from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import {TranslocoDirective} from "@ngneat/transloco";
|
||||||
|
|
||||||
enum PredicateType {
|
enum PredicateType {
|
||||||
Text = 1,
|
Text = 1,
|
||||||
Number = 2,
|
Number = 2,
|
||||||
Dropdown = 3,
|
Dropdown = 3,
|
||||||
Boolean = 4
|
Boolean = 4,
|
||||||
|
Date = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FilterRowUi {
|
||||||
|
unit = '';
|
||||||
|
tooltip = ''
|
||||||
|
constructor(unit: string = '', tooltip: string = '') {
|
||||||
|
this.unit = unit;
|
||||||
|
this.tooltip = tooltip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitLabels: Map<FilterField, FilterRowUi> = new Map([
|
||||||
|
[FilterField.ReadingDate, new FilterRowUi('unit-reading-date')],
|
||||||
|
[FilterField.ReadProgress, new FilterRowUi('unit-reading-progress')],
|
||||||
|
]);
|
||||||
|
|
||||||
const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath];
|
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,
|
||||||
@ -42,7 +66,8 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
|
|||||||
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
|
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
|
||||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
||||||
];
|
];
|
||||||
const BooleanFields = [FilterField.WantToRead]
|
const BooleanFields = [FilterField.WantToRead];
|
||||||
|
const DateFields = [FilterField.ReadingDate];
|
||||||
|
|
||||||
const DropdownFieldsWithoutMustContains = [
|
const DropdownFieldsWithoutMustContains = [
|
||||||
FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus
|
FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus
|
||||||
@ -59,7 +84,8 @@ const StringComparisons = [FilterComparison.Equal,
|
|||||||
FilterComparison.BeginsWith,
|
FilterComparison.BeginsWith,
|
||||||
FilterComparison.EndsWith,
|
FilterComparison.EndsWith,
|
||||||
FilterComparison.Matches];
|
FilterComparison.Matches];
|
||||||
const DateComparisons = [FilterComparison.IsBefore, FilterComparison.IsAfter, FilterComparison.IsInLast, FilterComparison.IsNotInLast];
|
const DateComparisons = [FilterComparison.IsBefore, FilterComparison.IsAfter, FilterComparison.Equal,
|
||||||
|
FilterComparison.NotEqual,];
|
||||||
const NumberComparisons = [FilterComparison.Equal,
|
const NumberComparisons = [FilterComparison.Equal,
|
||||||
FilterComparison.NotEqual,
|
FilterComparison.NotEqual,
|
||||||
FilterComparison.LessThan,
|
FilterComparison.LessThan,
|
||||||
@ -91,7 +117,11 @@ const BooleanComparisons = [
|
|||||||
NgIf,
|
NgIf,
|
||||||
Select2Module,
|
Select2Module,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
TagBadgeComponent
|
TagBadgeComponent,
|
||||||
|
NgbTooltip,
|
||||||
|
TranslocoDirective,
|
||||||
|
NgbDatepicker,
|
||||||
|
NgbInputDatepicker
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
@ -105,8 +135,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
@Input() availableFields: Array<FilterField> = allFields;
|
@Input() availableFields: Array<FilterField> = allFields;
|
||||||
@Output() filterStatement = new EventEmitter<FilterStatement>();
|
@Output() filterStatement = new EventEmitter<FilterStatement>();
|
||||||
|
|
||||||
|
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly dateParser = inject(NgbDateParserFormatter);
|
||||||
|
|
||||||
formGroup: FormGroup = new FormGroup({
|
formGroup: FormGroup = new FormGroup({
|
||||||
'comparison': new FormControl<FilterComparison>(FilterComparison.Equal, []),
|
'comparison': new FormControl<FilterComparison>(FilterComparison.Equal, []),
|
||||||
@ -119,6 +151,12 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
loaded: boolean = false;
|
loaded: boolean = false;
|
||||||
protected readonly PredicateType = PredicateType;
|
protected readonly PredicateType = PredicateType;
|
||||||
|
|
||||||
|
get UiLabel(): FilterRowUi | null {
|
||||||
|
const field = parseInt(this.formGroup.get('input')!.value, 10) as FilterField;
|
||||||
|
if (!unitLabels.has(field)) return null;
|
||||||
|
return unitLabels.get(field) as FilterRowUi;
|
||||||
|
}
|
||||||
|
|
||||||
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 || comp === FilterComparison.MustContains;
|
return comp === FilterComparison.Contains || comp === FilterComparison.NotContains || comp === FilterComparison.MustContains;
|
||||||
@ -149,30 +187,36 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
|
|
||||||
this.formGroup!.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
|
this.formGroup!.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
|
||||||
const stmt = {
|
this.propagateFilterUpdate();
|
||||||
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
|
|
||||||
field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField,
|
|
||||||
value: this.formGroup.get('filterValue')?.value!
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 + '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof stmt.value === 'boolean') {
|
|
||||||
stmt.value = stmt.value + '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stmt.value && (stmt.field !== FilterField.SeriesName && !BooleanFields.includes(stmt.field))) return;
|
|
||||||
this.filterStatement.emit(stmt);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
propagateFilterUpdate() {
|
||||||
|
const stmt = {
|
||||||
|
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
|
||||||
|
field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField,
|
||||||
|
value: this.formGroup.get('filterValue')?.value!
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof stmt.value === 'object' && DateFields.includes(stmt.field)) {
|
||||||
|
stmt.value = this.dateParser.format(stmt.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 + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof stmt.value === 'boolean') {
|
||||||
|
stmt.value = stmt.value + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stmt.value && (![FilterField.SeriesName, FilterField.Summary].includes(stmt.field) && !BooleanFields.includes(stmt.field))) return;
|
||||||
|
this.filterStatement.emit(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
populateFromPreset() {
|
populateFromPreset() {
|
||||||
const val = this.preset.value === "undefined" || !this.preset.value ? '' : this.preset.value;
|
const val = this.preset.value === "undefined" || !this.preset.value ? '' : this.preset.value;
|
||||||
@ -183,7 +227,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
this.formGroup.get('filterValue')?.patchValue(val);
|
this.formGroup.get('filterValue')?.patchValue(val);
|
||||||
} else if (BooleanFields.includes(this.preset.field)) {
|
} else if (BooleanFields.includes(this.preset.field)) {
|
||||||
this.formGroup.get('filterValue')?.patchValue(val);
|
this.formGroup.get('filterValue')?.patchValue(val);
|
||||||
} else if (DropdownFields.includes(this.preset.field)) {
|
} else if (DateFields.includes(this.preset.field)) {
|
||||||
|
this.formGroup.get('filterValue')?.patchValue(this.dateParser.parse(val)); // TODO: Figure out how this works
|
||||||
|
}
|
||||||
|
else if (DropdownFields.includes(this.preset.field)) {
|
||||||
if (this.MultipleDropdownAllowed || val.includes(',')) {
|
if (this.MultipleDropdownAllowed || val.includes(',')) {
|
||||||
this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10)));
|
this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10)));
|
||||||
} else {
|
} else {
|
||||||
@ -281,6 +328,16 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DateFields.includes(inputVal)) {
|
||||||
|
this.validComparisons$.next(DateComparisons);
|
||||||
|
this.predicateType$.next(PredicateType.Date);
|
||||||
|
|
||||||
|
if (this.loaded) {
|
||||||
|
this.formGroup.get('filterValue')?.patchValue(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (BooleanFields.includes(inputVal)) {
|
if (BooleanFields.includes(inputVal)) {
|
||||||
this.validComparisons$.next(BooleanComparisons);
|
this.validComparisons$.next(BooleanComparisons);
|
||||||
this.predicateType$.next(PredicateType.Boolean);
|
this.predicateType$.next(PredicateType.Boolean);
|
||||||
@ -306,4 +363,15 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onDateSelect(event: NgbDate) {
|
||||||
|
console.log('date selected: ', event);
|
||||||
|
this.propagateFilterUpdate();
|
||||||
|
}
|
||||||
|
updateIfDateFilled() {
|
||||||
|
console.log('date inputted: ', this.formGroup.get('filterValue')?.value);
|
||||||
|
this.propagateFilterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,8 @@ export class FilterFieldPipe implements PipeTransform {
|
|||||||
return translate('filter-field-pipe.file-path');
|
return translate('filter-field-pipe.file-path');
|
||||||
case FilterField.WantToRead:
|
case FilterField.WantToRead:
|
||||||
return translate('filter-field-pipe.want-to-read');
|
return translate('filter-field-pipe.want-to-read');
|
||||||
|
case FilterField.ReadingDate:
|
||||||
|
return translate('filter-field-pipe.read-date');
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid FilterField value: ${value}`);
|
throw new Error(`Invalid FilterField value: ${value}`);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<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" *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet">
|
<div *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" *ngIf="utilityService.getActiveBreakpoint() < Breakpoint.Desktop">
|
<div *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')}}
|
||||||
@ -51,6 +51,16 @@
|
|||||||
<div class="col-md-3 col-sm-10">
|
<div class="col-md-3 col-sm-10">
|
||||||
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
||||||
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
||||||
|
<!-- <select2 [data]="smartFilters"-->
|
||||||
|
<!-- id="filter-name"-->
|
||||||
|
<!-- formControlName="name"-->
|
||||||
|
<!-- (update)="updateFilterValue($event)"-->
|
||||||
|
<!-- [autoCreate]="true"-->
|
||||||
|
<!-- [multiple]="false"-->
|
||||||
|
<!-- [infiniteScroll]="false"-->
|
||||||
|
<!-- [hideSelectedItems]="true"-->
|
||||||
|
<!-- [resettable]="true">-->
|
||||||
|
<!-- </select2>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet" [ngTemplateOutlet]="buttons"></ng-container>
|
<ng-container *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet" [ngTemplateOutlet]="buttons"></ng-container>
|
||||||
@ -63,15 +73,13 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #buttons>
|
<ng-template #buttons>
|
||||||
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
||||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
<div class="col-md-2 col-sm-6 mt-4 pt-2 d-flex justify-content-between">
|
||||||
<button class="btn btn-secondary col-12" (click)="clear()">{{t('reset')}}</button>
|
<button class="btn btn-secondary col-6 me-1" (click)="clear()"><i class="fa-solid fa-arrow-rotate-left me-1" aria-hidden="true"></i>{{t('reset')}}</button>
|
||||||
|
<button class="btn btn-primary col-6" (click)="apply()"><i class="fa-solid fa-play me-1" aria-hidden="true"></i>{{t('apply')}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
<div class="col-md-1 col-sm-6 mt-4 pt-2">
|
||||||
<button class="btn btn-primary col-12" (click)="apply()">{{t('apply')}}</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
|
||||||
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
|
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
|
||||||
<!-- TODO: Icon here -->
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{t('save')}}
|
{{t('save')}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,6 +30,8 @@ import {MetadataService} from "../_services/metadata.service";
|
|||||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||||
import {FilterService} from "../_services/filter.service";
|
import {FilterService} from "../_services/filter.service";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
import {Select2Module, Select2Option, Select2UpdateEvent} from "ng-select2-component";
|
||||||
|
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-metadata-filter',
|
selector: 'app-metadata-filter',
|
||||||
@ -38,7 +40,7 @@ import {ToastrService} from "ngx-toastr";
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
||||||
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf]
|
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module]
|
||||||
})
|
})
|
||||||
export class MetadataFilterComponent implements OnInit {
|
export class MetadataFilterComponent implements OnInit {
|
||||||
|
|
||||||
@ -78,16 +80,22 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
allSortFields = allSortFields;
|
allSortFields = allSortFields;
|
||||||
allFilterFields = allFields;
|
allFilterFields = allFields;
|
||||||
|
|
||||||
handleFilters(filter: SeriesFilterV2) {
|
smartFilters!: Array<Select2Option>;
|
||||||
this.filterV2 = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly toastr = inject(ToastrService);
|
private readonly toastr = inject(ToastrService);
|
||||||
|
|
||||||
|
|
||||||
constructor(public toggleService: ToggleService, private filterService: FilterService) {}
|
constructor(public toggleService: ToggleService, private filterService: FilterService) {
|
||||||
|
this.filterService.getAllFilters().subscribe(res => {
|
||||||
|
this.smartFilters = res.map(r => {
|
||||||
|
return {
|
||||||
|
value: r,
|
||||||
|
label: r.name,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.filterSettings === undefined) {
|
if (this.filterSettings === undefined) {
|
||||||
@ -106,6 +114,11 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
this.loadFromPresetsAndSetup();
|
this.loadFromPresetsAndSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFilterValue(event: Select2UpdateEvent<any>) {
|
||||||
|
console.log('event: ', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.filterOpen.emit(false);
|
this.filterOpen.emit(false);
|
||||||
this.filteringCollapsed = true;
|
this.filteringCollapsed = true;
|
||||||
@ -137,6 +150,10 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
return clonedObj;
|
return clonedObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFilters(filter: SeriesFilterV2) {
|
||||||
|
this.filterV2 = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
loadFromPresetsAndSetup() {
|
loadFromPresetsAndSetup() {
|
||||||
this.fullyLoaded = false;
|
this.fullyLoaded = false;
|
||||||
@ -187,7 +204,7 @@ 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) {
|
||||||
this.toggleSelected();
|
this.toggleSelected();
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,19 @@ export class SortFieldPipe implements PipeTransform {
|
|||||||
transform(value: SortField): string {
|
transform(value: SortField): string {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case SortField.SortName:
|
case SortField.SortName:
|
||||||
return this.translocoService.translate('sort-field-pipe.sort-name')
|
return this.translocoService.translate('sort-field-pipe.sort-name');
|
||||||
case SortField.Created:
|
case SortField.Created:
|
||||||
return this.translocoService.translate('sort-field-pipe.created')
|
return this.translocoService.translate('sort-field-pipe.created');
|
||||||
case SortField.LastModified:
|
case SortField.LastModified:
|
||||||
return this.translocoService.translate('sort-field-pipe.last-modified')
|
return this.translocoService.translate('sort-field-pipe.last-modified');
|
||||||
case SortField.LastChapterAdded:
|
case SortField.LastChapterAdded:
|
||||||
return this.translocoService.translate('sort-field-pipe.last-chapter-added')
|
return this.translocoService.translate('sort-field-pipe.last-chapter-added');
|
||||||
case SortField.TimeToRead:
|
case SortField.TimeToRead:
|
||||||
return this.translocoService.translate('sort-field-pipe.time-to-read')
|
return this.translocoService.translate('sort-field-pipe.time-to-read');
|
||||||
case SortField.ReleaseYear:
|
case SortField.ReleaseYear:
|
||||||
return this.translocoService.translate('sort-field-pipe.release-year')
|
return this.translocoService.translate('sort-field-pipe.release-year');
|
||||||
|
case SortField.ReadProgress:
|
||||||
|
return this.translocoService.translate('sort-field-pipe.read-progress');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
<button ngbDropdownItem (click)="read(true)">
|
<button ngbDropdownItem (click)="read(true)">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-glasses" aria-hidden="true"></i>
|
<i class="fa fa-glasses" aria-hidden="true"></i>
|
||||||
<span class="read-btn--text"> {{(hasReadingProgress) ? t('continue') : t('read')}} Incognito</span>
|
<span class="read-btn--text"> {{(hasReadingProgress) ? t('continue-incognito') : t('read-incognito')}}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -101,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto ms-2 d-none d-md-block" *ngIf="isAdmin || hasDownloadingRole">
|
<div class="col-auto ms-2 d-none d-md-block" *ngIf="isAdmin || hasDownloadingRole">
|
||||||
<button class="btn btn-secondary" (click)="downloadSeries()" title="Download Series" [disabled]="downloadInProgress">
|
<button class="btn btn-secondary" (click)="downloadSeries()" [title]="t('download-series--tooltip')" [disabled]="downloadInProgress">
|
||||||
<ng-container *ngIf="downloadInProgress; else notDownloading">
|
<ng-container *ngIf="downloadInProgress; else notDownloading">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<span class="visually-hidden">{{t('downloading-status')}}</span>
|
<span class="visually-hidden">{{t('downloading-status')}}</span>
|
||||||
|
@ -681,6 +681,8 @@
|
|||||||
"continue-from": "Continue {{title}}",
|
"continue-from": "Continue {{title}}",
|
||||||
"read": "{{common.read}}",
|
"read": "{{common.read}}",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
|
"read-incognito": "Read Incognito",
|
||||||
|
"continue-incognito": "Continue Incognito",
|
||||||
"read-options-alt": "Read options",
|
"read-options-alt": "Read options",
|
||||||
"incognito": "Incognito",
|
"incognito": "Incognito",
|
||||||
"remove-from-want-to-read": "Remove from Want to Read",
|
"remove-from-want-to-read": "Remove from Want to Read",
|
||||||
@ -1509,7 +1511,7 @@
|
|||||||
"reset": "{{common.reset}}",
|
"reset": "{{common.reset}}",
|
||||||
"apply": "{{common.apply}}",
|
"apply": "{{common.apply}}",
|
||||||
"save": "{{common.save}}",
|
"save": "{{common.save}}",
|
||||||
"limit-label": "Limit To",
|
"limit-label": "Limit",
|
||||||
|
|
||||||
"format-label": "Format",
|
"format-label": "Format",
|
||||||
"libraries-label": "Libraries",
|
"libraries-label": "Libraries",
|
||||||
@ -1541,13 +1543,19 @@
|
|||||||
"max": "Max"
|
"max": "Max"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"metadata-filter-row": {
|
||||||
|
"unit-reading-date": "Date",
|
||||||
|
"unit-reading-progress": "Percent"
|
||||||
|
},
|
||||||
|
|
||||||
"sort-field-pipe": {
|
"sort-field-pipe": {
|
||||||
"sort-name": "Sort Name",
|
"sort-name": "Sort Name",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"last-modified": "Last Modified",
|
"last-modified": "Last Modified",
|
||||||
"last-chapter-added": "Item Added",
|
"last-chapter-added": "Item Added",
|
||||||
"time-to-read": "Time to Read",
|
"time-to-read": "Time to Read",
|
||||||
"release-year": "Release Year"
|
"release-year": "Release Year",
|
||||||
|
"read-progress": "Read Progress"
|
||||||
},
|
},
|
||||||
|
|
||||||
"edit-series-modal": {
|
"edit-series-modal": {
|
||||||
@ -1751,7 +1759,8 @@
|
|||||||
"writers": "Writers",
|
"writers": "Writers",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
"file-path": "File Path",
|
"file-path": "File Path",
|
||||||
"want-to-read": "Want to Read"
|
"want-to-read": "Want to Read",
|
||||||
|
"read-date": "Reading Date"
|
||||||
},
|
},
|
||||||
|
|
||||||
"filter-comparison-pipe": {
|
"filter-comparison-pipe": {
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
mkdir Projects
|
|
||||||
|
|
||||||
cd Projects
|
|
||||||
|
|
||||||
git clone https://github.com/Kareadita/Kavita.git
|
|
||||||
git clone https://github.com/Kareadita/Kavita-webui.git
|
|
||||||
|
|
||||||
cd Kavita
|
|
||||||
chmod +x build.sh
|
|
||||||
|
|
||||||
#Builds program based on the target platform
|
|
||||||
|
|
||||||
if [ "$TARGETPLATFORM" == "linux/amd64" ]
|
|
||||||
then
|
|
||||||
./build.sh linux-x64
|
|
||||||
mv /Projects/Kavita/_output/linux-x64 /Projects/Kavita/_output/build
|
|
||||||
elif [ "$TARGETPLATFORM" == "linux/arm/v7" ]
|
|
||||||
then
|
|
||||||
./build.sh linux-arm
|
|
||||||
mv /Projects/Kavita/_output/linux-arm /Projects/Kavita/_output/build
|
|
||||||
elif [ "$TARGETPLATFORM" == "linux/arm64" ]
|
|
||||||
then
|
|
||||||
./build.sh linux-arm64
|
|
||||||
mv /Projects/Kavita/_output/linux-arm64 /Projects/Kavita/_output/build
|
|
||||||
fi
|
|
@ -15,9 +15,9 @@ ProgressEnd()
|
|||||||
|
|
||||||
Build()
|
Build()
|
||||||
{
|
{
|
||||||
local RID="$1"
|
local RID="$1"
|
||||||
|
|
||||||
ProgressStart 'Build for $RID'
|
ProgressStart "Build for $RID"
|
||||||
|
|
||||||
slnFile=Kavita.sln
|
slnFile=Kavita.sln
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ Build()
|
|||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID
|
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID
|
||||||
|
|
||||||
ProgressEnd 'Build for $RID'
|
ProgressEnd "Build for $RID"
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildUI()
|
BuildUI()
|
||||||
@ -54,17 +54,16 @@ BuildUI()
|
|||||||
|
|
||||||
Package()
|
Package()
|
||||||
{
|
{
|
||||||
local framework="$1"
|
local runtime="$1"
|
||||||
local runtime="$2"
|
|
||||||
local lOutputFolder=../_output/"$runtime"/Kavita
|
local lOutputFolder=../_output/"$runtime"/Kavita
|
||||||
|
|
||||||
ProgressStart "Creating $runtime Package for $framework"
|
ProgressStart "Creating $runtime Package"
|
||||||
|
|
||||||
# TODO: Use no-restore? Because Build should have already done it for us
|
# TODO: Use no-restore? Because Build should have already done it for us
|
||||||
echo "Building"
|
echo "Building"
|
||||||
cd API
|
cd API
|
||||||
echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework
|
echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder"
|
||||||
dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework
|
dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder"
|
||||||
|
|
||||||
echo "Copying Install information"
|
echo "Copying Install information"
|
||||||
cp ../INSTALL.txt "$lOutputFolder"/README.txt
|
cp ../INSTALL.txt "$lOutputFolder"/README.txt
|
||||||
@ -79,7 +78,7 @@ Package()
|
|||||||
cd ../$outputFolder/"$runtime"/
|
cd ../$outputFolder/"$runtime"/
|
||||||
tar -czvf ../kavita-$runtime.tar.gz Kavita
|
tar -czvf ../kavita-$runtime.tar.gz Kavita
|
||||||
|
|
||||||
ProgressEnd "Creating $runtime Package for $framework"
|
ProgressEnd "Creating $runtime Package"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,17 +93,17 @@ BuildUI
|
|||||||
|
|
||||||
#Build for x64
|
#Build for x64
|
||||||
Build "linux-x64"
|
Build "linux-x64"
|
||||||
Package "net6.0" "linux-x64"
|
Package "linux-x64"
|
||||||
cd "$dir"
|
cd "$dir"
|
||||||
|
|
||||||
#Build for arm
|
#Build for arm
|
||||||
Build "linux-arm"
|
Build "linux-arm"
|
||||||
Package "net6.0" "linux-arm"
|
Package "linux-arm"
|
||||||
cd "$dir"
|
cd "$dir"
|
||||||
|
|
||||||
#Build for arm64
|
#Build for arm64
|
||||||
Build "linux-arm64"
|
Build "linux-arm64"
|
||||||
Package "net6.0" "linux-arm64"
|
Package "linux-arm64"
|
||||||
cd "$dir"
|
cd "$dir"
|
||||||
|
|
||||||
#Builds Docker images
|
#Builds Docker images
|
||||||
|
@ -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.8.2"
|
"version": "0.7.8.4"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -14142,7 +14142,8 @@
|
|||||||
23,
|
23,
|
||||||
24,
|
24,
|
||||||
25,
|
25,
|
||||||
26
|
26,
|
||||||
|
27
|
||||||
],
|
],
|
||||||
"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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user