More Filtering and Support for ComicInfo v2.1 (draft) Tags (#851)
* Added a reoccuring task to cleanup db entries that might be abandoned. On library page, the Library in question will be prepoulated. * Laid out the foundation for customized sorting. Added all series page to the UI when clicking on Libraries section header on home page so user can apply any filtering they like. * When filtering, the current library filter will now automatically filter out the options for people and genres. * Implemented Sorting controls * Clear now clears sorting and read progress. Sorting is disabled on deck and recently added. * Fixed an issue where all-series page couldn't click to open series * Don't let the user unselect the last read progress. Added new comicinfo v2.1 draft tags. * Hooked in Translator tag into backend and UI. * Fixed an issue where you could open multiple typeaheads at the same time * Integrated Translator and Tags ComicInfo extension fields. Started work on a badge expander. * Reworked a bit more on badge expander. Added the UI code for Age Rating and Tags * Integrated backend for Tags, Translator, and Age Rating * Metadata tags now collapse if more than 4 present * Some code cleanup * Made the not read badge slightly smaller
This commit is contained in:
parent
21da5d8134
commit
94bad97511
71 changed files with 4324 additions and 207 deletions
|
@ -1,5 +1,6 @@
|
|||
import { MangaFile } from './manga-file';
|
||||
import { Person } from './person';
|
||||
import { Tag } from './tag';
|
||||
|
||||
export interface Chapter {
|
||||
id: number;
|
||||
|
@ -31,4 +32,5 @@ export interface Chapter {
|
|||
coverArtist: Array<Person>;
|
||||
editor: Array<Person>;
|
||||
publisher: Array<Person>;
|
||||
tags: Array<Tag>;
|
||||
}
|
||||
|
|
6
UI/Web/src/app/_models/metadata/age-rating-dto.ts
Normal file
6
UI/Web/src/app/_models/metadata/age-rating-dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { AgeRating } from "./age-rating";
|
||||
|
||||
export interface AgeRatingDto {
|
||||
value: AgeRating;
|
||||
title: string;
|
||||
}
|
|
@ -9,7 +9,8 @@ export enum PersonRole {
|
|||
CoverArtist = 8,
|
||||
Editor = 9,
|
||||
Publisher = 10,
|
||||
Character = 11
|
||||
Character = 11,
|
||||
Translator = 12
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
|
|
|
@ -20,8 +20,24 @@ export interface SeriesFilter {
|
|||
editor: Array<number>;
|
||||
publisher: Array<number>;
|
||||
character: Array<number>;
|
||||
translators: Array<number>;
|
||||
collectionTags: Array<number>;
|
||||
rating: number;
|
||||
ageRating: Array<number>;
|
||||
sortOptions: SortOptions | null;
|
||||
tags: Array<number>;
|
||||
languages: Array<string>;
|
||||
}
|
||||
|
||||
export interface SortOptions {
|
||||
sortField: SortField;
|
||||
isAscending: boolean;
|
||||
}
|
||||
|
||||
export enum SortField {
|
||||
SortName = 1,
|
||||
Created = 2,
|
||||
LastModified = 3
|
||||
}
|
||||
|
||||
export interface ReadStatus {
|
||||
|
|
|
@ -2,12 +2,14 @@ import { CollectionTag } from "./collection-tag";
|
|||
import { Genre } from "./genre";
|
||||
import { AgeRating } from "./metadata/age-rating";
|
||||
import { Person } from "./person";
|
||||
import { Tag } from "./tag";
|
||||
|
||||
export interface SeriesMetadata {
|
||||
publisher: string;
|
||||
summary: string;
|
||||
genres: Array<Genre>;
|
||||
tags: Array<CollectionTag>;
|
||||
tags: Array<Tag>;
|
||||
collectionTags: Array<CollectionTag>;
|
||||
writers: Array<Person>;
|
||||
artists: Array<Person>;
|
||||
publishers: Array<Person>;
|
||||
|
@ -17,7 +19,9 @@ export interface SeriesMetadata {
|
|||
colorists: Array<Person>;
|
||||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
translators: Array<Person>;
|
||||
ageRating: AgeRating;
|
||||
releaseYear: number;
|
||||
language: string;
|
||||
seriesId: number;
|
||||
}
|
|
@ -7,7 +7,6 @@ export interface Series {
|
|||
originalName: string; // This is not shown to user
|
||||
localizedName: string;
|
||||
sortName: string;
|
||||
//summary: string;
|
||||
coverImageLocked: boolean;
|
||||
volumes: Volume[];
|
||||
pages: number; // Total pages in series
|
||||
|
|
4
UI/Web/src/app/_models/tag.ts
Normal file
4
UI/Web/src/app/_models/tag.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Tag {
|
||||
id: number,
|
||||
title: string;
|
||||
}
|
|
@ -6,7 +6,10 @@ import { environment } from 'src/environments/environment';
|
|||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
import { Genre } from '../_models/genre';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRatingDto } from '../_models/metadata/age-rating-dto';
|
||||
import { Language } from '../_models/metadata/language';
|
||||
import { Person } from '../_models/person';
|
||||
import { Tag } from '../_models/tag';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -19,29 +22,57 @@ export class MetadataService {
|
|||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
// getChapterMetadata(chapterId: number) {
|
||||
// return this.httpClient.get<ChapterMetadata>(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId);
|
||||
// }
|
||||
|
||||
getAgeRating(ageRating: AgeRating) {
|
||||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
return of(this.ageRatingTypes[ageRating]);
|
||||
}
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, {responseType: 'text' as 'json'}).pipe(map(l => {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, {responseType: 'text' as 'json'}).pipe(map(ratingString => {
|
||||
if (this.ageRatingTypes === undefined) {
|
||||
this.ageRatingTypes = {};
|
||||
}
|
||||
|
||||
this.ageRatingTypes[ageRating] = l;
|
||||
this.ageRatingTypes[ageRating] = ratingString;
|
||||
return this.ageRatingTypes[ageRating];
|
||||
}));
|
||||
}
|
||||
|
||||
getAllGenres() {
|
||||
return this.httpClient.get<Genre[]>(this.baseUrl + 'metadata/genres');
|
||||
getAllAgeRatings(libraries?: Array<number>) {
|
||||
let method = 'metadata/age-ratings'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
method += '?libraryIds=' + libraries.join(',');
|
||||
}
|
||||
return this.httpClient.get<Array<AgeRatingDto>>(this.baseUrl + method);;
|
||||
}
|
||||
|
||||
getAllPeople() {
|
||||
return this.httpClient.get<Person[]>(this.baseUrl + 'metadata/people');
|
||||
getAllTags(libraries?: Array<number>) {
|
||||
let method = 'metadata/tags'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
method += '?libraryIds=' + libraries.join(',');
|
||||
}
|
||||
return this.httpClient.get<Array<Tag>>(this.baseUrl + method);;
|
||||
}
|
||||
|
||||
getAllGenres(libraries?: Array<number>) {
|
||||
let method = 'metadata/genres'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
method += '?libraryIds=' + libraries.join(',');
|
||||
}
|
||||
return this.httpClient.get<Genre[]>(this.baseUrl + method);
|
||||
}
|
||||
|
||||
getAllLanguages(libraries?: Array<number>) {
|
||||
let method = 'metadata/languages'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
method += '?libraryIds=' + libraries.join(',');
|
||||
}
|
||||
return this.httpClient.get<Language[]>(this.baseUrl + method);
|
||||
}
|
||||
|
||||
getAllPeople(libraries?: Array<number>) {
|
||||
let method = 'metadata/people'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
method += '?libraryIds=' + libraries.join(',');
|
||||
}
|
||||
return this.httpClient.get<Person[]>(this.baseUrl + method);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,18 @@ export class SeriesService {
|
|||
return paginatedVariable;
|
||||
}
|
||||
|
||||
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
let params = new HttpParams();
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
return this._cachePaginatedResults(response, this.paginatedResults);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
let params = new HttpParams();
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
@ -137,7 +149,7 @@ export class SeriesService {
|
|||
|
||||
getMetadata(seriesId: number) {
|
||||
return this.httpClient.get<SeriesMetadata>(this.baseUrl + 'series/metadata?seriesId=' + seriesId).pipe(map(items => {
|
||||
items?.tags.forEach(tag => tag.coverImage = this.imageService.getCollectionCoverImage(tag.id));
|
||||
items?.collectionTags.forEach(tag => tag.coverImage = this.imageService.getCollectionCoverImage(tag.id));
|
||||
return items;
|
||||
}));
|
||||
}
|
||||
|
@ -189,13 +201,18 @@ export class SeriesService {
|
|||
editor: [],
|
||||
publisher: [],
|
||||
character: [],
|
||||
translators: [],
|
||||
collectionTags: [],
|
||||
rating: 0,
|
||||
readStatus: {
|
||||
read: true,
|
||||
inProgress: true,
|
||||
notRead: true
|
||||
}
|
||||
},
|
||||
sortOptions: null,
|
||||
ageRating: [],
|
||||
tags: [],
|
||||
languages: []
|
||||
};
|
||||
|
||||
if (filter === undefined) return data;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<p class="text-warning pt-2">Port, Base Url, and Logging Level require a manual restart of Kavita to take effect.</p>
|
||||
<p class="text-warning pt-2">Port and Logging Level require a manual restart of Kavita to take effect.</p>
|
||||
<div class="form-group">
|
||||
<label for="settings-cachedir">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
||||
|
|
14
UI/Web/src/app/all-series/all-series.component.html
Normal file
14
UI/Web/src/app/all-series/all-series.component.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout header="All Series"
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[actions]="actions"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
0
UI/Web/src/app/all-series/all-series.component.scss
Normal file
0
UI/Web/src/app/all-series/all-series.component.scss
Normal file
145
UI/Web/src/app/all-series/all-series.component.ts
Normal file
145
UI/Web/src/app/all-series/all-series.component.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { take, debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Library } from '../_models/library';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
import { SeriesFilter } from '../_models/series-filter';
|
||||
import { ActionItem, Action } from '../_services/action-factory.service';
|
||||
import { ActionService } from '../_services/action.service';
|
||||
import { MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-all-series',
|
||||
templateUrl: './all-series.component.html',
|
||||
styleUrls: ['./all-series.component.scss']
|
||||
})
|
||||
export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
|
||||
series: Series[] = [];
|
||||
loadingSeries = false;
|
||||
pagination!: Pagination;
|
||||
actions: ActionItem<Library>[] = [];
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.AddToCollection:
|
||||
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private router: Router, private seriesService: SeriesService,
|
||||
private titleService: Title, private actionService: ActionService,
|
||||
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService) {
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
this.titleService.setTitle('Kavita - All Series');
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hubService.seriesAdded.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: SeriesAddedEvent) => {
|
||||
this.loadPage();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keyup.shift', ['$event'])
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(data: SeriesFilter) {
|
||||
this.filter = data;
|
||||
if (this.pagination !== undefined && this.pagination !== null) {
|
||||
this.pagination.currentPage = 1;
|
||||
this.onPageChange(this.pagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.loadingSeries = true;
|
||||
|
||||
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import { AuthGuard } from './_guards/auth.guard';
|
|||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||
import { OnDeckComponent } from './on-deck/on-deck.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||
|
||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||
|
||||
|
@ -55,6 +56,8 @@ const routes: Routes = [
|
|||
{path: 'library', component: DashboardComponent},
|
||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||
{path: 'on-deck', component: OnDeckComponent},
|
||||
{path: 'all-series', component: AllSeriesComponent},
|
||||
|
||||
]
|
||||
},
|
||||
{path: 'login', component: UserLoginComponent},
|
||||
|
|
|
@ -34,6 +34,7 @@ import { ConfigData } from './_models/config-data';
|
|||
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
|
||||
import { PersonRolePipe } from './person-role.pipe';
|
||||
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
|
||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
@ -52,6 +53,7 @@ import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-m
|
|||
NavEventsToggleComponent,
|
||||
PersonRolePipe,
|
||||
SeriesMetadataDetailComponent,
|
||||
AllSeriesComponent,
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
|
|
|
@ -86,8 +86,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.seriesService.getMetadata(this.series.id).subscribe(metadata => {
|
||||
if (metadata) {
|
||||
this.metadata = metadata;
|
||||
this.settings.savedData = metadata.tags;
|
||||
this.tags = metadata.tags;
|
||||
this.settings.savedData = metadata.collectionTags;
|
||||
this.tags = metadata.collectionTags;
|
||||
this.editSeriesForm.get('summary')?.setValue(this.metadata.summary);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<ng-template #filterSection>
|
||||
<div class="filter-section">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-3" *ngIf="!filterSettings.formatDisabled">
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.formatDisabled">
|
||||
<div class="form-group">
|
||||
<label for="format">Format</label>
|
||||
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
|
||||
|
@ -54,7 +54,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3"*ngIf="!filterSettings.libraryDisabled">
|
||||
<div class="col-md-2 mr-3"*ngIf="!filterSettings.libraryDisabled">
|
||||
<div class="form-group">
|
||||
<label for="libraries">Libraries</label>
|
||||
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
|
||||
|
@ -68,7 +68,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="!filterSettings.collectionDisabled">
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.collectionDisabled">
|
||||
<div class="form-group">
|
||||
<label for="collections">Collections</label>
|
||||
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
|
||||
|
@ -82,7 +82,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="!filterSettings.genresDisabled">
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.genresDisabled">
|
||||
<div class="form-group">
|
||||
<label for="genres">Genres</label>
|
||||
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
|
||||
|
@ -95,10 +95,24 @@
|
|||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.tagsDisabled">
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- The People row -->
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
|
||||
<div class="form-group">
|
||||
<label for="cover-artist">(Cover) Artists</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
|
||||
|
@ -112,7 +126,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
|
||||
<div class="form-group">
|
||||
<label for="writers">Writers</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
|
||||
|
@ -126,7 +140,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
|
||||
<div class="form-group">
|
||||
<label for="publisher">Publisher</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
|
||||
|
@ -140,9 +154,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
|
||||
<div class="form-group">
|
||||
<label for="Penciller">Penciller</label>
|
||||
<label for="penciller">Penciller</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
|
@ -154,7 +168,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
|
||||
<div class="form-group">
|
||||
<label for="letterer">Letterer</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
|
||||
|
@ -168,7 +182,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
|
||||
<div class="form-group">
|
||||
<label for="inker">Inker</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
|
||||
|
@ -182,7 +196,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
|
||||
<div class="form-group">
|
||||
<label for="editor">Editor</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
|
||||
|
@ -196,7 +210,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
|
||||
<div class="form-group">
|
||||
<label for="colorist">Colorist</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
|
||||
|
@ -210,7 +224,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
|
||||
<div class="form-group">
|
||||
<label for="character">Character</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
|
||||
|
@ -223,11 +237,23 @@
|
|||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
|
||||
<div class="form-group">
|
||||
<label for="translators">Translators</label>
|
||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- Rating/Review/Progress -->
|
||||
<div class="col" *ngIf="!filterSettings.readProgressDisabled">
|
||||
<!-- Not sure how to do this on the backend, might have to be a UI control -->
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.readProgressDisabled">
|
||||
<label>Read Progress</label>
|
||||
<form [formGroup]="readProgressGroup" class="ml-2">
|
||||
<div class="form-check form-check-inline">
|
||||
|
@ -245,7 +271,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col" *ngIf="!filterSettings.ratingDisabled">
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ratingDisabled">
|
||||
<label for="ratings">Rating</label>
|
||||
<form class="form-inline ml-2">
|
||||
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
|
||||
|
@ -255,15 +281,59 @@
|
|||
</ngb-rating>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<button class="btn btn-secondary mr-2" (click)="clear()">Clear</button>
|
||||
<button class="btn btn-primary" (click)="apply()">Apply</button>
|
||||
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ageRatingDisabled">
|
||||
<label for="age-rating">Age Rating</label>
|
||||
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.languageDisabled">
|
||||
<label for="languages">Language</label>
|
||||
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.sortDisabled">
|
||||
<form [formGroup]="sortGroup">
|
||||
<div class="form-group">
|
||||
<label for="sort-options">Sort By</label>
|
||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
||||
<i class="fa fa-arrow-down" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
|
||||
<ng-template #descSort>
|
||||
<i class="fa fa-arrow-up" title="Descending"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
<select id="sort-options" class="form-control" formControlName="sortField" style="height: 38px;">
|
||||
<option [value]="SortField.SortName">Sort Name</option>
|
||||
<option [value]="SortField.Created">Created</option>
|
||||
<option [value]="SortField.LastModified">Last Modified</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<!-- Sort by functionalities -->
|
||||
<div class="col">
|
||||
<!-- TODO: Make this float right -->
|
||||
<button class="btn btn-secondary mr-2" (click)="clear()">Clear</button>
|
||||
<button class="btn btn-primary" (click)="apply()">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Genre } from 'src/app/_models/genre';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
||||
import { Language } from 'src/app/_models/metadata/language';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
import { Person, PersonRole } from 'src/app/_models/person';
|
||||
import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter';
|
||||
import { FilterItem, mangaFormatFilters, SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||
import { Tag } from 'src/app/_models/tag';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
|
@ -29,6 +33,12 @@ export class FilterSettings {
|
|||
peopleDisabled = false;
|
||||
readProgressDisabled = false;
|
||||
ratingDisabled = false;
|
||||
presetLibraryId = 0;
|
||||
presetCollectionId = 0;
|
||||
sortDisabled = false;
|
||||
ageRatingDisabled = false;
|
||||
tagsDisabled = false;
|
||||
languageDisabled = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -59,6 +69,9 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
librarySettings: TypeaheadSettings<FilterItem<Library>> = new TypeaheadSettings();
|
||||
genreSettings: TypeaheadSettings<FilterItem<Genre>> = new TypeaheadSettings();
|
||||
collectionSettings: TypeaheadSettings<FilterItem<CollectionTag>> = new TypeaheadSettings();
|
||||
ageRatingSettings: TypeaheadSettings<FilterItem<AgeRatingDto>> = new TypeaheadSettings();
|
||||
tagsSettings: TypeaheadSettings<FilterItem<Tag>> = new TypeaheadSettings();
|
||||
languageSettings: TypeaheadSettings<FilterItem<Language>> = new TypeaheadSettings();
|
||||
peopleSettings: {[PersonRole: string]: TypeaheadSettings<FilterItem<Person>>} = {};
|
||||
resetTypeaheads: Subject<boolean> = new ReplaySubject(1);
|
||||
|
||||
|
@ -74,6 +87,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
collectionTags: Array<FilterItem<CollectionTag>> = [];
|
||||
|
||||
readProgressGroup!: FormGroup;
|
||||
sortGroup!: FormGroup;
|
||||
isAscendingSort: boolean = true;
|
||||
|
||||
updateApplied: number = 0;
|
||||
|
||||
|
@ -83,6 +98,10 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
return PersonRole;
|
||||
}
|
||||
|
||||
get SortField(): typeof SortField {
|
||||
return SortField;
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
|
@ -92,10 +111,39 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
inProgress: new FormControl(this.filter.readStatus.inProgress, []),
|
||||
});
|
||||
|
||||
this.sortGroup = new FormGroup({
|
||||
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
|
||||
});
|
||||
|
||||
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
|
||||
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
|
||||
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
|
||||
this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value;
|
||||
|
||||
let sum = 0;
|
||||
sum += (this.filter.readStatus.read ? 1 : 0);
|
||||
sum += (this.filter.readStatus.inProgress ? 1 : 0);
|
||||
sum += (this.filter.readStatus.notRead ? 1 : 0);
|
||||
|
||||
if (sum === 1) {
|
||||
if (this.filter.readStatus.read) this.readProgressGroup.get('read')?.disable({ emitEvent: false });
|
||||
if (this.filter.readStatus.notRead) this.readProgressGroup.get('notRead')?.disable({ emitEvent: false });
|
||||
if (this.filter.readStatus.inProgress) this.readProgressGroup.get('inProgress')?.disable({ emitEvent: false });
|
||||
} else {
|
||||
this.readProgressGroup.get('read')?.enable({ emitEvent: false });
|
||||
this.readProgressGroup.get('notRead')?.enable({ emitEvent: false });
|
||||
this.readProgressGroup.get('inProgress')?.enable({ emitEvent: false });
|
||||
}
|
||||
});
|
||||
|
||||
this.sortGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
|
||||
if (this.filter.sortOptions == null) {
|
||||
this.filter.sortOptions = {
|
||||
isAscending: this.isAscendingSort,
|
||||
sortField: parseInt(this.sortGroup.get('sortField')?.value, 10)
|
||||
};
|
||||
}
|
||||
this.filter.sortOptions.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -107,18 +155,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.filterSettings = new FilterSettings();
|
||||
}
|
||||
|
||||
|
||||
this.metadataService.getAllGenres().subscribe(genres => {
|
||||
this.genres = genres.map(genre => {
|
||||
return {
|
||||
title: genre.title,
|
||||
value: genre,
|
||||
selected: false,
|
||||
}
|
||||
});
|
||||
this.setupGenreTypeahead();
|
||||
|
||||
});
|
||||
this.setupGenreTypeahead();
|
||||
|
||||
this.libraryService.getLibrariesForMember().subscribe(libs => {
|
||||
this.libraries = libs.map(lib => {
|
||||
|
@ -131,27 +168,11 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.setupLibraryTypeahead();
|
||||
});
|
||||
|
||||
this.metadataService.getAllPeople().subscribe(res => {
|
||||
this.persons = res.map(lib => {
|
||||
return {
|
||||
title: lib.name,
|
||||
value: lib,
|
||||
selected: true,
|
||||
}
|
||||
});
|
||||
this.setupPersonTypeahead();
|
||||
});
|
||||
|
||||
this.collectionTagService.allTags().subscribe(tags => {
|
||||
this.collectionTags = tags.map(lib => {
|
||||
return {
|
||||
title: lib.title,
|
||||
value: lib,
|
||||
selected: false,
|
||||
}
|
||||
});
|
||||
this.setupCollectionTagTypeahead();
|
||||
});
|
||||
this.setupCollectionTagTypeahead();
|
||||
this.setupPersonTypeahead();
|
||||
this.setupAgeRatingSettings();
|
||||
this.setupTagSettings();
|
||||
this.setupLanguageSettings();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -171,7 +192,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
}
|
||||
this.formatSettings.savedData = mangaFormatFilters;
|
||||
}
|
||||
|
||||
setupLibraryTypeahead() {
|
||||
|
@ -187,6 +207,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
}
|
||||
|
||||
if (this.filterSettings.presetLibraryId > 0) {
|
||||
this.librarySettings.savedData = this.libraries.filter(item => item.value.id === this.filterSettings.presetLibraryId);
|
||||
this.filter.libraries = this.librarySettings.savedData.map(item => item.value.id);
|
||||
this.resetTypeaheads.next(true); // For some reason library just doesn't update properly with savedData
|
||||
}
|
||||
}
|
||||
|
||||
setupGenreTypeahead() {
|
||||
|
@ -196,7 +222,15 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.genreSettings.unique = true;
|
||||
this.genreSettings.addIfNonExisting = false;
|
||||
this.genreSettings.fetchFn = (filter: string) => {
|
||||
return of (this.genres)
|
||||
return this.metadataService.getAllGenres(this.filter.libraries).pipe(map(genres => {
|
||||
return genres.map(genre => {
|
||||
return {
|
||||
title: genre.title,
|
||||
value: genre,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
}));
|
||||
};
|
||||
this.genreSettings.compareFn = (options: FilterItem<Genre>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
|
@ -204,6 +238,75 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
setupAgeRatingSettings() {
|
||||
this.ageRatingSettings.minCharacters = 0;
|
||||
this.ageRatingSettings.multiple = true;
|
||||
this.ageRatingSettings.id = 'age-rating';
|
||||
this.ageRatingSettings.unique = true;
|
||||
this.ageRatingSettings.addIfNonExisting = false;
|
||||
this.ageRatingSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllAgeRatings(this.filter.libraries).pipe(map(ratings => {
|
||||
return ratings.map(rating => {
|
||||
return {
|
||||
title: rating.title,
|
||||
value: rating,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
}));
|
||||
};
|
||||
this.ageRatingSettings.compareFn = (options: FilterItem<AgeRatingDto>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
}
|
||||
|
||||
setupTagSettings() {
|
||||
this.tagsSettings.minCharacters = 0;
|
||||
this.tagsSettings.multiple = true;
|
||||
this.tagsSettings.id = 'tags';
|
||||
this.tagsSettings.unique = true;
|
||||
this.tagsSettings.addIfNonExisting = false;
|
||||
this.tagsSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllTags(this.filter.libraries).pipe(map(tags => {
|
||||
return tags.map(tag => {
|
||||
return {
|
||||
title: tag.title,
|
||||
value: tag,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
}));
|
||||
};
|
||||
this.tagsSettings.compareFn = (options: FilterItem<Tag>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
}
|
||||
|
||||
setupLanguageSettings() {
|
||||
this.languageSettings.minCharacters = 0;
|
||||
this.languageSettings.multiple = true;
|
||||
this.languageSettings.id = 'languages';
|
||||
this.languageSettings.unique = true;
|
||||
this.languageSettings.addIfNonExisting = false;
|
||||
this.languageSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllLanguages(this.filter.libraries).pipe(map(tags => {
|
||||
return tags.map(tag => {
|
||||
return {
|
||||
title: tag.title,
|
||||
value: tag,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
}));
|
||||
};
|
||||
this.languageSettings.compareFn = (options: FilterItem<Language>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
}
|
||||
|
||||
setupCollectionTagTypeahead() {
|
||||
this.collectionSettings.minCharacters = 0;
|
||||
this.collectionSettings.multiple = true;
|
||||
|
@ -211,12 +314,25 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.collectionSettings.unique = true;
|
||||
this.collectionSettings.addIfNonExisting = false;
|
||||
this.collectionSettings.fetchFn = (filter: string) => {
|
||||
return of (this.collectionTags)
|
||||
return this.collectionTagService.allTags().pipe(map(tags => {
|
||||
return tags.map(lib => {
|
||||
return {
|
||||
title: lib.title,
|
||||
value: lib,
|
||||
selected: false,
|
||||
}
|
||||
});
|
||||
}));
|
||||
};
|
||||
this.collectionSettings.compareFn = (options: FilterItem<CollectionTag>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
}
|
||||
if (this.filterSettings.presetCollectionId > 0) {
|
||||
this.collectionSettings.savedData = this.collectionTags.filter(item => item.value.id === this.filterSettings.presetCollectionId);
|
||||
this.filter.collectionTags = this.collectionSettings.savedData.map(item => item.value.id);
|
||||
this.resetTypeaheads.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
setupPersonTypeahead() {
|
||||
|
@ -224,58 +340,75 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
|
||||
var personSettings = this.createBlankPersonSettings('writers');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Writer && this.utilityService.filter(p.value.name, filter)));
|
||||
return this.fetchPeople(PersonRole.Writer, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Writer] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('character');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Character && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Character, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Character] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('colorist');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Colorist && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Colorist, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Colorist] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('cover-artist');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.CoverArtist && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.CoverArtist, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.CoverArtist] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('editor');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Editor && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Editor, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Editor] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('inker');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Inker && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Inker, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Inker] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('letterer');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Letterer && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Letterer, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Letterer] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('penciller');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Penciller && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Penciller, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Penciller] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('publisher');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return of (this.persons.filter(p => p.value.role == PersonRole.Publisher && this.utilityService.filter(p.title, filter)))
|
||||
return this.fetchPeople(PersonRole.Publisher, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Publisher] = personSettings;
|
||||
|
||||
personSettings = this.createBlankPersonSettings('translators');
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return this.fetchPeople(PersonRole.Translator, filter);
|
||||
};
|
||||
this.peopleSettings[PersonRole.Translator] = personSettings;
|
||||
}
|
||||
|
||||
fetchPeople(role: PersonRole, filter: string): Observable<FilterItem<Person>[]> {
|
||||
return this.metadataService.getAllPeople(this.filter.libraries).pipe(map(people => {
|
||||
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter)).map((p: Person) => {
|
||||
return {
|
||||
title: p.name,
|
||||
value: p,
|
||||
selected: false,
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
createBlankPersonSettings(id: string) {
|
||||
|
@ -325,6 +458,10 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.filter.genres = genres.map(item => item.value.id) || [];
|
||||
}
|
||||
|
||||
updateTagFilters(tags: FilterItem<Tag>[]) {
|
||||
this.filter.tags = tags.map(item => item.value.id) || [];
|
||||
}
|
||||
|
||||
updatePersonFilters(persons: FilterItem<Person>[], role: PersonRole) {
|
||||
switch (role) {
|
||||
case PersonRole.CoverArtist:
|
||||
|
@ -357,6 +494,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
case PersonRole.Writer:
|
||||
this.filter.writers = persons.map(p => p.value.id);
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.filter.translators = persons.map(p => p.value.id);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -369,6 +508,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.filter.rating = rating;
|
||||
}
|
||||
|
||||
updateAgeRating(ratingDtos: FilterItem<AgeRatingDto>[]) {
|
||||
this.filter.ageRating = ratingDtos.map(item => item.value.value) || [];
|
||||
}
|
||||
|
||||
updateLanguageRating(languages: FilterItem<Language>[]) {
|
||||
this.filter.languages = languages.map(item => item.value.isoCode) || [];
|
||||
}
|
||||
|
||||
updateReadStatus(status: string) {
|
||||
console.log('readstatus: ', this.filter.readStatus);
|
||||
if (status === 'read') {
|
||||
|
@ -380,13 +527,26 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
updateSortOrder() {
|
||||
this.isAscendingSort = !this.isAscendingSort;
|
||||
if (this.filter.sortOptions !== null) {
|
||||
this.filter.sortOptions.isAscending = this.isAscendingSort;
|
||||
}
|
||||
}
|
||||
|
||||
getPersonsSettings(role: PersonRole) {
|
||||
return this.peopleSettings[role];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.readProgressGroup.get('read')?.setValue(true);
|
||||
this.readProgressGroup.get('notRead')?.setValue(true);
|
||||
this.readProgressGroup.get('inProgress')?.setValue(true);
|
||||
this.sortGroup.get('sortField')?.setValue(SortField.SortName);
|
||||
this.isAscendingSort = true;
|
||||
this.resetTypeaheads.next(true);
|
||||
|
||||
this.applyFilter.emit(this.filter);
|
||||
this.updateApplied++;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@use '../../../theme/colors';
|
||||
|
||||
$triangle-size: 40px;
|
||||
$triangle-size: 30px;
|
||||
$image-height: 230px;
|
||||
$image-width: 160px;
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[pagination]="seriesPagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
|
||||
import { FilterSettings } from 'src/app/cards/card-detail-layout/card-detail-layout.component';
|
||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
|
@ -38,6 +39,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
|
||||
|
@ -95,6 +97,9 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
const tagId = parseInt(routeId, 10);
|
||||
|
||||
this.filterSettings.presetCollectionId = tagId;
|
||||
|
||||
this.updateTag(tagId);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[items]="series"
|
||||
[actions]="actions"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Library } from '../_models/library';
|
||||
|
@ -31,6 +32,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
actions: ActionItem<Library>[] = [];
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
|
@ -85,6 +87,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
this.filterSettings.presetLibraryId = this.libraryId;
|
||||
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
|
@ -147,7 +151,12 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
this.loadingSeries = true;
|
||||
|
||||
this.seriesService.getSeriesForLibrary(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter.libraries.push(this.libraryId);
|
||||
}
|
||||
|
||||
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;
|
||||
this.loadingSeries = false;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<app-carousel-reel [items]="libraries" title="Libraries">
|
||||
<app-carousel-reel [items]="libraries" title="Libraries" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-library-card [data]="item"></app-library-card>
|
||||
</ng-template>
|
||||
|
|
|
@ -110,6 +110,8 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
this.router.navigate(['recently-added']);
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
this.router.navigate(['on-deck']);
|
||||
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
||||
this.router.navigate(['all-series']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export class OnDeckComponent implements OnInit {
|
|||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
this.filterSettings.readProgressDisabled = true;
|
||||
this.filterSettings.sortDisabled = true;
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(applyFilter)="applyFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
|
@ -30,6 +31,7 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
|
|||
libraryId!: number;
|
||||
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
|
||||
onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
@ -40,6 +42,8 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
|
|||
if (this.pagination === undefined || this.pagination === null) {
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
this.filterSettings.sortDisabled = true;
|
||||
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<div class="row no-gutters {{series?.userReview ? '' : 'mt-2'}}">
|
||||
<div class="row no-gutters mt-2 mb-2">
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<div class="row no-gutters" *ngIf="seriesMetadata.ageRating">
|
||||
<app-tag-badge title="Age Rating">{{ageRatingName}}</app-tag-badge>
|
||||
<div class="row no-gutters mb-2">
|
||||
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
|
||||
<ng-container *ngIf="series">
|
||||
<!-- Maybe we can put the library this resides in to make it easier to get back -->
|
||||
<!-- tooltip here explaining how this is year of first issue -->
|
||||
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
||||
<app-tag-badge *ngIf="seriesMetadata.language !== ''" title="Language">{{seriesMetadata.language}}</app-tag-badge>
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed">
|
||||
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
|
||||
</app-tag-badge>
|
||||
|
@ -20,17 +21,25 @@
|
|||
<h5>Genres</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-tag-badge *ngFor="let genre of seriesMetadata.genres" [selectionMode]="TagBadgeCursor.Clickable">{{genre.title}}</app-tag-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Collections</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-tag-badge *ngFor="let tag of seriesMetadata.tags" a11y-click="13,32" class="clickable" routerLink="/collections/{{tag.id}}" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{tag.title}}
|
||||
</app-tag-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.collectionTags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="clickable" routerLink="/collections/{{item.id}}" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
|
||||
|
@ -38,12 +47,16 @@
|
|||
<h5>Authors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.writers" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<hr class="col-md-11">
|
||||
<hr class="col-md-11" *ngIf="hasExtendedProperites" >
|
||||
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" class="col-md-1 read-more-link" (click)="toggleView()"> <i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i> See {{isCollapsed ? 'More' : 'Less'}}</a>
|
||||
</div>
|
||||
|
||||
|
@ -53,7 +66,11 @@
|
|||
<h5>Artists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.artists" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.artists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -62,7 +79,11 @@
|
|||
<h5>Characters</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.characters" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -71,7 +92,11 @@
|
|||
<h5>Colorists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.colorists" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -80,7 +105,11 @@
|
|||
<h5>Editors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.editors" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -89,7 +118,11 @@
|
|||
<h5>Inkers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.inkers" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -98,7 +131,35 @@
|
|||
<h5>Letterers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.letterers" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Tags</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Translators</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -107,7 +168,11 @@
|
|||
<h5>Pencillers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.pencillers" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -116,7 +181,11 @@
|
|||
<h5>Publishers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.publishers" [person]="person"></app-person-badge>
|
||||
<app-badge-expander [items]="seriesMetadata.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -19,10 +19,6 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
isCollapsed: boolean = true;
|
||||
hasExtendedProperites: boolean = false;
|
||||
|
||||
/**
|
||||
* String representation of AgeRating enum
|
||||
*/
|
||||
ageRatingName: string = '';
|
||||
/**
|
||||
* Html representation of Series Summary
|
||||
*/
|
||||
|
@ -36,7 +32,7 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
return TagBadgeCursor;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService, private metadataService: MetadataService) { }
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService) { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
|
||||
|
@ -45,11 +41,9 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
this.seriesMetadata.inkers.length > 0 ||
|
||||
this.seriesMetadata.letterers.length > 0 ||
|
||||
this.seriesMetadata.pencillers.length > 0 ||
|
||||
this.seriesMetadata.publishers.length > 0;
|
||||
|
||||
this.metadataService.getAgeRating(this.seriesMetadata.ageRating).subscribe(rating => {
|
||||
this.ageRatingName = rating;
|
||||
});
|
||||
this.seriesMetadata.publishers.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0 ||
|
||||
this.seriesMetadata.tags.length > 0;
|
||||
|
||||
if (this.seriesMetadata !== null) {
|
||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<div class="badge-expander">
|
||||
<div class="content">
|
||||
<ng-container *ngFor="let item of visibleItems; index as i;" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
<button type="button" *ngIf="!isCollapsed && itemsLeft !== 0" class="btn btn-outline-primary" (click)="toggleVisible()" [attr.aria-expanded]="!isCollapsed">
|
||||
and {{itemsLeft}} more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
height: 35px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.badge-expander {
|
||||
//display: inline-block;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-badge-expander',
|
||||
templateUrl: './badge-expander.component.html',
|
||||
styleUrls: ['./badge-expander.component.scss']
|
||||
})
|
||||
export class BadgeExpanderComponent implements OnInit {
|
||||
|
||||
@Input() items: Array<any> = [];
|
||||
@ContentChild('badgeExpanderItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
|
||||
visibleItems: Array<any> = [];
|
||||
isCollapsed: boolean = false;
|
||||
|
||||
get itemsLeft() {
|
||||
return Math.max(this.items.length - 4, 0);
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.visibleItems = this.items.slice(0, 4);
|
||||
}
|
||||
|
||||
toggleVisible() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
this.visibleItems = this.items;
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ import { CircularLoaderComponent } from './circular-loader/circular-loader.compo
|
|||
import { NgCircleProgressModule } from 'ng-circle-progress';
|
||||
import { SentenceCasePipe } from './sentence-case.pipe';
|
||||
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
||||
import { BadgeExpanderComponent } from './badge-expander/badge-expander.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -32,7 +33,8 @@ import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
|||
UpdateNotificationModalComponent,
|
||||
CircularLoaderComponent,
|
||||
SentenceCasePipe,
|
||||
PersonBadgeComponent
|
||||
PersonBadgeComponent,
|
||||
BadgeExpanderComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -55,7 +57,8 @@ import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
|||
SeriesFormatComponent,
|
||||
TagBadgeComponent,
|
||||
CircularLoaderComponent,
|
||||
PersonBadgeComponent
|
||||
PersonBadgeComponent,
|
||||
BadgeExpanderComponent
|
||||
],
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
|
|
@ -35,6 +35,9 @@ input {
|
|||
line-height: inherit !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
input:empty {
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .bg-dark .typeahead-input {
|
||||
|
@ -62,7 +65,7 @@ input {
|
|||
overflow-x: hidden;
|
||||
|
||||
.list-group-item {
|
||||
padding: 5px 5px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -254,7 +254,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
handleDocumentClick() {
|
||||
handleDocumentClick(event: any) {
|
||||
this.hasFocus = false;
|
||||
}
|
||||
|
||||
|
@ -370,6 +370,8 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (this.inputElem) {
|
||||
// hack: To prevent multiple typeaheads from being open at once, click document then trigger the focus
|
||||
document.querySelector('body')?.click();
|
||||
this.inputElem.nativeElement.focus();
|
||||
this.hasFocus = true;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue