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:
parent
e3aa9abf55
commit
e630e0b2c9
28 changed files with 1856 additions and 291 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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> <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> <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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.hide-if-empty:empty {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .companion-bar {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue