diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index 776bc0e26..c42db3524 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -40,6 +40,10 @@ public enum FilterField /// /// On Want To Read or Not /// - WantToRead = 26 + WantToRead = 26, + /// + /// Last time User Read + /// + ReadingDate = 27 } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b1a31c752..d6ad22931 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -868,8 +868,6 @@ public class SeriesRepository : ISeriesRepository .HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres) .HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!) .HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0) - - // TODO: This needs different treatment .HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds) .WhereIf(onlyParentSeries, @@ -917,6 +915,7 @@ public class SeriesRepository : ISeriesRepository SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded), SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead), 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 }; } @@ -930,6 +929,7 @@ public class SeriesRepository : ISeriesRepository SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded), SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead), 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 }; } @@ -1089,6 +1089,7 @@ public class SeriesRepository : ISeriesRepository FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList) value), FilterField.ReleaseYear => query.HasReleaseYear(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() }; } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 524c9db6d..76135444d 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -439,8 +439,8 @@ public class UserRepository : IUserRepository var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new BookmarkSeriesPair() { - bookmark = bookmark, - series = series + Bookmark = bookmark, + Series = series }); var filterStatement = filter.Statements.FirstOrDefault(f => f.Field == FilterField.SeriesName); @@ -457,34 +457,34 @@ public class UserRepository : IUserRepository switch (filterStatement.Comparison) { case FilterComparison.Equal: - filterSeriesQuery = filterSeriesQuery.Where(s => s.series.Name.Equals(queryString) - || s.series.OriginalName.Equals(queryString) - || s.series.LocalizedName.Equals(queryString) - || s.series.SortName.Equals(queryString)); + filterSeriesQuery = filterSeriesQuery.Where(s => s.Series.Name.Equals(queryString) + || s.Series.OriginalName.Equals(queryString) + || s.Series.LocalizedName.Equals(queryString) + || s.Series.SortName.Equals(queryString)); break; case FilterComparison.BeginsWith: - filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"{queryString}%") - ||EF.Functions.Like(s.series.OriginalName, $"{queryString}%") - || EF.Functions.Like(s.series.LocalizedName, $"{queryString}%") - || EF.Functions.Like(s.series.SortName, $"{queryString}%")); + filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"{queryString}%") + ||EF.Functions.Like(s.Series.OriginalName, $"{queryString}%") + || EF.Functions.Like(s.Series.LocalizedName, $"{queryString}%") + || EF.Functions.Like(s.Series.SortName, $"{queryString}%")); break; case FilterComparison.EndsWith: - filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"%{queryString}") - ||EF.Functions.Like(s.series.OriginalName, $"%{queryString}") - || EF.Functions.Like(s.series.LocalizedName, $"%{queryString}") - || EF.Functions.Like(s.series.SortName, $"%{queryString}")); + filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"%{queryString}") + ||EF.Functions.Like(s.Series.OriginalName, $"%{queryString}") + || EF.Functions.Like(s.Series.LocalizedName, $"%{queryString}") + || EF.Functions.Like(s.Series.SortName, $"%{queryString}")); break; case FilterComparison.Matches: - filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.series.Name, $"%{queryString}%") - ||EF.Functions.Like(s.series.OriginalName, $"%{queryString}%") - || EF.Functions.Like(s.series.LocalizedName, $"%{queryString}%") - || EF.Functions.Like(s.series.SortName, $"%{queryString}%")); + filterSeriesQuery = filterSeriesQuery.Where(s => EF.Functions.Like(s.Series.Name, $"%{queryString}%") + ||EF.Functions.Like(s.Series.OriginalName, $"%{queryString}%") + || EF.Functions.Like(s.Series.LocalizedName, $"%{queryString}%") + || EF.Functions.Like(s.Series.SortName, $"%{queryString}%")); break; case FilterComparison.NotEqual: - filterSeriesQuery = filterSeriesQuery.Where(s => s.series.Name != queryString - || s.series.OriginalName != queryString - || s.series.LocalizedName != queryString - || s.series.SortName != queryString); + filterSeriesQuery = filterSeriesQuery.Where(s => s.Series.Name != queryString + || s.Series.OriginalName != queryString + || s.Series.LocalizedName != queryString + || s.Series.SortName != queryString); break; case FilterComparison.MustContains: case FilterComparison.NotContains: @@ -504,7 +504,7 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .Select(o => o.bookmark) + .Select(o => o.Bookmark) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs index d5b1a6d9b..dec7e7c4f 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs @@ -6,8 +6,8 @@ namespace API.Extensions.QueryExtensions.Filtering; public class BookmarkSeriesPair { - public AppUserBookmark bookmark { get; set; } - public Series series { get; set; } + public AppUserBookmark Bookmark { get; set; } + public Series Series { get; set; } } public static class BookmarkSort @@ -31,12 +31,13 @@ public static class BookmarkSort { query = sortOptions.SortField switch { - SortField.SortName => query.OrderBy(s => s.series.SortName.ToLower()), - SortField.CreatedDate => query.OrderBy(s => s.series.Created), - SortField.LastModifiedDate => query.OrderBy(s => s.series.LastModified), - SortField.LastChapterAdded => query.OrderBy(s => s.series.LastChapterAdded), - SortField.TimeToRead => query.OrderBy(s => s.series.AvgHoursToRead), - SortField.ReleaseYear => query.OrderBy(s => s.series.Metadata.ReleaseYear), + SortField.SortName => query.OrderBy(s => s.Series.SortName.ToLower()), + SortField.CreatedDate => query.OrderBy(s => s.Series.Created), + SortField.LastModifiedDate => query.OrderBy(s => s.Series.LastModified), + SortField.LastChapterAdded => query.OrderBy(s => s.Series.LastChapterAdded), + SortField.TimeToRead => query.OrderBy(s => s.Series.AvgHoursToRead), + 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 }; } @@ -44,12 +45,13 @@ public static class BookmarkSort { query = sortOptions.SortField switch { - SortField.SortName => query.OrderByDescending(s => s.series.SortName.ToLower()), - SortField.CreatedDate => query.OrderByDescending(s => s.series.Created), - SortField.LastModifiedDate => query.OrderByDescending(s => s.series.LastModified), - SortField.LastChapterAdded => query.OrderByDescending(s => s.series.LastChapterAdded), - SortField.TimeToRead => query.OrderByDescending(s => s.series.AvgHoursToRead), - SortField.ReleaseYear => query.OrderByDescending(s => s.series.Metadata.ReleaseYear), + SortField.SortName => query.OrderByDescending(s => s.Series.SortName.ToLower()), + SortField.CreatedDate => query.OrderByDescending(s => s.Series.Created), + SortField.LastModifiedDate => query.OrderByDescending(s => s.Series.LastModified), + SortField.LastChapterAdded => query.OrderByDescending(s => s.Series.LastChapterAdded), + SortField.TimeToRead => query.OrderByDescending(s => s.Series.AvgHoursToRead), + 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 }; } diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index d6e78da65..3bc97b3ad 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -288,6 +288,64 @@ public static class SeriesFilter return queryable.Where(s => ids.Contains(s.Id)); } + public static IQueryable HasReadingDate(this IQueryable 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 HasTags(this IQueryable queryable, bool condition, FilterComparison comparison, IList tags) { diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs index cd5d28b0b..b7eabcd6b 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs @@ -31,7 +31,7 @@ public static class SeriesSort SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded), SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead), 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 }; } @@ -45,6 +45,7 @@ public static class SeriesSort SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded), SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead), 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 }; } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index ffc7bb181..e3f8b8202 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -36,12 +36,12 @@ public class AutoMapperProfiles : Profile public AutoMapperProfiles() { CreateMap() - .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.VolumeId, opt => opt.MapFrom(src => src.bookmark.VolumeId)) - .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.Series, opt => opt.MapFrom(src => src.series)); + .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.VolumeId, opt => opt.MapFrom(src => src.Bookmark.VolumeId)) + .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.Series, opt => opt.MapFrom(src => src.Series)); CreateMap(); CreateMap(); CreateMap(); diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index 292f1ff2f..4bc057835 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -69,6 +69,7 @@ public static class FilterFieldValueConverter .ToList(), typeof(IList)), FilterField.WantToRead => (bool.Parse(value), typeof(bool)), FilterField.ReadProgress => (int.Parse(value), typeof(int)), + FilterField.ReadingDate => (DateTime.Parse(value), typeof(DateTime?)), FilterField.Formats => (value.Split(',') .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) .ToList(), typeof(IList)), diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 87f064f2a..843844416 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -20,6 +20,7 @@ export enum SortField { LastChapterAdded = 4, TimeToRead = 5, ReleaseYear = 6, + ReadProgress = 7, } export const allSortFields = Object.keys(SortField) diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index 8dec89f95..e29b7d1d2 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -27,7 +27,8 @@ export enum FilterField ReadTime = 23, Path = 24, FilePath = 25, - WantToRead = 26 + WantToRead = 26, + ReadingDate = 27 } export const allFields = Object.keys(FilterField) diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 393aade6c..f2a659ceb 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -205,7 +205,7 @@ export class ActionFactoryService { action: Action.Scan, title: 'scan-library', callback: this.dummyCallback, - requiresAdmin: false, + requiresAdmin: true, children: [], }, { diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html index 4894d96e0..8808e4d60 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html @@ -1,47 +1,73 @@ + +
+
+
+ +
- -
-
- -
+
+ +
-
- -
+
+ + + + + + + + + + + + +
+ + +
-
- - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - -
+
- -
- +
+ + {{t(UiLabel.unit)}} + + +
+ + +
+ +
diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss index ee0c6d278..4abcdcdda 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss @@ -1,3 +1,23 @@ ::ng-deep .select2-selection__rendered { 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; +} diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts index d61a4df86..5261f9349 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts @@ -7,7 +7,7 @@ import { inject, Input, OnInit, - Output, + Output, ViewChild, } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; 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 {Select2Module, Select2Option} from "ng-select2-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 { Text = 1, Number = 2, 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 = 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 NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating]; 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.Formats, FilterField.CollectionTags, FilterField.Tags ]; -const BooleanFields = [FilterField.WantToRead] +const BooleanFields = [FilterField.WantToRead]; +const DateFields = [FilterField.ReadingDate]; const DropdownFieldsWithoutMustContains = [ FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus @@ -59,7 +84,8 @@ const StringComparisons = [FilterComparison.Equal, FilterComparison.BeginsWith, FilterComparison.EndsWith, 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, FilterComparison.NotEqual, FilterComparison.LessThan, @@ -91,7 +117,11 @@ const BooleanComparisons = [ NgIf, Select2Module, NgTemplateOutlet, - TagBadgeComponent + TagBadgeComponent, + NgbTooltip, + TranslocoDirective, + NgbDatepicker, + NgbInputDatepicker ], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -105,8 +135,10 @@ export class MetadataFilterRowComponent implements OnInit { @Input() availableFields: Array = allFields; @Output() filterStatement = new EventEmitter(); + private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); + private readonly dateParser = inject(NgbDateParserFormatter); formGroup: FormGroup = new FormGroup({ 'comparison': new FormControl(FilterComparison.Equal, []), @@ -119,6 +151,12 @@ export class MetadataFilterRowComponent implements OnInit { loaded: boolean = false; 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() { const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison; 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(_ => { - 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! - }; - - // 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.propagateFilterUpdate(); }); this.loaded = true; 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() { 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); } else if (BooleanFields.includes(this.preset.field)) { 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(',')) { this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10))); } else { @@ -281,6 +328,16 @@ export class MetadataFilterRowComponent implements OnInit { 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)) { this.validComparisons$.next(BooleanComparisons); 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(); + } + } diff --git a/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts b/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts index 7fe77b79b..10e4399bc 100644 --- a/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts +++ b/UI/Web/src/app/metadata-filter/_pipes/filter-field.pipe.ts @@ -64,6 +64,8 @@ export class FilterFieldPipe implements PipeTransform { return translate('filter-field-pipe.file-path'); case FilterField.WantToRead: return translate('filter-field-pipe.want-to-read'); + case FilterField.ReadingDate: + return translate('filter-field-pipe.read-date'); default: throw new Error(`Invalid FilterField value: ${value}`); } diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 5cbf39384..99210e5a1 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -1,12 +1,12 @@ -
+
-
+
{{t('filter-title')}} @@ -51,6 +51,16 @@
+ + + + + + + + + +
@@ -63,15 +73,13 @@ -
- +
+ +
-
- -
-
+
diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index 8ffb1ac38..3ffdc9654 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -30,6 +30,8 @@ import {MetadataService} from "../_services/metadata.service"; import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; import {FilterService} from "../_services/filter.service"; import {ToastrService} from "ngx-toastr"; +import {Select2Module, Select2Option, Select2UpdateEvent} from "ng-select2-component"; +import {SmartFilter} from "../_models/metadata/v2/smart-filter"; @Component({ selector: 'app-metadata-filter', @@ -38,7 +40,7 @@ import {ToastrService} from "ngx-toastr"; changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, 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 { @@ -78,16 +80,22 @@ export class MetadataFilterComponent implements OnInit { allSortFields = allSortFields; allFilterFields = allFields; - handleFilters(filter: SeriesFilterV2) { - this.filterV2 = filter; - } - + smartFilters!: Array; private readonly cdRef = inject(ChangeDetectorRef); 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 { if (this.filterSettings === undefined) { @@ -106,6 +114,11 @@ export class MetadataFilterComponent implements OnInit { this.loadFromPresetsAndSetup(); } + updateFilterValue(event: Select2UpdateEvent) { + console.log('event: ', event); + } + + close() { this.filterOpen.emit(false); this.filteringCollapsed = true; @@ -137,6 +150,10 @@ export class MetadataFilterComponent implements OnInit { return clonedObj; } + handleFilters(filter: SeriesFilterV2) { + this.filterV2 = filter; + } + loadFromPresetsAndSetup() { this.fullyLoaded = false; @@ -187,7 +204,7 @@ export class MetadataFilterComponent implements OnInit { apply() { this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!}); - + if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) { this.toggleSelected(); } diff --git a/UI/Web/src/app/pipe/sort-field.pipe.ts b/UI/Web/src/app/pipe/sort-field.pipe.ts index be9679e76..1fe878860 100644 --- a/UI/Web/src/app/pipe/sort-field.pipe.ts +++ b/UI/Web/src/app/pipe/sort-field.pipe.ts @@ -14,17 +14,19 @@ export class SortFieldPipe implements PipeTransform { transform(value: SortField): string { switch (value) { case SortField.SortName: - return this.translocoService.translate('sort-field-pipe.sort-name') + return this.translocoService.translate('sort-field-pipe.sort-name'); case SortField.Created: - return this.translocoService.translate('sort-field-pipe.created') + return this.translocoService.translate('sort-field-pipe.created'); case SortField.LastModified: - return this.translocoService.translate('sort-field-pipe.last-modified') + return this.translocoService.translate('sort-field-pipe.last-modified'); 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: - return this.translocoService.translate('sort-field-pipe.time-to-read') + return this.translocoService.translate('sort-field-pipe.time-to-read'); 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'); } } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index fa204a2c4..cfa6b5e02 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -74,7 +74,7 @@
@@ -101,7 +101,7 @@
-