More cleanup by migrating some stuff to signals and cleaning up the generic mess that this has become.

This commit is contained in:
Joseph Milazzo 2025-06-13 17:41:25 -05:00
parent e99822cf28
commit 9b35d20510
11 changed files with 49 additions and 73 deletions

View file

@ -50,8 +50,8 @@ export const mangaFormatFilters = [
} }
]; ];
export interface FilterEvent { export interface FilterEvent<TFilter extends number = number, TSort extends number = number> {
filterV2: FilterV2<number>; filterV2: FilterV2<TFilter, TSort>;
isFirst: boolean; isFirst: boolean;
} }

View file

@ -188,7 +188,7 @@ export class AllSeriesComponent implements OnInit {
return this.title === translate('side-nav.all-series') && this.filter && this.filter.statements.length === 1 && this.filter.statements[0].comparison === FilterComparison.Equal return this.title === translate('side-nav.all-series') && this.filter && this.filter.statements.length === 1 && this.filter.statements[0].comparison === FilterComparison.Equal
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<FilterField, SortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;

View file

@ -211,7 +211,7 @@ export class BookmarksComponent implements OnInit {
this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id)); this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id));
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<FilterField, SortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;

View file

@ -1,12 +1,4 @@
import { import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, inject} from '@angular/core';
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
EventEmitter,
inject,
OnInit
} from '@angular/core';
import { import {
SideNavCompanionBarComponent SideNavCompanionBarComponent
} from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; } from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
@ -51,7 +43,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
styleUrl: './browse-authors.component.scss', styleUrl: './browse-authors.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class BrowseAuthorsComponent implements OnInit { export class BrowseAuthorsComponent {
protected readonly PersonSortField = PersonSortField; protected readonly PersonSortField = PersonSortField;
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
@ -85,12 +77,6 @@ export class BrowseAuthorsComponent implements OnInit {
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => { this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.filter = data['filter'] as FilterV2<PersonFilterField, PersonSortField>; this.filter = data['filter'] as FilterV2<PersonFilterField, PersonSortField>;
// if (this.filter == null) {
// this.filter
// }
console.log('filter from url:', this.filter);
this.filterActiveCheck = this.filterUtilityService.createPersonV2Filter(); this.filterActiveCheck = this.filterUtilityService.createPersonV2Filter();
this.filterActiveCheck!.statements.push({value: `${PersonRole.Writer},${PersonRole.CoverArtist}`, field: PersonFilterField.Role, comparison: FilterComparison.Contains}); this.filterActiveCheck!.statements.push({value: `${PersonRole.Writer},${PersonRole.CoverArtist}`, field: PersonFilterField.Role, comparison: FilterComparison.Contains});
this.filterSettings.presetsV2 = this.filter; this.filterSettings.presetsV2 = this.filter;
@ -101,14 +87,7 @@ export class BrowseAuthorsComponent implements OnInit {
} }
ngOnInit() {
}
loadData() { loadData() {
console.log('loading data with filter', this.filter!);
if (!this.filter) { if (!this.filter) {
this.filter = this.filterUtilityService.createPersonV2Filter(); this.filter = this.filterUtilityService.createPersonV2Filter();
this.filter.statements.push({value: `${PersonRole.Writer},${PersonRole.CoverArtist}`, field: PersonFilterField.Role, comparison: FilterComparison.Contains}); this.filter.statements.push({value: `${PersonRole.Writer},${PersonRole.CoverArtist}`, field: PersonFilterField.Role, comparison: FilterComparison.Contains});
@ -128,7 +107,7 @@ export class BrowseAuthorsComponent implements OnInit {
this.router.navigate(['person', person.name]); this.router.navigate(['person', person.name]);
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<PersonFilterField, PersonSortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;

View file

@ -24,7 +24,9 @@
</div> </div>
} }
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter> @if (filterSettings) {
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
}
<div class="viewport-container ms-1" [ngClass]="{'empty': items.length === 0 && !isLoading}"> <div class="viewport-container ms-1" [ngClass]="{'empty': items.length === 0 && !isLoading}">
<div class="content-container"> <div class="content-container">
@ -33,7 +35,7 @@
<p><ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container></p> <p><ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container></p>
} }
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll"> <virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items()" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll">
<div class="grid row g-0" #container> <div class="grid row g-0" #container>
@for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) { @for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) {
<div class="card col-auto mt-2 mb-2" <div class="card col-auto mt-2 mb-2"
@ -54,11 +56,13 @@
</div> </div>
<ng-template #cardTemplate> <ng-template #cardTemplate>
<virtual-scroller #scroll [items]="items" [bufferAmount]="bufferAmount"> <virtual-scroller #scroll [items]="items()" [bufferAmount]="bufferAmount">
<div class="grid row g-0" #container> <div class="grid row g-0" #container>
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" (click)="tryToSaveJumpKey()" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i"> <div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" (click)="tryToSaveJumpKey()" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container> <ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</div> </div>
</div> </div>
</virtual-scroller> </virtual-scroller>

View file

@ -3,6 +3,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
ContentChild, ContentChild,
DestroyRef, DestroyRef,
ElementRef, ElementRef,
@ -15,16 +16,17 @@ import {
OnChanges, OnChanges,
OnInit, OnInit,
Output, Output,
signal,
Signal, Signal,
SimpleChange, SimpleChange,
SimpleChanges, SimpleChanges,
TemplateRef, TemplateRef,
TrackByFunction, TrackByFunction,
ViewChild ViewChild,
WritableSignal
} from '@angular/core'; } from '@angular/core';
import {NavigationStart, Router} from '@angular/router'; import {NavigationStart, Router} from '@angular/router';
import {VirtualScrollerComponent, VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller'; import {VirtualScrollerComponent, VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
import {JumpKey} from 'src/app/_models/jumpbar/jump-key'; import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
import {Library} from 'src/app/_models/library/library'; import {Library} from 'src/app/_models/library/library';
@ -64,11 +66,11 @@ const ANIMATION_TIME_MS = 0;
TranslocoDirective, NgTemplateOutlet, NgClass, NgForOf], TranslocoDirective, NgTemplateOutlet, NgClass, NgForOf],
templateUrl: './card-detail-layout.component.html', templateUrl: './card-detail-layout.component.html',
styleUrls: ['./card-detail-layout.component.scss'], styleUrls: ['./card-detail-layout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true
}) })
export class CardDetailLayoutComponent implements OnInit, OnChanges { export class CardDetailLayoutComponent<TFilter extends number, TSort extends number> implements OnInit, OnChanges {
private readonly filterUtilityService = inject(FilterUtilitiesService);
protected readonly utilityService = inject(UtilityService); protected readonly utilityService = inject(UtilityService);
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly jumpbarService = inject(JumpbarService); private readonly jumpbarService = inject(JumpbarService);
@ -79,8 +81,11 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
header: Signal<string> = input(''); header: Signal<string> = input('');
@Input() isLoading: boolean = false; @Input() isLoading: boolean = false;
@Input() items: any[] = []; //@Input() items: any[] = [];
@Input() pagination!: Pagination; @Input() pagination!: Pagination;
items = input.required<any[]>();
/** /**
* Parent scroll for virtualize pagination * Parent scroll for virtualize pagination
*/ */
@ -100,7 +105,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
* A trackBy to help with rendering. This is required as without it there are issues when scrolling * A trackBy to help with rendering. This is required as without it there are issues when scrolling
*/ */
@Input({required: true}) trackByIdentity!: TrackByFunction<any>; @Input({required: true}) trackByIdentity!: TrackByFunction<any>;
@Input() filterSettings!: FilterSettingsBase; @Input() filterSettings: FilterSettingsBase | undefined = undefined;
entityType = input<ValidFilterEntity | 'other'>(); entityType = input<ValidFilterEntity | 'other'>();
@Input() refresh!: EventEmitter<void>; @Input() refresh!: EventEmitter<void>;
@ -108,7 +113,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
/** /**
* Will force the jumpbar to be disabled - in cases where you're not using a traditional filter config * Will force the jumpbar to be disabled - in cases where you're not using a traditional filter config
*/ */
@Input() customSort: boolean = false; customSort = input(false);
@Input() jumpBarKeys: Array<JumpKey> = []; // This is approx 784 pixels tall, original keys @Input() jumpBarKeys: Array<JumpKey> = []; // This is approx 784 pixels tall, original keys
jumpBarKeysToRender: Array<JumpKey> = []; // What is rendered on screen jumpBarKeysToRender: Array<JumpKey> = []; // What is rendered on screen
@ -126,12 +131,15 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
updateApplied: number = 0; updateApplied: number = 0;
bufferAmount: number = 1; bufferAmount: number = 1;
/**
* Pass the filter object optionally. If not passed, will create a SeriesFilter by default
*/
filter: FilterV2<number> | undefined = undefined;
filterSignal: WritableSignal<FilterV2<number, number> | undefined> = signal(undefined);
hasCustomSort = computed(() => {
if (this.customSort()) return true;
if (this.filteringDisabled) return false;
const filter = this.filterSignal();
return filter?.sortOptions?.sortField != SortField.SortName || !filter?.sortOptions.isAscending;
});
constructor(@Inject(DOCUMENT) private document: Document) {} constructor(@Inject(DOCUMENT) private document: Document) {}
@ -149,19 +157,10 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
if (this.trackByIdentity === undefined) { if (this.trackByIdentity === undefined) {
this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
} }
// This shouldn't be needed as filter is a temp variable to check if a custom sort is present
// if (!this.filter) {
// this.filter = this.filterUtilityService.createSeriesV2Filter();
// }
if (this.filterSettings === undefined) {
this.filterSettings = this.filterUtilityService.getDefaultSettings(this.entityType());
console.log('[card detail layout] Filter Setting is not set, defaulting: ', this.filterSettings);
this.cdRef.markForCheck();
}
if (this.pagination === undefined) { if (this.pagination === undefined) {
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1}; const items = this.items();
this.pagination = {currentPage: 1, itemsPerPage: items.length, totalItems: items.length, totalPages: 1};
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -200,23 +199,16 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
} }
} }
hasCustomSort() {
if (this.customSort) return true;
if (this.filteringDisabled) return false;
return this.filter?.sortOptions?.sortField != SortField.SortName || !this.filter?.sortOptions.isAscending;
}
performAction(action: ActionItem<any>) { performAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') { if (typeof action.callback === 'function') {
action.callback(action, undefined); action.callback(action, undefined);
} }
} }
applyMetadataFilter(event: FilterEvent) { applyMetadataFilter(event: FilterEvent<number, number>) {
this.applyFilter.emit(event); this.applyFilter.emit(event as FilterEvent<TFilter, TSort>);
this.updateApplied++; this.updateApplied++;
this.filter = event.filterV2; this.filterSignal.set(event.filterV2);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View file

@ -282,7 +282,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
}); });
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<FilterField, SortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;

View file

@ -316,7 +316,7 @@ export class LibraryDetailComponent implements OnInit {
} }
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<FilterField, SortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;

View file

@ -23,7 +23,7 @@ import {ToggleService} from '../_services/toggle.service';
import {FilterV2} from '../_models/metadata/v2/filter-v2'; import {FilterV2} from '../_models/metadata/v2/filter-v2';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {DrawerComponent} from '../shared/drawer/drawer.component'; import {DrawerComponent} from '../shared/drawer/drawer.component';
import {AsyncPipe, JsonPipe, NgClass, NgTemplateOutlet} from '@angular/common'; import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco"; import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco";
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component"; import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
import {FilterService} from "../_services/filter.service"; import {FilterService} from "../_services/filter.service";
@ -53,7 +53,7 @@ import {FilterUtilitiesService} from "../shared/_services/filter-utilities.servi
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgTemplateOutlet, DrawerComponent, imports: [NgTemplateOutlet, DrawerComponent,
ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule, ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule,
MetadataBuilderComponent, NgClass, SortButtonComponent, JsonPipe] MetadataBuilderComponent, NgClass, SortButtonComponent]
}) })
export class MetadataFilterComponent<TFilter extends number = number, TSort extends number = number> implements OnInit { export class MetadataFilterComponent<TFilter extends number = number, TSort extends number = number> implements OnInit {
@ -74,7 +74,7 @@ export class MetadataFilterComponent<TFilter extends number = number, TSort exte
filterSettings = input.required<FilterSettingsBase<TFilter, TSort>>(); filterSettings = input.required<FilterSettingsBase<TFilter, TSort>>();
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter(); @Output() applyFilter: EventEmitter<FilterEvent<TFilter, TSort>> = new EventEmitter();
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse; @ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
@ -231,7 +231,7 @@ export class MetadataFilterComponent<TFilter extends number = number, TSort exte
} }
apply() { apply() {
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!}); this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!} as FilterEvent<TFilter, TSort>);
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) { if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {
this.toggleSelected(); this.toggleSelected();

View file

@ -31,7 +31,8 @@ import {User} from "../../../_models/user";
templateUrl: './reading-lists.component.html', templateUrl: './reading-lists.component.html',
styleUrls: ['./reading-lists.component.scss'], styleUrls: ['./reading-lists.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SideNavCompanionBarComponent, CardActionablesComponent, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective, BulkOperationsComponent] imports: [SideNavCompanionBarComponent, CardActionablesComponent, CardDetailLayoutComponent, CardItemComponent,
DecimalPipe, TranslocoDirective, BulkOperationsComponent]
}) })
export class ReadingListsComponent implements OnInit { export class ReadingListsComponent implements OnInit {
protected readonly WikiLink = WikiLink; protected readonly WikiLink = WikiLink;

View file

@ -63,7 +63,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
isLoading: boolean = true; isLoading: boolean = true;
series: Array<Series> = []; series: Array<Series> = [];
pagination: Pagination = new Pagination(); pagination: Pagination = new Pagination();
filter: FilterV2<FilterField> | undefined = undefined; filter: FilterV2<FilterField, SortField> | undefined = undefined;
filterSettings: SeriesFilterSettings = new SeriesFilterSettings(); filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
refresh: EventEmitter<void> = new EventEmitter(); refresh: EventEmitter<void> = new EventEmitter();
@ -182,7 +182,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
}); });
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent<FilterField, SortField>) {
if (data.filterV2 === undefined) return; if (data.filterV2 === undefined) return;
this.filter = data.filterV2; this.filter = data.filterV2;