Filtering Bugfixes (#1220)

* Cleaned up random strings and unified them in one place.

* Implemented the ability to disable typeaheads

* Refactored disable state to disable controls on filter

* Fixed an overflow regression on title

* Updated ComicInfo DTO which had some bad properties on it

* Cleaned up some code around disabled typeaheads/filters

* Fixed typeaheads causing resets to state and mucking up filter presets

* Fixed state not refreshing between page loads

* Fixed a bad parsing for My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras

* Cleanup within the metadata filter to reuse logic and minimize extra loops.

* Fixed a timing issue with typeahead and first load for people

* Fixed a bug in Publication Status for a given library, which would fail due to not performing some of the query in memory. Removed a custom index on Series table that wasn't used and potentially caused constraint issues.

* Added a wiki link for stats collections

* Security bump

* Fixed the regex
This commit is contained in:
Joseph Milazzo 2022-04-16 18:29:11 -05:00 committed by GitHub
parent e3aa9abf55
commit e630e0b2c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1856 additions and 291 deletions

View file

@ -195,6 +195,7 @@ export class SeriesService {
}
createSeriesFilter(filter?: SeriesFilter) {
if (filter !== undefined) return filter;
const data: SeriesFilter = {
formats: [],
libraries: [],
@ -225,8 +226,6 @@ export class SeriesService {
seriesNameQuery: '',
};
if (filter === undefined) return data;
return filter;
return data;
}
}

View file

@ -40,7 +40,7 @@
<div class="mb-3">
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect. See <a href="https://wiki.kavitareader.com/en/faq" target="_blank" referrerpolicy="no-refer">wiki</a> for what is collected.</p>
<div class="form-check form-switch">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection">
<label for="stat-collection" class="form-check-label">Send Data</label>

View file

@ -31,6 +31,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
bulkActionCallback = (action: Action, data: any) => {
@ -79,8 +80,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - All Series');
this.pagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterActiveCheck = this.seriesService.createSeriesFilter();
}
ngOnInit(): void {
@ -117,12 +119,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
}
loadPage() {
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
if (this.filter == undefined) {
this.filter = this.seriesService.createSeriesFilter();
}
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;

View file

@ -15,7 +15,6 @@ import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-adde
import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series';
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
@ -41,6 +40,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
summary: string = '';
actionInProgress: boolean = false;
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -98,9 +98,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
const tagId = parseInt(routeId, 10);
this.seriesPagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterSettings.presets.collectionTags = [tagId];
this.filterActiveCheck = this.seriesService.createSeriesFilter();
this.filterActiveCheck.collectionTags = [tagId];
this.updateTag(tagId);
}
@ -161,7 +163,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
loadPage() {
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.seriesPagination = series.pagination;

View file

@ -37,6 +37,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
filterActiveCheck!: SeriesFilter;
tabs: Array<{title: string, fragment: string}> = [
{title: 'Library', fragment: ''},
@ -101,9 +102,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
});
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.filterSettings.presets.libraries = [this.libraryId];
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
// Setup filterActiveCheck to check filter against
this.filterActiveCheck = this.seriesService.createSeriesFilter();
this.filterActiveCheck.libraries = [this.libraryId];
this.filterSettings.libraryDisabled = true;
}
ngOnInit(): void {
@ -153,6 +159,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
updateFilter(data: FilterEvent) {
this.filter = data.filter;
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
this.loadPage();
}
@ -165,7 +172,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}
this.loadingSeries = true;
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;

View file

@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { ReplaySubject, Subject } from 'rxjs';
import { debounceTime, filter, take, takeUntil } from 'rxjs/operators';
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { Library } from '../_models/library';
@ -153,19 +154,19 @@ export class LibraryComponent implements OnInit, OnDestroy {
handleSectionClick(sectionTitle: string) {
if (sectionTitle.toLowerCase() === 'recently updated series') {
const params: any = {};
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params['page'] = 1;
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
} else if (sectionTitle.toLowerCase() === 'on deck') {
const params: any = {};
params['readStatus'] = 'true,false,false';
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params['page'] = 1;
params[FilterQueryParam.ReadStatus] = 'true,false,false';
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}else if (sectionTitle.toLowerCase() === 'newly added series') {
const params: any = {};
params['sortBy'] = SortField.Created + ',false'; // sort by created, desc
params['page'] = 1;
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}
}

View file

@ -13,9 +13,11 @@ export class FilterSettings {
tagsDisabled = false;
languageDisabled = false;
publicationStatusDisabled = false;
searchNameDisabled = false;
presets: SeriesFilter | undefined;
/**
* Should the filter section be open by default
* @deprecated This is deprecated UX pattern. New style is to show highlight on filter button.
*/
openByDefault = false;
}

View file

@ -19,13 +19,13 @@
<ng-template #filterSection>
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
<div class="filter-section mx-auto pb-3">
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded">
<div class="row justify-content-center g-0">
<div class="col-md-2 me-3" *ngIf="!filterSettings.formatDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="format" class="form-label">Format</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="visually-hidden" id="filter-global-format-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.formatDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -36,10 +36,10 @@
</div>
</div>
<div class="col-md-2 me-3"*ngIf="!filterSettings.libraryDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="libraries" class="form-label">Libraries</label>
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads" [disabled]="filterSettings.libraryDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -50,11 +50,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.collectionDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="collections" class="form-label">Collections</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="visually-hidden" id="filter-global-collections-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.collectionDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -65,10 +65,10 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.genresDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="genres" class="form-label">Genres</label>
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.genresDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -79,10 +79,10 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.tagsDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.tagsDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -95,10 +95,11 @@
</div>
<div class="row justify-content-center g-0">
<!-- The People row -->
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="cover-artist" class="form-label">Cover Artists</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -109,10 +110,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="writers" class="form-label">Writers</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Writer)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -123,10 +125,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="publisher" class="form-label">Publisher</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Publisher)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -137,10 +140,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="penciller" class="form-label">Penciller</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Penciller)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -151,10 +155,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="letterer" class="form-label">Letterer</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Letterer)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -165,10 +170,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="inker" class="form-label">Inker</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Inker)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -179,10 +185,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="editor" class="form-label">Editor</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Editor)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -193,10 +200,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="colorist" class="form-label">Colorist</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Colorist)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -207,10 +215,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="character" class="form-label">Character</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Character)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -221,10 +230,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="translators" class="form-label">Translators</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Translator)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -236,7 +246,7 @@
</div>
</div>
<div class="row justify-content-center g-0">
<div class="col-md-2 me-3" *ngIf="!filterSettings.readProgressDisabled">
<div class="col-md-2 me-3">
<label class="form-label">Read Progress</label>
<form [formGroup]="readProgressGroup">
<div class="form-check form-check-inline">
@ -254,7 +264,7 @@
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.ratingDisabled">
<div class="col-md-2 me-3">
<label for="ratings" class="form-label">Rating</label>
<form class="form-inline">
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
@ -265,9 +275,9 @@
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.ageRatingDisabled">
<div class="col-md-2 me-3">
<label for="age-rating" class="form-label">Age Rating</label>
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.ageRatingDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -277,9 +287,10 @@
</app-typeahead>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.languageDisabled">
<div class="col-md-2 me-3">
<label for="languages" class="form-label">Language</label>
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateLanguages($event)" [settings]="languageSettings"
[reset]="resetTypeaheads" [disabled]="filterSettings.languageDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -289,9 +300,10 @@
</app-typeahead>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.publicationStatusDisabled">
<div class="col-md-2 me-3">
<label for="publication-status" class="form-label">Publication Status</label>
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings"
[reset]="resetTypeaheads" [disabled]="filterSettings.publicationStatusDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -313,11 +325,11 @@
</div>
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
<div class="col-md-2 me-3">
<form [formGroup]="sortGroup">
<div class="mb-3">
<label for="sort-options" class="form-label">Sort By</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;" [disabled]="filterSettings.sortDisabled">
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-down" title="Descending"></i>
@ -332,7 +344,6 @@
</div>
</form>
</div>
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
<div class="col-md-2 me-3"></div>
<div class="col-md-2 me-3 mt-4">
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>

View file

@ -53,7 +53,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
tagsSettings: TypeaheadSettings<Tag> = new TypeaheadSettings();
languageSettings: TypeaheadSettings<Language> = new TypeaheadSettings();
peopleSettings: {[PersonRole: string]: TypeaheadSettings<Person>} = {};
resetTypeaheads: Subject<boolean> = new ReplaySubject(1);
resetTypeaheads: ReplaySubject<boolean> = new ReplaySubject(1);
/**
* Controls the visiblity of extended controls that sit below the main header.
@ -71,6 +71,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
updateApplied: number = 0;
fullyLoaded: boolean = false;
private onDestory: Subject<void> = new Subject();
@ -84,20 +86,32 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
}
ngOnInit(): void {
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
}
if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => {
this.filteringCollapsed = !openState;
});
}
this.filter = this.seriesService.createSeriesFilter();
this.readProgressGroup = new FormGroup({
read: new FormControl(this.filter.readStatus.read, []),
notRead: new FormControl(this.filter.readStatus.notRead, []),
inProgress: new FormControl(this.filter.readStatus.inProgress, []),
read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []),
notRead: new FormControl({value: this.filter.readStatus.notRead, disabled: this.filterSettings.readProgressDisabled}, []),
inProgress: new FormControl({value: this.filter.readStatus.inProgress, disabled: this.filterSettings.readProgressDisabled}, []),
});
this.sortGroup = new FormGroup({
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
sortField: new FormControl({value: this.filter.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []),
});
this.seriesNameGroup = new FormGroup({
seriesNameQuery: new FormControl(this.filter.seriesNameQuery || '', [])
seriesNameQuery: new FormControl({value: this.filter.seriesNameQuery || '', disabled: this.filterSettings.searchNameDisabled}, [])
});
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
@ -138,19 +152,21 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
.subscribe(changes => {
this.filter.seriesNameQuery = changes;
});
this.loadFromPresetsAndSetup();
}
ngOnInit(): void {
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
}
ngOnDestroy() {
this.onDestory.next();
this.onDestory.complete();
}
if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => {
this.filteringCollapsed = !openState;
});
}
getPersonsSettings(role: PersonRole) {
return this.peopleSettings[role];
}
loadFromPresetsAndSetup() {
this.fullyLoaded = false;
if (this.filterSettings.presets) {
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
@ -174,21 +190,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
}
this.setupTypeaheads();
}
ngOnDestroy() {
this.onDestory.next();
this.onDestory.complete();
}
getPersonsSettings(role: PersonRole) {
return this.peopleSettings[role];
}
setupTypeaheads() {
this.setupFormatTypeahead();
forkJoin([
@ -201,7 +202,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.setupGenreTypeahead(),
this.setupPersonTypeahead(),
]).subscribe(results => {
this.resetTypeaheads.next(true);
this.fullyLoaded = true;
this.resetTypeaheads.next(false); // Pass false to ensure we reset to the preset and not to an empty typeahead
if (this.filterSettings.openByDefault) {
this.filteringCollapsed = false;
}
@ -226,8 +228,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.formats && this.filterSettings.presets?.formats.length > 0) {
this.formatSettings.savedData = mangaFormatFilters.filter(item => this.filterSettings.presets?.formats.includes(item.value));
this.filter.formats = this.formatSettings.savedData.map(item => item.value);
this.resetTypeaheads.next(true);
this.updateFormatFilters(this.formatSettings.savedData);
}
}
@ -251,7 +252,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.libraries && this.filterSettings.presets?.libraries.length > 0) {
return this.librarySettings.fetchFn('').pipe(map(libraries => {
this.librarySettings.savedData = libraries.filter(item => this.filterSettings.presets?.libraries.includes(item.id));
this.filter.libraries = this.librarySettings.savedData.map(item => item.id);
this.updateLibraryFilters(this.librarySettings.savedData);
return of(true);
}));
}
@ -278,7 +279,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.genres && this.filterSettings.presets?.genres.length > 0) {
return this.genreSettings.fetchFn('').pipe(map(genres => {
this.genreSettings.savedData = genres.filter(item => this.filterSettings.presets?.genres.includes(item.id));
this.filter.genres = this.genreSettings.savedData.map(item => item.id);
this.updateGenreFilters(this.genreSettings.savedData);
return of(true);
}));
}
@ -305,7 +306,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.ageRating && this.filterSettings.presets?.ageRating.length > 0) {
return this.ageRatingSettings.fetchFn('').pipe(map(rating => {
this.ageRatingSettings.savedData = rating.filter(item => this.filterSettings.presets?.ageRating.includes(item.value));
this.filter.ageRating = this.ageRatingSettings.savedData.map(item => item.value);
this.updateAgeRating(this.ageRatingSettings.savedData);
return of(true);
}));
}
@ -332,7 +333,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.publicationStatus && this.filterSettings.presets?.publicationStatus.length > 0) {
return this.publicationStatusSettings.fetchFn('').pipe(map(statuses => {
this.publicationStatusSettings.savedData = statuses.filter(item => this.filterSettings.presets?.publicationStatus.includes(item.value));
this.filter.publicationStatus = this.publicationStatusSettings.savedData.map(item => item.value);
this.updatePublicationStatus(this.publicationStatusSettings.savedData);
return of(true);
}));
}
@ -358,7 +359,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.tags && this.filterSettings.presets?.tags.length > 0) {
return this.tagsSettings.fetchFn('').pipe(map(tags => {
this.tagsSettings.savedData = tags.filter(item => this.filterSettings.presets?.tags.includes(item.id));
this.filter.tags = this.tagsSettings.savedData.map(item => item.id);
this.updateTagFilters(this.tagsSettings.savedData);
return of(true);
}));
}
@ -384,7 +385,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.languages && this.filterSettings.presets?.languages.length > 0) {
return this.languageSettings.fetchFn('').pipe(map(languages => {
this.languageSettings.savedData = languages.filter(item => this.filterSettings.presets?.languages.includes(item.isoCode));
this.filter.languages = this.languageSettings.savedData.map(item => item.isoCode);
this.updateLanguages(this.languageSettings.savedData);
return of(true);
}));
}
@ -410,7 +411,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) {
return this.collectionSettings.fetchFn('').pipe(map(tags => {
this.collectionSettings.savedData = tags.filter(item => this.filterSettings.presets?.collectionTags.includes(item.id));
this.filter.collectionTags = this.collectionSettings.savedData.map(item => item.id);
this.updateCollectionFilters(this.collectionSettings.savedData);
return of(true);
}));
}
@ -423,16 +424,15 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
const fetch = personSettings.fetchFn as ((filter: string) => Observable<Person[]>);
return fetch('').pipe(map(people => {
personSettings.savedData = people.filter(item => presetField.includes(item.id));
peopleFilterField = personSettings.savedData.map(item => item.id);
this.resetTypeaheads.next(true);
this.peopleSettings[role] = personSettings;
this.updatePersonFilters(personSettings.savedData as Person[], role);
this.updatePersonFilters(personSettings.savedData, role);
return true;
}));
} else {
this.peopleSettings[role] = personSettings;
return of(true);
}
this.peopleSettings[role] = personSettings;
return of(true);
}
setupPersonTypeahead() {
@ -449,8 +449,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.updateFromPreset('penciller', this.filter.penciller, this.filterSettings.presets?.penciller, PersonRole.Penciller),
this.updateFromPreset('publisher', this.filter.publisher, this.filterSettings.presets?.publisher, PersonRole.Publisher),
this.updateFromPreset('translators', this.filter.translators, this.filterSettings.presets?.translators, PersonRole.Translator)
]).pipe(map(results => {
this.resetTypeaheads.next(true);
]).pipe(map(_ => {
return of(true);
}));
}
@ -537,6 +536,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
updateRating(rating: any) {
if (this.filterSettings.ratingDisabled) return;
this.filter.rating = rating;
}
@ -548,7 +548,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.filter.publicationStatus = dtos.map(item => item.value) || [];
}
updateLanguageRating(languages: Language[]) {
updateLanguages(languages: Language[]) {
this.filter.languages = languages.map(item => item.isoCode) || [];
}
@ -563,6 +563,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
updateSortOrder() {
if (this.filterSettings.sortDisabled) return;
this.isAscendingSort = !this.isAscendingSort;
if (this.filter.sortOptions === null) {
this.filter.sortOptions = {
@ -582,7 +583,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.sortGroup.get('sortField')?.setValue(SortField.SortName);
this.isAscendingSort = true;
// Apply any presets which will trigger the apply
this.setupTypeaheads();
this.loadFromPresetsAndSetup();
}
apply() {

View file

@ -1,10 +1,10 @@
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs';
import { finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';

View file

@ -4,12 +4,12 @@
<!-- This first row will have random information about the series-->
<div class="row g-0 mb-2">
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo('ageRating', seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<ng-container *ngIf="series">
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo('languages', seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo('publicationStatus', seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('format', series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Format, series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
@ -25,7 +25,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.genres">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('genres', item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
@ -70,7 +70,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.writers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('writers', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Writers, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -89,7 +89,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.coverArtists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('coverArtists', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.CoverArtists, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -102,7 +102,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('character', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Character, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -115,7 +115,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.colorists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('colorist', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Colorist, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -128,7 +128,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.editors">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('editor', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Editor, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -141,7 +141,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.inkers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('inker', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Inker, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -154,7 +154,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.letterers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('letterer', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Letterer, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -166,7 +166,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.tags">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('tags', item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
@ -178,7 +178,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.translators">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('translators', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Translator, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -191,7 +191,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.pencillers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('penciller', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -204,7 +204,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.publishers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('publisher', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Publisher, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>

View file

@ -1,6 +1,7 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
import { UtilityService } from '../shared/_services/utility.service';
import { MangaFormat } from '../_models/manga-format';
import { ReadingList } from '../_models/reading-list';
@ -38,6 +39,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
return TagBadgeCursor;
}
get FilterQueryParam() {
return FilterQueryParam;
}
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router) { }
ngOnChanges(changes: SimpleChanges): void {
@ -64,10 +69,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
this.isCollapsed = !this.isCollapsed;
}
goTo(queryParamName: string, filter: any) {
goTo(queryParamName: FilterQueryParam, filter: any) {
let params: any = {};
params[queryParamName] = filter;
params['page'] = 1;
params[FilterQueryParam.Page] = 1;
this.router.navigate(['library', this.series.libraryId], {queryParams: params});
}

View file

@ -1,10 +1,42 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { LibraryType } from 'src/app/_models/library';
import { Pagination } from 'src/app/_models/pagination';
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
import { SeriesService } from 'src/app/_services/series.service';
/**
* 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'
}
@Injectable({
providedIn: 'root'
})
@ -38,10 +70,11 @@ export class FilterUtilitiesService {
/**
* Will fetch current page from route if present
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
* @returns A default pagination object
*/
pagination(): Pagination {
return {currentPage: parseInt(this.route.snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
pagination(snapshot: ActivatedRouteSnapshot): Pagination {
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
}
@ -54,46 +87,44 @@ export class FilterUtilitiesService {
urlFromFilter(currentUrl: string, filter: SeriesFilter | undefined) {
if (filter === undefined) return currentUrl;
let params = '';
params += this.joinFilter(filter.formats, 'format');
params += this.joinFilter(filter.genres, 'genres');
params += this.joinFilter(filter.ageRating, 'ageRating');
params += this.joinFilter(filter.publicationStatus, 'publicationStatus');
params += this.joinFilter(filter.tags, 'tags');
params += this.joinFilter(filter.languages, 'languages');
params += this.joinFilter(filter.collectionTags, 'collectionTags');
params += this.joinFilter(filter.libraries, 'libraries');
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, 'writers');
params += this.joinFilter(filter.artists, 'artists');
params += this.joinFilter(filter.character, 'character');
params += this.joinFilter(filter.colorist, 'colorist');
params += this.joinFilter(filter.coverArtist, 'coverArtists');
params += this.joinFilter(filter.editor, 'editor');
params += this.joinFilter(filter.inker, 'inker');
params += this.joinFilter(filter.letterer, 'letterer');
params += this.joinFilter(filter.penciller, 'penciller');
params += this.joinFilter(filter.publisher, 'publisher');
params += this.joinFilter(filter.translators, 'translators');
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 += '&readStatus=' + `${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
params += `&${FilterQueryParam.ReadStatus}=${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
}
// sortBy (additional check to not save to url if default case)
if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) {
params += '&sortBy=' + filter.sortOptions.sortField + ',' + filter.sortOptions.isAscending;
params += `&${FilterQueryParam.SortBy}=${filter.sortOptions.sortField},${filter.sortOptions.isAscending}`;
}
if (filter.rating > 0) {
params += '&rating=' + filter.rating;
params += `&${FilterQueryParam.Rating}=${filter.rating}`;
}
if (filter.seriesNameQuery !== '') {
params += '&name=' + encodeURIComponent(filter.seriesNameQuery);
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
}
return currentUrl + params;
@ -102,143 +133,143 @@ export class FilterUtilitiesService {
private joinFilter(filterProp: Array<any>, key: string) {
let params = '';
if (filterProp.length > 0) {
params += `&${key}=` + filterProp.join(',');
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(): [SeriesFilter, boolean] {
const snapshot = this.route.snapshot;
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] {
const filter = this.seriesService.createSeriesFilter();
let anyChanged = false;
const format = snapshot.queryParamMap.get('format');
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;
}
const genres = snapshot.queryParamMap.get('genres');
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;
}
const ageRating = snapshot.queryParamMap.get('ageRating');
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;
}
const publicationStatus = snapshot.queryParamMap.get('publicationStatus');
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;
}
const tags = snapshot.queryParamMap.get('tags');
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;
}
const languages = snapshot.queryParamMap.get('languages');
const languages = snapshot.queryParamMap.get(FilterQueryParam.Languages);
if (languages !== undefined && languages !== null) {
filter.languages = [...filter.languages, ...languages.split(',')];
anyChanged = true;
}
const writers = snapshot.queryParamMap.get('writers');
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;
}
const artists = snapshot.queryParamMap.get('artists');
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 character = snapshot.queryParamMap.get('character');
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('colorist');
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('coverArtists');
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('editor');
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('inker');
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('letterer');
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('penciller');
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('publisher');
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('translators');
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('libraries');
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('collectionTags');
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('rating');
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('readStatus');
const readStatus = snapshot.queryParamMap.get(FilterQueryParam.ReadStatus);
if (readStatus !== undefined && readStatus !== null) {
const values = readStatus.split(',').map(i => i === 'true');
if (values.length === 3) {
@ -249,7 +280,7 @@ export class FilterUtilitiesService {
}
}
const sortBy = snapshot.queryParamMap.get('sortBy');
const sortBy = snapshot.queryParamMap.get(FilterQueryParam.SortBy);
if (sortBy !== undefined && sortBy !== null) {
const values = sortBy.split(',');
if (values.length === 1) {
@ -264,7 +295,7 @@ export class FilterUtilitiesService {
}
}
const searchNameQuery = snapshot.queryParamMap.get('name');
const searchNameQuery = snapshot.queryParamMap.get(FilterQueryParam.Name);
if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') {
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
anyChanged = true;

View file

@ -4,7 +4,7 @@
<ng-content select="[title]"></ng-content>
<ng-content select="[subtitle]"></ng-content>
</div>
<div class="col mr-auto hide-if-empty d-none d-sm-flex">
<div class="col mr-auto d-none d-sm-flex hide-if-empty">
<ng-content select="[main]"></ng-content>
</div>
<div class="col" *ngIf="hasFilter">

View file

@ -1,5 +1,5 @@
.hide-if-empty:empty {
display: none;
display: none !important;
}
::ng-deep .companion-bar {

View file

@ -5,17 +5,17 @@
<span class="visually-hidden">Field is locked</span>
</span>
</ng-container>
<div class="typeahead-input" (click)="onInputFocus($event)">
<div class="typeahead-input" [ngClass]="{'disabled': disabled}" (click)="onInputFocus($event)">
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
<i class="fa fa-times" *ngIf="!disabled" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
</app-tag-badge>
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead">
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead" *ngIf="!disabled">
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
<span class="visually-hidden">Loading...</span>
</div>
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
<ng-container *ngIf="!disabled && settings.multiple && (selectedData | async) as selected">
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections()"></button>
</ng-container>
</div>

View file

@ -43,6 +43,10 @@ input {
border: 1px solid var(--input-border-color);
color: var(--body-text-color);
&.disabled {
cursor: not-allowed !important;
}
input {
outline: 0 !important;
border-radius: .28571429rem;

View file

@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common';
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { KEY_CODES } from '../shared/_services/utility.service';
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
@ -141,17 +141,22 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
*/
@Input() settings!: TypeaheadSettings<any>;
/**
* When true, component will re-init and set back to false.
* When true, will reset field to no selections. When false, will reset to saved data
*/
@Input() reset: Subject<boolean> = new ReplaySubject(1);
@Input() reset: ReplaySubject<boolean> = new ReplaySubject(1);
/**
* When a field is locked, we render custom css to indicate to the user. Does not affect functionality.
*/
@Input() locked: boolean = false;
/**
* If disabled, a user will not be able to interact with the typeahead
*/
@Input() disabled: boolean = false;
@Output() selectedData = new EventEmitter<any[] | any>();
@Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>();
@Output() lockedChange = new EventEmitter<boolean>();
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
@ContentChild('optionItem') optionTemplate!: TemplateRef<any>;
@ -178,8 +183,8 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
ngOnInit() {
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => {
this.clearSelections();
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((resetToEmpty: boolean) => {
this.clearSelections(resetToEmpty);
this.init();
});
@ -274,6 +279,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
@HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) {
if (!this.hasFocus) { return; }
if (this.disabled) return;
switch(event.key) {
case KEY_CODES.DOWN_ARROW:
@ -347,15 +353,26 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.resetField();
}
clearSelections() {
clearSelections(untoggleAll: boolean = false) {
if (this.optionSelection) {
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
if (!untoggleAll && this.settings.savedData) {
const isArray = this.settings.savedData.hasOwnProperty('length');
if (isArray) {
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData);
} else {
this.optionSelection = new SelectionModel<any>(true, [this.settings.savedData]);
}
} else {
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
}
this.selectedData.emit(this.optionSelection.selected());
this.resetField();
}
}
handleOptionClick(opt: any) {
if (this.disabled) return;
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
return;
}
@ -402,6 +419,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
event.stopPropagation();
event.preventDefault();
}
if (this.disabled) return;
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
return;
@ -452,6 +470,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}
unlock(event: any) {
if (this.disabled) return;
this.locked = !this.locked;
this.onUnlock.emit();
this.lockedChange.emit(this.locked);