Filtering Overhaul (#2207)

* Implemented the first version of dynamic filtering which is all Extension based.

* Implemented basic generic property filter for expanded metadata filtering.

* Fixed up the code to allow for nested properties and fixed up the Contains to work only for IList's

* Started refactoring for the new approach

* More progress, need to rethink a few filters like read progress to be % based and people needs to be more explicit.

* Refactored most of the existing filtering operations into dedicate extensions for the appropriate comparisons. People still need to be reworked to be more dynamic.

* Fixed a bug with continue point where it fails on chapters or volumes tagged with a range

* Wired up a basic api path to start building groups. No and/or support yet.

* Started on the UI

* Made a bit of progress on the UI as I'm putting the pieces together about how to design it.

* Refactored names to make it more consistent. New thinking is we will have one row that will take a filter statement and manipulate it. It will emit said statement and a builder will turn into the higher level statement.

* Started working on updating code to use new inject() method.

* Fixed the code to switch the comparisons.

* Added dynamic input structure in and moved add/remove to the builder.

* Fixed an enum bug

* Hooked in basic dropdown support that is dynamic to the field. Only language is missing as that needs a DTO change (but don't want to break API)

* Fixed a bug where dropdown options wouldn't re-populate when switching fields that are both dropdowns

* Started adding metadata builder

* Fixed when typing on filter row the focus resetting

* Refactored to add an additional component which handles the compounding of filter rows.

* Started hooking up v2 dto in the UI to send to the backend.

* Started working on building group UI for and/or support.

* Lots of backend code fixes to ensure OR and AND statements combine correctly.

* More trying to figure out how to write the UI code

* Started debugging to remember what I was last doing.

* Lots of progress towards building out the UI recursively

* I got the dto to build and propagate up the chain

* Started hooking up to the actual api to fetch the data.

* Basic wire up to the backend is working.

* HasName is now complete

* Refactored SortOptions code into an extension and streamlined LimitTo to the correct place.

* Fixed a bug where Library Filters from the Group weren't actually being taken into account.

* Refactored a lot of code so builder will now export the full dto.

* Cleaned up the data flow from metadata filter to library detail

* Got the dropdown to load preset values on first load, but now it triggers twice.

* Changed so when you add a new filter, it does it at top and fixed remove

* Fixed the remove button being on the wrong row

* Cleaned up the arrays to make it easier to manage

* Cleaned up some of the backend to ensure it doesn't throw an incorrect exception

* I'm starting to tread water, taking a break

* Fixed a merge issue

* Cleaned up Docker checks.

* Default IpAddresses to empty string.

* Refactored IsDocker to be completely static

* Figured out the issue with the dropdown not working.

* Almost got it, but the event isn't being called.

* I think i might try something else. This doesn't seem to be working.

* On the new implementation, implemented remove group.

* Use enums to reduce copy/paste

* the new system is working pretty well, ill go with it and move on. Can alwasy refactor.

* Code is totally broken, but working the cache resume code with some hiccups.

* I need to take a break

* Stashing my broken code. I have an idea on how to serialize to the URL, but I need to rearchitect a lot.

* Reverted last commit

* remove domain

* Fixed up some hardcoded caching. I'm giving up on this implementation and going to a simpler version

* Refactored the backend to just allow flat filtering.

* Started refactoring the components to make it flat filtering only.

* Finished refactoring so that the base preset case will render.

* Implemented basic query functionality on desktop. Clear needs some work and url code.

* Some cleanup

* Working on filtering url encode/decode

* Interacting with filters now saves to url and can be reloaded from the url. Named filters is not hooked up.

* Fixed a double load on the library detail page.

* Moved the library filtering code out of the FilterBuilder as it needs to be handled differently.

* Fixed up how we handle library statements in the filter.

* Fixed up how links that perform a filter work.

* Refactored a bunch of linking to a search page.

* LimitTo works, my css however does not.

* Switched some code to use localized strings.

* Cleaned up some css

* Hooked up Languages and put some additional code in so that Languages will return invalid Language codes back.

* Removed a duplicate language signature.

* Hooked up ability to preload collection tag.

* Want To Read is converted

* Converted lots of code to new filtering system. Need to do Bookmarks.

* Fixed a potential bug with default filter creation.

* Hooked up the ability to disable certain filter fields from appearing.

* Added mobile drawer code and a hook for Robbie to take a look for some css.

* Converted the APIs for dashboard along with other safety fixes to ensure bad data doesn't break any of the filtering apis

* Added the backend code to handle summary query

* Converted Want to Read api properly now.

* Fixed the HasReadingProgress query

* Hooked back the Reading Progress for legacy APIs

* Fixed some bad localization strings

* Wrote the filtering code for all-bookmarks.

* OPDS is now using the new filter

* Fixed OPDS reading lists and covers not sending their images.

* Fixed up the OPDS feed and fixed a bug where libraries also weren't sending their images over OPDS

* All but dropdown options have been validated and tested.

* Fixed up some default cases for setting up the filter.

* Sorted filter fields and re-keyed to be better suited based on user's needs.

Fixed a bug where OPDS Series (from library view) wasn't showing the summary.

Moved the (Format) from the title to the description to make the UX much better for OPDS.

MOved

* don't send empty summaries in the new summary formatting

* Fixed up some default cases for setting up the filter.

* Fixed the reset button

* Fixed infinite scroller not having correct scope key

* Added localization to the new components and removed old debug code

* Styling fixes

* Fixed deep linking across the app. Made it so you can click Characters from Reading list and open a filtered search.

* A bit of styling for mobile

* Don't show language if it's not properly set

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2023-08-11 16:30:36 -05:00 committed by GitHub
parent bc2a12a9cd
commit 9cc5953d07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 3299 additions and 1827 deletions

View file

@ -228,7 +228,6 @@ export class DownloadService {
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
console.log('saving: ', filename)
this.save(blob, decodeURIComponent(filename));
}),
tap((d) => this.updateDownloadState(d, downloadType, subtitle)),

View file

@ -1,347 +1,231 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Pagination } from 'src/app/_models/pagination';
import { SeriesFilter, SortField } from 'src/app/_models/metadata/series-filter';
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, 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";
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";
/**
* Used to pass state between the filter and the url
*/
export enum FilterQueryParam {
Format = 'format',
Genres = 'genres',
AgeRating = 'ageRating',
PublicationStatus = 'publicationStatus',
Tags = 'tags',
Languages = 'languages',
CollectionTags = 'collectionTags',
Libraries = 'libraries',
Writers = 'writers',
Artists = 'artists',
Character = 'character',
Colorist = 'colorist',
CoverArtists = 'coverArtists',
Editor = 'editor',
Inker = 'inker',
Letterer = 'letterer',
Penciller = 'penciller',
Publisher = 'publisher',
Translator = 'translators',
ReadStatus = 'readStatus',
SortBy = 'sortBy',
Rating = 'rating',
Name = 'name',
/**
* This is a pagination control
*/
Page = 'page',
/**
* Special case for the UI. Does not trigger filtering
*/
None = 'none'
Format = 'format',
Genres = 'genres',
AgeRating = 'ageRating',
PublicationStatus = 'publicationStatus',
Tags = 'tags',
Languages = 'languages',
CollectionTags = 'collectionTags',
Libraries = 'libraries',
Writers = 'writers',
Artists = 'artists',
Character = 'character',
Colorist = 'colorist',
CoverArtists = 'coverArtists',
Editor = 'editor',
Inker = 'inker',
Letterer = 'letterer',
Penciller = 'penciller',
Publisher = 'publisher',
Translator = 'translators',
ReadStatus = 'readStatus',
SortBy = 'sortBy',
Rating = 'rating',
Name = 'name',
/**
* This is a pagination control
*/
Page = 'page',
/**
* Special case for the UI. Does not trigger filtering
*/
None = 'none'
}
@Injectable({
providedIn: 'root'
providedIn: 'root'
})
export class FilterUtilitiesService {
constructor() { }
/**
* Updates the window location with a custom url based on filter and pagination objects
* @param pagination
* @param filter
*/
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) {
const params = '?page=' + pagination.currentPage;
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
}
/**
* Patches the page query param in the window location.
* @param pagination
*/
updateUrlFromPagination(pagination: Pagination) {
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, 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 ActivatedRouteSnapshot 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
*/
urlFromFilter(currentUrl: string, filter: SeriesFilter | undefined) {
if (filter === undefined) return currentUrl;
let params = '';
params += this.joinFilter(filter.formats, FilterQueryParam.Format);
params += this.joinFilter(filter.genres, FilterQueryParam.Genres);
params += this.joinFilter(filter.ageRating, FilterQueryParam.AgeRating);
params += this.joinFilter(filter.publicationStatus, FilterQueryParam.PublicationStatus);
params += this.joinFilter(filter.tags, FilterQueryParam.Tags);
params += this.joinFilter(filter.languages, FilterQueryParam.Languages);
params += this.joinFilter(filter.collectionTags, FilterQueryParam.CollectionTags);
params += this.joinFilter(filter.libraries, FilterQueryParam.Libraries);
params += this.joinFilter(filter.writers, FilterQueryParam.Writers);
params += this.joinFilter(filter.artists, FilterQueryParam.Artists);
params += this.joinFilter(filter.character, FilterQueryParam.Character);
params += this.joinFilter(filter.colorist, FilterQueryParam.Colorist);
params += this.joinFilter(filter.coverArtist, FilterQueryParam.CoverArtists);
params += this.joinFilter(filter.editor, FilterQueryParam.Editor);
params += this.joinFilter(filter.inker, FilterQueryParam.Inker);
params += this.joinFilter(filter.letterer, FilterQueryParam.Letterer);
params += this.joinFilter(filter.penciller, FilterQueryParam.Penciller);
params += this.joinFilter(filter.publisher, FilterQueryParam.Publisher);
params += this.joinFilter(filter.translators, FilterQueryParam.Translator);
// readStatus (we need to do an additonal check as there is a default case)
if (filter.readStatus && filter.readStatus.inProgress !== true && filter.readStatus.notRead !== true && filter.readStatus.read !== true) {
params += `&${FilterQueryParam.ReadStatus}=${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
constructor(private metadataService: MetadataService, private router: Router) {
}
// sortBy (additional check to not save to url if default case)
if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) {
params += `&${FilterQueryParam.SortBy}=${filter.sortOptions.sortField},${filter.sortOptions.isAscending}`;
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
};
console.log('applying filter: ', this.urlFromFilterV2(page.join('/') + '?', dto))
this.router.navigateByUrl(this.urlFromFilterV2(page.join('/') + '?', dto));
}
if (filter.rating > 0) {
params += `&${FilterQueryParam.Rating}=${filter.rating}`;
/**
* 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));
}
if (filter.seriesNameQuery !== '') {
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
private replacePaginationOnUrl(url: string, pagination: Pagination) {
return url.replace(/page=\d+/i, 'page=' + pagination.currentPage);
}
return currentUrl + params;
}
private joinFilter(filterProp: Array<any>, key: string) {
let params = '';
if (filterProp.length > 0) {
params += `&${key}=${filterProp.join(',')}`;
}
return params;
}
/**
* Returns a new instance of a filterSettings that is populated with filter presets from URL
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
* @returns The Preset filter and if something was set within
*/
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] {
const filter = this.createSeriesFilter();
let anyChanged = false;
const format = snapshot.queryParamMap.get(FilterQueryParam.Format);
if (format !== undefined && format !== null) {
filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
/**
* Will fetch current page from route if present
* @param ActivatedRouteSnapshot 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};
}
const genres = snapshot.queryParamMap.get(FilterQueryParam.Genres);
if (genres !== undefined && genres !== null) {
filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
/**
* 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);
}
const ageRating = snapshot.queryParamMap.get(FilterQueryParam.AgeRating);
if (ageRating !== undefined && ageRating !== null) {
filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
encodeSeriesFilter(filter: SeriesFilterV2) {
const encodedStatements = this.encodeFilterStatements(filter.statements);
const encodedSortOptions = filter.sortOptions ? `sortOptions=${this.encodeSortOptions(filter.sortOptions)}` : '';
const encodedLimitTo = `limitTo=${filter.limitTo}`;
return `${this.encodeName(filter.name)}stmts=${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&combination=${filter.combination}`;
}
const publicationStatus = snapshot.queryParamMap.get(FilterQueryParam.PublicationStatus);
if (publicationStatus !== undefined && publicationStatus !== null) {
filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
encodeName(name: string | undefined) {
if (name === undefined || name === '') return '';
return `name=${encodeURIComponent(name)}&`
}
const tags = snapshot.queryParamMap.get(FilterQueryParam.Tags);
if (tags !== undefined && tags !== null) {
filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
encodeSortOptions(sortOptions: SortOptions) {
return `sortField=${sortOptions.sortField}&isAscending=${sortOptions.isAscending}`;
}
const languages = snapshot.queryParamMap.get(FilterQueryParam.Languages);
if (languages !== undefined && languages !== null) {
filter.languages = [...filter.languages, ...languages.split(',')];
anyChanged = true;
encodeFilterStatements(statements: Array<FilterStatement>) {
return 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(','));
}
const writers = snapshot.queryParamMap.get(FilterQueryParam.Writers);
if (writers !== undefined && writers !== null) {
filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
filterPresetsFromUrlV2(snapshot: ActivatedRouteSnapshot): SeriesFilterV2 {
const filter = this.metadataService.createDefaultFilterDto();
if (!window.location.href.includes('?')) return filter;
const artists = snapshot.queryParamMap.get(FilterQueryParam.Artists);
if (artists !== undefined && artists !== null) {
filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const queryParams = snapshot.queryParams;
const character = snapshot.queryParamMap.get(FilterQueryParam.Character);
if (character !== undefined && character !== null) {
filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const colorist = snapshot.queryParamMap.get(FilterQueryParam.Colorist);
if (colorist !== undefined && colorist !== null) {
filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const coverArtists = snapshot.queryParamMap.get(FilterQueryParam.CoverArtists);
if (coverArtists !== undefined && coverArtists !== null) {
filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const editor = snapshot.queryParamMap.get(FilterQueryParam.Editor);
if (editor !== undefined && editor !== null) {
filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const inker = snapshot.queryParamMap.get(FilterQueryParam.Inker);
if (inker !== undefined && inker !== null) {
filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const letterer = snapshot.queryParamMap.get(FilterQueryParam.Letterer);
if (letterer !== undefined && letterer !== null) {
filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const penciller = snapshot.queryParamMap.get(FilterQueryParam.Penciller);
if (penciller !== undefined && penciller !== null) {
filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const publisher = snapshot.queryParamMap.get(FilterQueryParam.Publisher);
if (publisher !== undefined && publisher !== null) {
filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const translators = snapshot.queryParamMap.get(FilterQueryParam.Translator);
if (translators !== undefined && translators !== null) {
filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const libraries = snapshot.queryParamMap.get(FilterQueryParam.Libraries);
if (libraries !== undefined && libraries !== null) {
filter.libraries = [...filter.libraries, ...libraries.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const collectionTags = snapshot.queryParamMap.get(FilterQueryParam.CollectionTags);
if (collectionTags !== undefined && collectionTags !== null) {
filter.collectionTags = [...filter.collectionTags, ...collectionTags.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
// Rating, seriesName,
const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating);
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
filter.rating = parseInt(rating, 10);
anyChanged = true;
}
/// Read status is encoded as true,true,true
const readStatus = snapshot.queryParamMap.get(FilterQueryParam.ReadStatus);
if (readStatus !== undefined && readStatus !== null) {
const values = readStatus.split(',').map(i => i === 'true');
if (values.length === 3) {
filter.readStatus.inProgress = values[0];
filter.readStatus.notRead = values[1];
filter.readStatus.read = values[2];
anyChanged = true;
}
}
const sortBy = snapshot.queryParamMap.get(FilterQueryParam.SortBy);
if (sortBy !== undefined && sortBy !== null) {
const values = sortBy.split(',');
if (values.length === 1) {
values.push('true');
}
if (values.length === 2) {
filter.sortOptions = {
isAscending: values[1] === 'true',
sortField: Number(values[0])
if (queryParams.name) {
filter.name = queryParams.name;
}
anyChanged = true;
}
const fullUrl = window.location.href.split('?')[1];
const stmtsStartIndex = fullUrl.indexOf('stmts=');
let endIndex = fullUrl.indexOf('&sortOptions=');
if (endIndex < 0) {
endIndex = fullUrl.indexOf('&limitTo=');
}
if (stmtsStartIndex !== -1 && endIndex !== -1) {
const stmtsEncoded = fullUrl.substring(stmtsStartIndex + 6, endIndex);
filter.statements = this.decodeFilterStatements(stmtsEncoded);
}
if (queryParams.sortOptions) {
const sortOptions = this.decodeSortOptions(queryParams.sortOptions);
if (sortOptions) {
filter.sortOptions = sortOptions;
}
}
if (queryParams.limitTo) {
filter.limitTo = parseInt(queryParams.limitTo, 10);
}
if (queryParams.combination) {
filter.combination = parseInt(queryParams.combination, 10) as FilterCombination;
}
return filter;
}
const searchNameQuery = snapshot.queryParamMap.get(FilterQueryParam.Name);
if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') {
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
anyChanged = true;
decodeSortOptions(encodedSortOptions: string): SortOptions | null {
const parts = 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] === 'true';
return {sortField, isAscending};
}
return null;
}
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
const statementStrings = decodeURIComponent(encodedStatements).split(','); // I don't think this will wrk
return statementStrings.map(statementString => {
const parts = statementString.split('&');
if (parts === null || parts.length < 3) return null;
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
}
const comparisonStartToken = parts.find(part => part.startsWith('comparison='));
if (!comparisonStartToken) return null;
const comparison = parseInt(comparisonStartToken.split('=')[1], 10) as FilterComparison;
createSeriesFilter(filter?: SeriesFilter) {
if (filter !== undefined) return filter;
const data: SeriesFilter = {
formats: [],
libraries: [],
genres: [],
writers: [],
artists: [],
penciller: [],
inker: [],
colorist: [],
letterer: [],
coverArtist: [],
editor: [],
publisher: [],
character: [],
translators: [],
collectionTags: [],
rating: 0,
readStatus: {
read: true,
inProgress: true,
notRead: true
},
sortOptions: null,
ageRating: [],
tags: [],
languages: [],
publicationStatus: [],
seriesNameQuery: '',
releaseYearRange: null
};
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
}
}
return data;
}
}

View file

@ -155,6 +155,7 @@ export class UtilityService {
}
return true;
}
private isObject(object: any) {
return object != null && typeof object === 'object';
}