Smart Filter Encoding Fix (#2387)

This commit is contained in:
Joe Milazzo 2023-11-02 08:35:43 -05:00 committed by GitHub
parent b6d4938e22
commit 9894a2623c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 677 additions and 471 deletions

View file

@ -1,8 +1,15 @@
export interface Pagination {
export class Pagination {
currentPage: number;
itemsPerPage: number;
totalItems: number;
totalPages: number;
constructor() {
this.currentPage = 0;
this.itemsPerPage = 0;
this.totalItems = 0;
this.totalPages = 0;
}
}
export class PaginatedResult<T> {

View file

@ -1,16 +1,14 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {map, tap} from 'rxjs/operators';
import {of, ReplaySubject, switchMap} from 'rxjs';
import {tap} from 'rxjs/operators';
import {of} from 'rxjs';
import {environment} from 'src/environments/environment';
import {Genre} from '../_models/metadata/genre';
import {AgeRating} from '../_models/metadata/age-rating';
import {AgeRatingDto} from '../_models/metadata/age-rating-dto';
import {Language} from '../_models/metadata/language';
import {PublicationStatusDto} from '../_models/metadata/publication-status-dto';
import {Person, PersonRole} from '../_models/metadata/person';
import {Tag} from '../_models/tag';
import {TextResonse} from '../_types/text-response';
import {FilterComparison} from '../_models/metadata/v2/filter-comparison';
import {FilterField} from '../_models/metadata/v2/filter-field';
import {Router} from "@angular/router";
@ -93,10 +91,6 @@ export class MetadataService {
return this.httpClient.get<Array<Person>>(this.baseUrl + 'metadata/people-by-role?role=' + role);
}
// getChapterSummary(chapterId: number) {
// return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse);
// }
createDefaultFilterDto(): SeriesFilterV2 {
return {
statements: [] as FilterStatement[],

View file

@ -6,7 +6,7 @@
<h6 subtitle *ngIf="pagination">{{t('series-count', {num: pagination.totalItems | number})}}</h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingSeries"
[items]="series"
[trackByIdentity]="trackByIdentity"

View file

@ -47,7 +47,7 @@ export class AllSeriesComponent implements OnInit {
title: string = translate('all-series.title');
series: Series[] = [];
loadingSeries = false;
pagination!: Pagination;
pagination: Pagination = new Pagination();
filter: SeriesFilterV2 | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -113,20 +113,18 @@ export class AllSeriesComponent implements OnInit {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.title = this.route.snapshot.queryParamMap.get('title') || this.title;
this.titleService.setTitle('Kavita - ' + this.title);
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter;
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
this.title = this.route.snapshot.queryParamMap.get('title') || this.filter.name || this.title;
this.titleService.setTitle('Kavita - ' + this.title);
this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot);
if (this.filter.statements.length === 0) {
this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.cdRef.markForCheck();
this.cdRef.markForCheck();
});
}
ngOnInit(): void {
@ -155,16 +153,20 @@ export class AllSeriesComponent implements OnInit {
if (data.filterV2 === undefined) return;
this.filter = data.filterV2;
if (!data.isFirst) {
this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter);
if (data.isFirst) {
this.loadPage();
return;
}
this.loadPage();
this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => {
this.loadPage();
});
}
loadPage() {
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.loadingSeries = true;
this.title = this.route.snapshot.queryParamMap.get('title') || this.filter?.name || translate('all-series.title');
this.cdRef.markForCheck();
this.seriesService.getAllSeriesV2(undefined, undefined, this.filter!).pipe(take(1)).subscribe(series => {
this.series = series.result;

View file

@ -6,7 +6,7 @@
<h6 subtitle>{{t('series-count', {num: series.length | number})}}</h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingBookmarks"
[items]="series"
[filterSettings]="filterSettings"

View file

@ -26,7 +26,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { ReaderService } from 'src/app/_services/reader.service';
import { SeriesService } from 'src/app/_services/series.service';
import { DecimalPipe } from '@angular/common';
import {DecimalPipe, NgIf} from '@angular/common';
import { CardItemComponent } from '../../../cards/card-item/card-item.component';
import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component';
import { BulkOperationsComponent } from '../../../cards/bulk-operations/bulk-operations.component';
@ -36,12 +36,12 @@ import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
import {Title} from "@angular/platform-browser";
@Component({
selector: 'app-bookmarks',
templateUrl: './bookmarks.component.html',
styleUrls: ['./bookmarks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SideNavCompanionBarComponent, BulkOperationsComponent, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective]
selector: 'app-bookmarks',
templateUrl: './bookmarks.component.html',
styleUrls: ['./bookmarks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SideNavCompanionBarComponent, BulkOperationsComponent, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective, NgIf]
})
export class BookmarksComponent implements OnInit {
@ -54,7 +54,7 @@ export class BookmarksComponent implements OnInit {
actions: ActionItem<Series>[] = [];
jumpbarKeys: Array<JumpKey> = [];
pagination!: Pagination;
pagination: Pagination = new Pagination();
filter: SeriesFilterV2 | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -73,20 +73,23 @@ export class BookmarksComponent implements OnInit {
private router: Router, private readonly cdRef: ChangeDetectorRef,
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute,
private jumpbarService: JumpbarService, private titleService: Title) {
this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot);
if (this.filter.statements.length === 0) {
this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.filterSettings.statementLimit = 1;
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter;
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.filterSettings.statementLimit = 1;
this.cdRef.markForCheck();
});
this.titleService.setTitle('Kavita - ' + translate('bookmarks.title'));
}
ngOnInit(): void {
this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
}
@ -142,7 +145,7 @@ export class BookmarksComponent implements OnInit {
this.readerService.clearMultipleBookmarks(seriesIds).subscribe(() => {
this.toastr.success(this.translocoService.translate('bookmarks.delete-success'));
this.bulkSelectionService.deselectAll();
this.loadBookmarks();
this.loadPage();
});
break;
default:
@ -150,7 +153,7 @@ export class BookmarksComponent implements OnInit {
}
}
loadBookmarks() {
loadPage() {
this.loadingBookmarks = true;
this.cdRef.markForCheck();
@ -210,11 +213,13 @@ export class BookmarksComponent implements OnInit {
if (data.filterV2 === undefined) return;
this.filter = data.filterV2;
if (!data.isFirst) {
this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter);
if (data.isFirst) {
this.loadPage();
return;
}
this.loadBookmarks();
this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => {
this.loadPage();
});
}
}

View file

@ -170,7 +170,7 @@ export class BulkSelectionService {
private applyFilter(action: ActionItem<any>, allowedActions: Array<Action>) {
var ret = false;
let ret = false;
if (action.action === Action.Submenu || allowedActions.includes(action.action)) {
// Do something
ret = true;

View file

@ -22,8 +22,8 @@
</div>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
header="Series"
<app-card-detail-layout *ngIf="filter"
[header]="t('series-header')"
[isLoading]="isLoading"
[items]="series"
[pagination]="pagination"

View file

@ -72,7 +72,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
collectionTag!: CollectionTag;
isLoading: boolean = true;
series: Array<Series> = [];
pagination!: Pagination;
pagination: Pagination = new Pagination();
collectionTagActions: ActionItem<CollectionTag>[] = [];
filter: SeriesFilterV2 | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
@ -168,19 +168,19 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
}
const tagId = parseInt(routeId, 10);
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter;
this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot);
if (this.filter.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) {
this.filter!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal});
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal});
this.filterSettings.presetsV2 = this.filter;
if (this.filter.statements.filter(stmt => stmt.field === FilterField.CollectionTags).length === 0) {
this.filter!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal});
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal});
this.filterSettings.presetsV2 = this.filter;
this.cdRef.markForCheck();
this.cdRef.markForCheck();
this.updateTag(tagId);
this.updateTag(tagId);
});
}
ngOnInit(): void {
@ -252,11 +252,14 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
if (data.filterV2 === undefined) return;
this.filter = data.filterV2;
if (!data.isFirst) {
this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter);
if (data.isFirst) {
this.loadPage();
return;
}
this.loadPage();
this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => {
this.loadPage();
});
}
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
@ -284,4 +287,5 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
});
}
protected readonly undefined = undefined;
}

View file

@ -147,7 +147,10 @@ export class DashboardComponent implements OnInit {
s.api = this.seriesService.getRecentlyUpdatedSeries();
break;
case StreamType.SmartFilter:
s.api = this.seriesService.getAllSeriesV2(0, 20, this.filterUtilityService.decodeSeriesFilter(s.smartFilterEncoded!))
s.api = this.filterUtilityService.decodeFilter(s.smartFilterEncoded!).pipe(
switchMap(filter => {
return this.seriesService.getAllSeriesV2(0, 20, filter);
}))
.pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
break;
case StreamType.MoreInGenre:
@ -195,7 +198,7 @@ export class DashboardComponent implements OnInit {
filter.sortOptions.sortField = SortField.LastChapterAdded;
filter.sortOptions.isAscending = false;
}
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
} else if (sectionTitle.toLowerCase() === 'on deck') {
const params: any = {};
params['page'] = 1;
@ -208,7 +211,7 @@ export class DashboardComponent implements OnInit {
filter.sortOptions.sortField = SortField.LastChapterAdded;
filter.sortOptions.isAscending = false;
}
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
} else if (sectionTitle.toLowerCase() === 'newly added series') {
const params: any = {};
params['page'] = 1;
@ -218,14 +221,14 @@ export class DashboardComponent implements OnInit {
filter.sortOptions.sortField = SortField.Created;
filter.sortOptions.isAscending = false;
}
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
} else if (sectionTitle.toLowerCase() === 'more in genre') {
const params: any = {};
params['page'] = 1;
params['title'] = translate('more-in-genre-title', {genre: this.genre?.title});
const filter = this.filterUtilityService.createSeriesV2Filter();
filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains});
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
}
}

View file

@ -7,7 +7,7 @@
<h6 subtitle class="subtitle-with-actionables" *ngIf="active.fragment === ''">{{t('common.series-count', {num: pagination.totalItems | number})}} </h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingSeries"
[items]="series"
[pagination]="pagination"

View file

@ -60,9 +60,9 @@ export class LibraryDetailComponent implements OnInit {
libraryName = '';
series: Series[] = [];
loadingSeries = false;
pagination!: Pagination;
pagination: Pagination = {currentPage: 0, totalPages: 0, totalItems: 0, itemsPerPage: 0};
actions: ActionItem<Library>[] = [];
filterV2: SeriesFilterV2 | undefined = undefined;
filter: SeriesFilterV2 | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
@ -158,19 +158,20 @@ export class LibraryDetailComponent implements OnInit {
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
this.filterV2 = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot);
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter;
if (this.filterV2.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) {
this.filterV2!.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal});
}
if (this.filter.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) {
this.filter!.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal});
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal});
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal});
this.filterSettings.presetsV2 = this.filterV2;
this.filterSettings.presetsV2 = this.filter;
this.cdRef.markForCheck();
this.cdRef.markForCheck();
});
}
@ -179,7 +180,7 @@ export class LibraryDetailComponent implements OnInit {
if (event.event === EVENTS.SeriesAdded) {
const seriesAdded = event.payload as SeriesAddedEvent;
if (seriesAdded.libraryId !== this.libraryId) return;
if (!this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck)) {
if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) {
this.loadPage();
return;
}
@ -199,7 +200,7 @@ export class LibraryDetailComponent implements OnInit {
} else if (event.event === EVENTS.SeriesRemoved) {
const seriesRemoved = event.payload as SeriesRemovedEvent;
if (seriesRemoved.libraryId !== this.libraryId) return;
if (!this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck)) {
if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) {
this.loadPage();
return;
}
@ -257,21 +258,24 @@ export class LibraryDetailComponent implements OnInit {
updateFilter(data: FilterEvent) {
if (data.filterV2 === undefined) return;
this.filterV2 = data.filterV2;
this.filter = data.filterV2;
if (!data.isFirst) {
this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filterV2);
if (data.isFirst) {
this.loadPage();
return;
}
this.loadPage();
this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => {
this.loadPage();
});
}
loadPage() {
this.loadingSeries = true;
this.filterActive = !this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.cdRef.markForCheck();
this.seriesService.getSeriesForLibraryV2(undefined, undefined, this.filterV2)
this.seriesService.getSeriesForLibraryV2(undefined, undefined, this.filter)
.subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;
@ -282,4 +286,5 @@ export class LibraryDetailComponent implements OnInit {
}
trackByIdentity = (index: number, item: Series) => `${item.id}_${item.name}_${item.localizedName}_${item.pagesRead}`;
protected readonly undefined = undefined;
}

View file

@ -136,7 +136,7 @@ export class NavHeaderComponent implements OnInit {
filter.statements = [statement];
params['page'] = 1;
this.clearSearch();
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params);
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
}
goToOther(field: FilterField, value: string) {

View file

@ -234,6 +234,6 @@ export class ReadingListDetailComponent implements OnInit {
}
goToCharacter(character: Person) {
this.filterUtilityService.applyFilter(['all-series'], FilterField.Characters, FilterComparison.Contains, character.id + '');
this.filterUtilityService.applyFilter(['all-series'], FilterField.Characters, FilterComparison.Contains, character.id + '').subscribe();
}
}

View file

@ -123,5 +123,4 @@ export class ReadingListsComponent implements OnInit {
handleClick(list: ReadingList) {
this.router.navigateByUrl('lists/' + list.id);
}
}

View file

@ -24,12 +24,12 @@ export class MetadataDetailComponent {
@ContentChild('titleTemplate') titleTemplate!: TemplateRef<any>;
@ContentChild('itemTemplate') itemTemplate?: TemplateRef<any>;
private readonly filterUtilitiesService = inject(FilterUtilitiesService);
private readonly filterUtilityService = inject(FilterUtilitiesService);
protected readonly TagBadgeCursor = TagBadgeCursor;
goTo(queryParamName: FilterField, filter: any) {
if (queryParamName === FilterField.None) return;
this.filterUtilitiesService.applyFilter(['library', this.libraryId], queryParamName, FilterComparison.Equal, filter);
this.filterUtilityService.applyFilter(['library', this.libraryId], queryParamName, FilterComparison.Equal, filter).subscribe();
}
}

View file

@ -102,7 +102,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
goTo(queryParamName: FilterField, filter: any) {
this.filterUtilityService.applyFilter(['library', this.series.libraryId], queryParamName,
FilterComparison.Equal, filter);
FilterComparison.Equal, filter).subscribe();
}
navigate(basePage: string, id: number) {

View file

@ -1,6 +1,5 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Params, Router} from '@angular/router';
import {Pagination} from 'src/app/_models/pagination';
import {SortField, SortOptions} from 'src/app/_models/metadata/series-filter';
import {MetadataService} from "../../_services/metadata.service";
import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2";
@ -8,245 +7,93 @@ import {FilterStatement} from "../../_models/metadata/v2/filter-statement";
import {FilterCombination} from "../../_models/metadata/v2/filter-combination";
import {FilterField} from "../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {HttpClient} from "@angular/common/http";
import {TextResonse} from "../../_types/text-response";
import {environment} from "../../../environments/environment";
import {map, tap} from "rxjs/operators";
import {of, switchMap} from "rxjs";
const sortOptionsKey = 'sortOptions=';
const statementsKey = 'stmts=';
const limitToKey = 'limitTo=';
const combinationKey = 'combination=';
@Injectable({
providedIn: 'root'
})
export class FilterUtilitiesService {
constructor(private metadataService: MetadataService, private router: Router) {}
private apiUrl = environment.apiUrl;
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
const dto: SeriesFilterV2 = {
statements: [this.metadataService.createDefaultFilterStatement(filter, comparison, value + '')],
combination: FilterCombination.Or,
limitTo: 0
};
constructor(private metadataService: MetadataService, private router: Router, private http: HttpClient) {}
const url = this.urlFromFilterV2(page.join('/') + '?', dto);
return this.router.navigateByUrl(url);
}
applyFilterWithParams(page: Array<any>, filter: SeriesFilterV2, extraParams: Params) {
let url = this.urlFromFilterV2(page.join('/') + '?', filter);
url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join('');
return this.router.navigateByUrl(url, extraParams);
}
/**
* Updates the window location with a custom url based on filter and pagination objects
* @param pagination
* @param filter
*/
updateUrlFromFilterV2(pagination: Pagination, filter: SeriesFilterV2 | undefined) {
const params = '?page=' + pagination.currentPage + '&';
const url = this.urlFromFilterV2(window.location.href.split('?')[0] + params, filter);
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
}
private replacePaginationOnUrl(url: string, pagination: Pagination) {
return url.replace(/page=\d+/i, 'page=' + pagination.currentPage);
}
/**
* Will fetch current page from route if present
* @param snapshot to fetch page from. Must be from component else may get stale data
* @param itemsPerPage If you want pagination, pass non-zero number
* @returns A default pagination object
*/
pagination(snapshot: ActivatedRouteSnapshot, itemsPerPage: number = 0): Pagination {
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage, totalItems: 0, totalPages: 1};
}
/**
* Returns the current url with query params for the filter
* @param currentUrl Full url, with ?page=1 as a minimum
* @param filter Filter to build url off
* @returns current url with query params added
*/
urlFromFilterV2(currentUrl: string, filter: SeriesFilterV2 | undefined) {
if (filter === undefined) return currentUrl;
return currentUrl + this.encodeSeriesFilter(filter);
}
encodeSeriesFilter(filter: SeriesFilterV2) {
const encodedStatements = this.encodeFilterStatements(filter.statements);
const encodedSortOptions = filter.sortOptions ? `${sortOptionsKey}${this.encodeSortOptions(filter.sortOptions)}` : '';
const encodedLimitTo = `${limitToKey}${filter.limitTo}`;
return `${this.encodeName(filter.name)}${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&${combinationKey}${filter.combination}`;
}
encodeName(name: string | undefined) {
if (name === undefined || name === '') return '';
return `name=${encodeURIComponent(name)}&`
}
encodeSortOptions(sortOptions: SortOptions) {
return `sortField=${sortOptions.sortField},isAscending=${sortOptions.isAscending}`;
}
encodeFilterStatements(statements: Array<FilterStatement>) {
if (statements.length === 0) return '';
return statementsKey + encodeURIComponent(statements.map(statement => {
const encodedComparison = `comparison=${statement.comparison}`;
const encodedField = `field=${statement.field}`;
const encodedValue = `value=${encodeURIComponent(statement.value)}`;
return `${encodedComparison}&${encodedField}&${encodedValue}`;
}).join(','));
}
decodeSeriesFilter(encodedFilter: string) {
const filter = this.metadataService.createDefaultFilterDto();
if (encodedFilter.includes('name=')) {
filter.name = decodeURIComponent(encodedFilter).split('name=')[1].split('&')[0];
}
const stmtsStartIndex = encodedFilter.indexOf(statementsKey);
let endIndex = encodedFilter.indexOf('&' + sortOptionsKey);
if (endIndex < 0) {
endIndex = encodedFilter.indexOf('&' + limitToKey);
}
if (stmtsStartIndex !== -1 || endIndex !== -1) {
// +1 is for the =
const stmtsEncoded = encodedFilter.substring(stmtsStartIndex + statementsKey.length, endIndex);
filter.statements = this.decodeFilterStatements(stmtsEncoded);
}
if (encodedFilter.includes(sortOptionsKey)) {
const optionsStartIndex = encodedFilter.indexOf('&' + sortOptionsKey);
const endIndex = encodedFilter.indexOf('&' + limitToKey);
const sortOptionsEncoded = encodedFilter.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
if (sortOptions) {
filter.sortOptions = sortOptions;
}
}
if (encodedFilter.includes(limitToKey)) {
const limitTo = decodeURIComponent(encodedFilter).split(limitToKey)[1].split('&')[0];
filter.limitTo = parseInt(limitTo, 10);
}
if (encodedFilter.includes(combinationKey)) {
const combo = decodeURIComponent(encodedFilter).split(combinationKey)[1].split('&')[0];;
filter.combination = parseInt(combo, 10) as FilterCombination;
}
return filter;
encodeFilter(filter: SeriesFilterV2 | undefined) {
return this.http.post<string>(this.apiUrl + 'filter/encode', filter, TextResonse);
}
decodeFilter(encodedFilter: string) {
return this.http.post<SeriesFilterV2>(this.apiUrl + 'filter/decode', {encodedFilter}).pipe(map(filter => {
if (filter == null) {
filter = this.metadataService.createDefaultFilterDto();
filter.statements.push(this.createSeriesV2DefaultStatement());
}
filterPresetsFromUrlV2(snapshot: ActivatedRouteSnapshot): SeriesFilterV2 {
const filter = this.metadataService.createDefaultFilterDto();
if (!window.location.href.includes('?')) return filter;
return filter;
}))
}
const queryParams = snapshot.queryParams;
updateUrlFromFilter(filter: SeriesFilterV2 | undefined) {
return this.encodeFilter(filter).pipe(tap(encodedFilter => {
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0]+ '?' + encodedFilter);
}));
}
if (queryParams.name) {
filter.name = queryParams.name;
}
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot) {
const filter = this.metadataService.createDefaultFilterDto();
filter.statements.push(this.createSeriesV2DefaultStatement());
if (!window.location.href.includes('?')) return of(filter);
const fullUrl = window.location.href.split('?')[1];
const stmtsStartIndex = fullUrl.indexOf(statementsKey);
let endIndex = fullUrl.indexOf('&' + sortOptionsKey);
if (endIndex < 0) {
endIndex = fullUrl.indexOf('&' + limitToKey);
}
return this.decodeFilter(window.location.href.split('?')[1]);
}
if (stmtsStartIndex !== -1 || endIndex !== -1) {
// +1 is for the =
const stmtsEncoded = fullUrl.substring(stmtsStartIndex + statementsKey.length, endIndex);
filter.statements = this.decodeFilterStatements(stmtsEncoded);
}
/**
* Applies and redirects to the passed page with the filter encoded
* @param page
* @param filter
* @param comparison
* @param value
*/
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
const dto = this.createSeriesV2Filter();
dto.statements.push(this.metadataService.createDefaultFilterStatement(filter, comparison, value + ''));
if (queryParams.sortOptions) {
const optionsStartIndex = fullUrl.indexOf('&' + sortOptionsKey);
const endIndex = fullUrl.indexOf('&' + limitToKey);
const sortOptionsEncoded = fullUrl.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
if (sortOptions) {
filter.sortOptions = sortOptions;
}
}
return this.encodeFilter(dto).pipe(switchMap(encodedFilter => {
return this.router.navigateByUrl(page.join('/') + '?' + encodedFilter);
}));
}
if (queryParams.limitTo) {
filter.limitTo = parseInt(queryParams.limitTo, 10);
}
applyFilterWithParams(page: Array<any>, filter: SeriesFilterV2, extraParams: Params) {
return this.encodeFilter(filter).pipe(switchMap(encodedFilter => {
let url = page.join('/') + '?' + encodedFilter;
url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join('');
if (queryParams.combination) {
filter.combination = parseInt(queryParams.combination, 10) as FilterCombination;
}
return this.router.navigateByUrl(url, extraParams);
}));
}
return filter;
}
decodeSortOptions(encodedSortOptions: string): SortOptions | null {
const parts = decodeURIComponent(encodedSortOptions).split(',');
const sortFieldPart = parts.find(part => part.startsWith('sortField='));
const isAscendingPart = parts.find(part => part.startsWith('isAscending='));
if (sortFieldPart && isAscendingPart) {
const sortField = parseInt(sortFieldPart.split('=')[1], 10) as SortField;
const isAscending = isAscendingPart.split('=')[1].toLowerCase() === 'true';
return {sortField, isAscending};
}
return null;
}
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
const statementStrings = decodeURIComponent(encodedStatements).split(',').map(s => decodeURIComponent(s));
return statementStrings.map(statementString => {
const parts = statementString.split(',');
if (parts === null || parts.length < 3) return null;
const comparisonStartToken = parts.find(part => part.startsWith('comparison='));
if (!comparisonStartToken) return null;
const comparison = parseInt(comparisonStartToken.split('=')[1], 10) as FilterComparison;
const fieldStartToken = parts.find(part => part.startsWith('field='));
if (!fieldStartToken) return null;
const field = parseInt(fieldStartToken.split('=')[1], 10) as FilterField;
const valueStartToken = parts.find(part => part.startsWith('value='));
if (!valueStartToken) return null;
const value = decodeURIComponent(valueStartToken.split('=')[1]);
return {comparison, field, value};
}).filter(o => o != null) as FilterStatement[];
}
createSeriesV2Filter(): SeriesFilterV2 {
return {
combination: FilterCombination.And,
statements: [],
limitTo: 0,
sortOptions: {
isAscending: true,
sortField: SortField.SortName
},
};
}
createSeriesV2DefaultStatement(): FilterStatement {
return {
comparison: FilterComparison.Equal,
value: '',
field: FilterField.SeriesName
}
}
createSeriesV2Filter(): SeriesFilterV2 {
return {
combination: FilterCombination.And,
statements: [],
limitTo: 0,
sortOptions: {
isAscending: true,
sortField: SortField.SortName
},
};
}
createSeriesV2DefaultStatement(): FilterStatement {
return {
comparison: FilterComparison.Equal,
value: '',
field: FilterField.SeriesName
}
}
}

View file

@ -115,7 +115,7 @@ export class ServerStatsComponent {
ref.componentInstance.items = genres.map(t => t.title);
ref.componentInstance.title = translate('server-stats.genres');
ref.componentInstance.clicked = (item: string) => {
this.filterUtilityService.applyFilter(['all-series'], FilterField.Genres, FilterComparison.Contains, genres.filter(g => g.title === item)[0].id + '');
this.filterUtilityService.applyFilter(['all-series'], FilterField.Genres, FilterComparison.Contains, genres.filter(g => g.title === item)[0].id + '').subscribe();
};
});
}
@ -126,7 +126,7 @@ export class ServerStatsComponent {
ref.componentInstance.items = tags.map(t => t.title);
ref.componentInstance.title = translate('server-stats.tags');
ref.componentInstance.clicked = (item: string) => {
this.filterUtilityService.applyFilter(['all-series'], FilterField.Tags, FilterComparison.Contains, tags.filter(g => g.title === item)[0].id + '');
this.filterUtilityService.applyFilter(['all-series'], FilterField.Tags, FilterComparison.Contains, tags.filter(g => g.title === item)[0].id + '').subscribe();
};
});
}

View file

@ -13,7 +13,7 @@
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" #scrollingBlock>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
<app-card-detail-layout *ngIf="filter"
[isLoading]="isLoading"
[items]="series"
[pagination]="pagination"

View file

@ -56,7 +56,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
isLoading: boolean = true;
series: Array<Series> = [];
pagination!: Pagination;
pagination: Pagination = new Pagination();
filter: SeriesFilterV2 | undefined = undefined;
filterSettings: FilterSettings = new FilterSettings();
refresh: EventEmitter<void> = new EventEmitter();
@ -106,17 +106,15 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - ' + translate('want-to-read.title'));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
this.filter = filter;
this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot);
if (this.filter.statements.length === 0) {
this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
}
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter();
this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement());
this.filterSettings.presetsV2 = this.filter;
this.cdRef.markForCheck();
this.cdRef.markForCheck();
});
this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
if (event.event === EVENTS.SeriesRemoved) {
@ -187,11 +185,14 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
if (data.filterV2 === undefined) return;
this.filter = data.filterV2;
if (!data.isFirst) {
this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter);
if (data.isFirst) {
this.loadPage();
return;
}
this.loadPage();
this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => {
this.loadPage();
});
}
}

View file

@ -1299,7 +1299,8 @@
"collection-detail": {
"no-data": "There are no items. Try adding a series.",
"no-data-filtered": "No items match your current filter.",
"title-alt": "Kavita - {{collectionName}} Collection"
"title-alt": "Kavita - {{collectionName}} Collection",
"series-header": "Series"
},
"all-collections": {