New Scanner + People Pages (#3286)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
1ed0eae22d
commit
ba20ad4ecc
142 changed files with 17529 additions and 3038 deletions
|
@ -125,7 +125,6 @@ $image-width: 160px;
|
|||
|
||||
.overlay-information--centered {
|
||||
position: absolute;
|
||||
border-radius: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50px;
|
||||
top: 50%;
|
||||
|
@ -169,7 +168,7 @@ $image-width: 160px;
|
|||
margin: 0;
|
||||
text-align: center;
|
||||
max-width: 98px;
|
||||
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -17,7 +17,15 @@ export enum PersonRole {
|
|||
}
|
||||
|
||||
export interface Person {
|
||||
id: number;
|
||||
name: string;
|
||||
role: PersonRole;
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage?: string;
|
||||
coverImageLocked: boolean;
|
||||
malId?: number;
|
||||
aniListId?: number;
|
||||
hardcoverId?: string;
|
||||
asin?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {PersonRole} from "../person";
|
||||
|
||||
export enum FilterField
|
||||
{
|
||||
None = -1,
|
||||
|
@ -47,3 +49,36 @@ enumArray.sort((a, b) => a.value.localeCompare(b.value));
|
|||
|
||||
export const allFields = enumArray
|
||||
.map(key => parseInt(key.key, 10))as FilterField[];
|
||||
|
||||
export const allPeople = [
|
||||
FilterField.Characters,
|
||||
FilterField.Colorist,
|
||||
FilterField.CoverArtist,
|
||||
FilterField.Editor,
|
||||
FilterField.Inker,
|
||||
FilterField.Letterer,
|
||||
FilterField.Penciller,
|
||||
FilterField.Publisher,
|
||||
FilterField.Translators,
|
||||
FilterField.Writers,
|
||||
];
|
||||
|
||||
export const personRoleForFilterField = (role: PersonRole) => {
|
||||
switch (role) {
|
||||
case PersonRole.Artist: return FilterField.CoverArtist;
|
||||
case PersonRole.Character: return FilterField.Characters;
|
||||
case PersonRole.Colorist: return FilterField.Colorist;
|
||||
case PersonRole.CoverArtist: return FilterField.CoverArtist;
|
||||
case PersonRole.Editor: return FilterField.Editor;
|
||||
case PersonRole.Inker: return FilterField.Inker;
|
||||
case PersonRole.Letterer: return FilterField.Letterer;
|
||||
case PersonRole.Penciller: return FilterField.Penciller;
|
||||
case PersonRole.Publisher: return FilterField.Publisher;
|
||||
case PersonRole.Translator: return FilterField.Translators;
|
||||
case PersonRole.Writer: return FilterField.Writers;
|
||||
case PersonRole.Imprint: return FilterField.Imprint;
|
||||
case PersonRole.Location: return FilterField.Location;
|
||||
case PersonRole.Team: return FilterField.Team;
|
||||
case PersonRole.Other: return FilterField.None;
|
||||
}
|
||||
};
|
||||
|
|
6
UI/Web/src/app/_models/person/browse-person.ts
Normal file
6
UI/Web/src/app/_models/person/browse-person.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {Person} from "../metadata/person";
|
||||
|
||||
export interface BrowsePerson extends Person {
|
||||
seriesCount: number;
|
||||
issueCount: number;
|
||||
}
|
|
@ -4,14 +4,18 @@ import { MangaFile } from "../manga-file";
|
|||
import { SearchResult } from "./search-result";
|
||||
import { Tag } from "../tag";
|
||||
import {BookmarkSearchResult} from "./bookmark-search-result";
|
||||
import {Genre} from "../metadata/genre";
|
||||
import {ReadingList} from "../reading-list";
|
||||
import {UserCollection} from "../collection-tag";
|
||||
import {Person} from "../metadata/person";
|
||||
|
||||
export class SearchResultGroup {
|
||||
libraries: Array<Library> = [];
|
||||
series: Array<SearchResult> = [];
|
||||
collections: Array<Tag> = [];
|
||||
readingLists: Array<Tag> = [];
|
||||
persons: Array<Tag> = [];
|
||||
genres: Array<Tag> = [];
|
||||
collections: Array<UserCollection> = [];
|
||||
readingLists: Array<ReadingList> = [];
|
||||
persons: Array<Person> = [];
|
||||
genres: Array<Genre> = [];
|
||||
tags: Array<Tag> = [];
|
||||
files: Array<MangaFile> = [];
|
||||
chapters: Array<Chapter> = [];
|
||||
|
|
|
@ -7,4 +7,5 @@ export enum SideNavStreamType {
|
|||
ExternalSource = 6,
|
||||
AllSeries = 7,
|
||||
WantToRead = 8,
|
||||
BrowseAuthors = 9
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {SideNavStreamType} from "./sidenav-stream-type.enum";
|
||||
import {Library, LibraryType} from "../library/library";
|
||||
import {Library} from "../library/library";
|
||||
import {CommonStream} from "../common-stream";
|
||||
import {ExternalSource} from "./external-source";
|
||||
|
||||
|
|
9
UI/Web/src/app/_models/standalone-chapter.ts
Normal file
9
UI/Web/src/app/_models/standalone-chapter.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {Chapter} from "./chapter";
|
||||
import {LibraryType} from "./library/library";
|
||||
|
||||
export interface StandaloneChapter extends Chapter {
|
||||
seriesId: number;
|
||||
libraryId: number;
|
||||
libraryType: LibraryType;
|
||||
volumeTitle?: string;
|
||||
}
|
|
@ -18,7 +18,7 @@ export class PersonRolePipe implements PipeTransform {
|
|||
case PersonRole.Colorist:
|
||||
return this.translocoService.translate('person-role-pipe.colorist');
|
||||
case PersonRole.CoverArtist:
|
||||
return this.translocoService.translate('person-role-pipe.cover-artist');
|
||||
return this.translocoService.translate('person-role-pipe.artist');
|
||||
case PersonRole.Editor:
|
||||
return this.translocoService.translate('person-role-pipe.editor');
|
||||
case PersonRole.Inker:
|
||||
|
|
8
UI/Web/src/app/_routes/browse-authors-routing.module.ts
Normal file
8
UI/Web/src/app/_routes/browse-authors-routing.module.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Routes } from "@angular/router";
|
||||
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
|
||||
import {BrowseAuthorsComponent} from "../browse-people/browse-authors.component";
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '', component: BrowseAuthorsComponent, pathMatch: 'full'},
|
||||
];
|
19
UI/Web/src/app/_routes/person-detail-routing.module.ts
Normal file
19
UI/Web/src/app/_routes/person-detail-routing.module.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { AuthGuard } from '../_guards/auth.guard';
|
||||
import {PersonDetailComponent} from "../person-detail/person-detail.component";
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: ':name',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
component: PersonDetailComponent
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
component: PersonDetailComponent
|
||||
}
|
||||
];
|
|
@ -12,6 +12,7 @@ import { DeviceService } from './device.service';
|
|||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
import {translate} from "@jsverse/transloco";
|
||||
import {Person} from "../_models/metadata/person";
|
||||
|
||||
export enum Action {
|
||||
Submenu = -1,
|
||||
|
@ -160,6 +161,8 @@ export class ActionFactoryService {
|
|||
|
||||
bookmarkActions: Array<ActionItem<Series>> = [];
|
||||
|
||||
private personActions: Array<ActionItem<Person>> = [];
|
||||
|
||||
sideNavStreamActions: Array<ActionItem<SideNavStream>> = [];
|
||||
smartFilterActions: Array<ActionItem<SmartFilter>> = [];
|
||||
|
||||
|
@ -180,11 +183,11 @@ export class ActionFactoryService {
|
|||
}
|
||||
|
||||
getLibraryActions(callback: ActionCallback<Library>) {
|
||||
return this.applyCallbackToList(this.libraryActions, callback);
|
||||
return this.applyCallbackToList(this.libraryActions, callback);
|
||||
}
|
||||
|
||||
getSeriesActions(callback: ActionCallback<Series>) {
|
||||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
}
|
||||
|
||||
getSideNavStreamActions(callback: ActionCallback<SideNavStream>) {
|
||||
|
@ -196,7 +199,7 @@ export class ActionFactoryService {
|
|||
}
|
||||
|
||||
getVolumeActions(callback: ActionCallback<Volume>) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
}
|
||||
|
||||
getChapterActions(callback: ActionCallback<Chapter>) {
|
||||
|
@ -204,7 +207,7 @@ export class ActionFactoryService {
|
|||
}
|
||||
|
||||
getCollectionTagActions(callback: ActionCallback<UserCollection>) {
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback);
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback);
|
||||
}
|
||||
|
||||
getReadingListActions(callback: ActionCallback<ReadingList>) {
|
||||
|
@ -215,6 +218,10 @@ export class ActionFactoryService {
|
|||
return this.applyCallbackToList(this.bookmarkActions, callback);
|
||||
}
|
||||
|
||||
getPersonActions(callback: ActionCallback<Person>) {
|
||||
return this.applyCallbackToList(this.personActions, callback);
|
||||
}
|
||||
|
||||
dummyCallback(action: ActionItem<any>, data: any) {}
|
||||
|
||||
filterSendToAction(actions: Array<ActionItem<Chapter>>, chapter: Chapter) {
|
||||
|
@ -424,7 +431,7 @@ export class ActionFactoryService {
|
|||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
{
|
||||
action: Action.AddToWantToReadList,
|
||||
title: 'add-to-want-to-read',
|
||||
description: 'add-to-want-to-read-tooltip',
|
||||
|
@ -579,23 +586,23 @@ export class ActionFactoryService {
|
|||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'add-to',
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'add-to',
|
||||
description: '=',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
description: 'add-to-reading-list-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'send-to',
|
||||
|
@ -676,23 +683,23 @@ export class ActionFactoryService {
|
|||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'add-to',
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'add-to',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
description: 'add-to-reading-list-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'send-to',
|
||||
|
@ -785,6 +792,17 @@ export class ActionFactoryService {
|
|||
},
|
||||
];
|
||||
|
||||
this.personActions = [
|
||||
{
|
||||
action: Action.Edit,
|
||||
title: 'edit',
|
||||
description: 'edit-person-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true,
|
||||
children: [],
|
||||
}
|
||||
];
|
||||
|
||||
this.bookmarkActions = [
|
||||
{
|
||||
action: Action.ViewSeries,
|
||||
|
@ -854,13 +872,13 @@ export class ActionFactoryService {
|
|||
});
|
||||
}
|
||||
|
||||
public applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: ActionItem<any>, data: any) => void): Array<ActionItem<any>> {
|
||||
const actions = list.map((a) => {
|
||||
return { ...a };
|
||||
});
|
||||
actions.forEach((action) => this.applyCallback(action, callback));
|
||||
return actions;
|
||||
}
|
||||
public applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: ActionItem<any>, data: any) => void): Array<ActionItem<any>> {
|
||||
const actions = list.map((a) => {
|
||||
return { ...a };
|
||||
});
|
||||
actions.forEach((action) => this.applyCallback(action, callback));
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Checks the whole tree for the action and returns true if it exists
|
||||
public hasAction(actions: Array<ActionItem<any>>, action: Action) {
|
||||
|
|
|
@ -17,18 +17,21 @@ export class ImageService {
|
|||
public errorImage = 'assets/images/error-placeholder2.dark-min.png';
|
||||
public resetCoverImage = 'assets/images/image-reset-cover-min.png';
|
||||
public errorWebLinkImage = 'assets/images/broken-white-32x32.png';
|
||||
public nextChapterImage = 'assets/images/image-placeholder.dark-min.png'
|
||||
public nextChapterImage = 'assets/images/image-placeholder.dark-min.png';
|
||||
public noPersonImage = 'assets/images/error-person-missing.dark.min.png';
|
||||
|
||||
constructor(private accountService: AccountService, private themeService: ThemeService) {
|
||||
this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => {
|
||||
if (this.themeService.isDarkTheme()) {
|
||||
this.placeholderImage = 'assets/images/image-placeholder.dark-min.png';
|
||||
this.errorImage = 'assets/images/error-placeholder2.dark-min.png';
|
||||
this.errorWebLinkImage = 'assets/images/broken-white-32x32.png';
|
||||
this.errorWebLinkImage = 'assets/images/broken-black-32x32.png';
|
||||
this.noPersonImage = 'assets/images/error-person-missing.dark.min.png';
|
||||
} else {
|
||||
this.placeholderImage = 'assets/images/image-placeholder-min.png';
|
||||
this.errorImage = 'assets/images/error-placeholder2-min.png';
|
||||
this.errorWebLinkImage = 'assets/images/broken-black-32x32.png';
|
||||
this.errorWebLinkImage = 'assets/images/broken-white-32x32.png';
|
||||
this.noPersonImage = 'assets/images/error-person-missing.min.png';
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -59,6 +62,13 @@ export class ImageService {
|
|||
return part.substring(0, equalIndex).replace('Id', '');
|
||||
}
|
||||
|
||||
getPersonImage(personId: number) {
|
||||
return `${this.baseUrl}image/person-cover?personId=${personId}&apiKey=${this.encodedKey}`;
|
||||
}
|
||||
getPersonImageByName(name: string) {
|
||||
return `${this.baseUrl}image/person-cover-by-name?name=${name}&apiKey=${this.encodedKey}`;
|
||||
}
|
||||
|
||||
getLibraryCoverImage(libraryId: number) {
|
||||
return `${this.baseUrl}image/library-cover?libraryId=${libraryId}&apiKey=${this.encodedKey}`;
|
||||
}
|
||||
|
|
53
UI/Web/src/app/_services/person.service.ts
Normal file
53
UI/Web/src/app/_services/person.service.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient, HttpParams} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {Person, PersonRole} from "../_models/metadata/person";
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {PaginatedResult} from "../_models/pagination";
|
||||
import {Series} from "../_models/series";
|
||||
import {map} from "rxjs/operators";
|
||||
import {UtilityService} from "../shared/_services/utility.service";
|
||||
import {BrowsePerson} from "../_models/person/browse-person";
|
||||
import {Chapter} from "../_models/chapter";
|
||||
import {StandaloneChapter} from "../_models/standalone-chapter";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PersonService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||
|
||||
updatePerson(person: Person) {
|
||||
return this.httpClient.post<Person>(this.baseUrl + "person/update", person);
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.httpClient.get<Person>(this.baseUrl + `person?name=${name}`);
|
||||
}
|
||||
|
||||
getRolesForPerson(name: string) {
|
||||
return this.httpClient.get<Array<PersonRole>>(this.baseUrl + `person/roles?name=${name}`);
|
||||
}
|
||||
|
||||
getSeriesMostKnownFor(personId: number) {
|
||||
return this.httpClient.get<Array<Series>>(this.baseUrl + `person/series-known-for?personId=${personId}`);
|
||||
}
|
||||
|
||||
getChaptersByRole(personId: number, role: PersonRole) {
|
||||
return this.httpClient.get<Array<StandaloneChapter>>(this.baseUrl + `person/chapters-by-role?personId=${personId}&role=${role}`);
|
||||
}
|
||||
|
||||
getAuthorsToBrowse(pageNum?: number, itemsPerPage?: number) {
|
||||
let params = new HttpParams();
|
||||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
||||
return this.httpClient.post<PaginatedResult<BrowsePerson[]>>(this.baseUrl + 'person/authors', {}, {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
return this.utilityService.createPaginatedResult(response) as PaginatedResult<BrowsePerson[]>;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -195,7 +195,7 @@ export class ThemeService {
|
|||
* @param entity
|
||||
* @param id
|
||||
*/
|
||||
refreshColorScape(entity: 'series' | 'volume' | 'chapter', id: number) {
|
||||
refreshColorScape(entity: 'series' | 'volume' | 'chapter' | 'person', id: number) {
|
||||
return this.httpClient.get<ColorScape>(`${this.baseUrl}colorscape/${entity}?id=${id}`).pipe(tap((cs) => {
|
||||
this.setColorScape(cs.primary || '', cs.secondary);
|
||||
}));
|
||||
|
|
|
@ -64,6 +64,12 @@ export class UploadService {
|
|||
}));
|
||||
}
|
||||
|
||||
updatePersonCoverImage(personId: number, url: string, lockCover: boolean = true) {
|
||||
return this.httpClient.post<number>(this.baseUrl + 'upload/person', {id: personId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => {
|
||||
this.toastr.info(translate('series-detail.cover-change'));
|
||||
}));
|
||||
}
|
||||
|
||||
resetChapterCoverLock(chapterId: number, ) {
|
||||
return this.httpClient.post<number>(this.baseUrl + 'upload/reset-chapter-lock', {id: chapterId, url: ''});
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.writers" [title]="t('writers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Writers, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.colorists" [title]="t('colorists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Colorist, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.editors" [title]="t('editors-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Editor, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.coverArtists" [title]="t('cover-artists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.CoverArtist, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -74,7 +74,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.inkers" [title]="t('inkers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Inker, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -82,7 +82,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.letterers" [title]="t('letterers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Letterer, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -90,7 +90,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.pencillers" [title]="t('pencillers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Penciller, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.translators" [title]="t('translators-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Translators, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -106,7 +106,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.characters" [title]="t('characters-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Characters, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -114,7 +114,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.locations" [title]="t('locations-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Location, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -122,7 +122,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.teams" [title]="t('teams-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Team, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -130,7 +130,7 @@
|
|||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.imprints" [title]="t('imprints-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Imprint, item)"></app-person-badge>
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
|
|
@ -46,16 +46,8 @@ export class DetailsTabComponent {
|
|||
@Input() webLinks: Array<string> = [];
|
||||
|
||||
|
||||
openPerson(queryParamName: FilterField, filter: Person) {
|
||||
if (queryParamName === FilterField.None) return;
|
||||
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter.id}`).subscribe();
|
||||
}
|
||||
|
||||
openGeneric(queryParamName: FilterField, filter: string | number) {
|
||||
if (queryParamName === FilterField.None) return;
|
||||
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe();
|
||||
}
|
||||
|
||||
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ export class EditChapterModalComponent implements OnInit {
|
|||
})).subscribe();
|
||||
|
||||
this.editForm.addControl('titleName', new FormControl(this.chapter.titleName, []));
|
||||
this.editForm.addControl('sortOrder', new FormControl(this.chapter.sortOrder, [Validators.required, Validators.min(0)]));
|
||||
this.editForm.addControl('sortOrder', new FormControl(Math.max(0, this.chapter.sortOrder), [Validators.required, Validators.min(0)]));
|
||||
this.editForm.addControl('summary', new FormControl(this.chapter.summary || '', []));
|
||||
this.editForm.addControl('language', new FormControl(this.chapter.language, []));
|
||||
this.editForm.addControl('isbn', new FormControl(this.chapter.isbn, []));
|
||||
|
@ -466,12 +466,12 @@ export class EditChapterModalComponent implements OnInit {
|
|||
|
||||
fetchPeople(role: PersonRole, filter: string) {
|
||||
return this.metadataService.getAllPeople().pipe(map(people => {
|
||||
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
|
||||
return people.filter(p => this.utilityService.filter(p.name, filter));
|
||||
}));
|
||||
}
|
||||
|
||||
createBlankPersonSettings(id: string, role: PersonRole) {
|
||||
var personSettings = new TypeaheadSettings<Person>();
|
||||
let personSettings = new TypeaheadSettings<Person>();
|
||||
personSettings.minCharacters = 0;
|
||||
personSettings.multiple = true;
|
||||
personSettings.showLocked = true;
|
||||
|
@ -486,14 +486,14 @@ export class EditChapterModalComponent implements OnInit {
|
|||
}
|
||||
|
||||
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
return a.name == b.name;
|
||||
}
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return this.fetchPeople(role, filter).pipe(map(items => personSettings.compareFn(items, filter)));
|
||||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, role: role };
|
||||
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false };
|
||||
});
|
||||
|
||||
return personSettings;
|
||||
|
|
|
@ -46,6 +46,14 @@ const routes: Routes = [
|
|||
path: 'home',
|
||||
loadChildren: () => import('./_routes/dashboard-routing.module').then(m => m.routes)
|
||||
},
|
||||
{
|
||||
path: 'person',
|
||||
loadChildren: () => import('./_routes/person-detail-routing.module').then(m => m.routes)
|
||||
},
|
||||
{
|
||||
path: 'browse/authors',
|
||||
loadChildren: () => import('./_routes/browse-authors-routing.module').then(m => m.routes)
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
runGuardsAndResolvers: 'always',
|
||||
|
|
33
UI/Web/src/app/browse-people/browse-authors.component.html
Normal file
33
UI/Web/src/app/browse-people/browse-authors.component.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
<div class="main-container container-fluid">
|
||||
<ng-container *transloco="let t; read:'browse-authors'" >
|
||||
<app-side-nav-companion-bar [hasFilter]="false">
|
||||
<h2 title>
|
||||
<span>{{t('title')}}</span>
|
||||
</h2>
|
||||
<h6 subtitle>{{t('author-count', {num: pagination.totalItems | number})}} </h6>
|
||||
|
||||
</app-side-nav-companion-bar>
|
||||
|
||||
<app-card-detail-layout
|
||||
[isLoading]="isLoading"
|
||||
[items]="authors"
|
||||
[pagination]="pagination"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
[filteringDisabled]="true"
|
||||
[refresh]="refresh"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-person-card [entity]="item" [title]="item.name" [imageUrl]="imageService.getPersonImage(item.id)" (clicked)="goToPerson(item)">
|
||||
<ng-template #subtitle>
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<div style="font-size: 12px">{{item.seriesCount | compactNumber}} series</div>
|
||||
<div style="font-size: 12px">{{item.issueCount | compactNumber}} issues</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-person-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
.main-container {
|
||||
margin-top: 10px;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
90
UI/Web/src/app/browse-people/browse-authors.component.ts
Normal file
90
UI/Web/src/app/browse-people/browse-authors.component.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {Series} from "../_models/series";
|
||||
import {Pagination} from "../_models/pagination";
|
||||
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {ActionFactoryService} from "../_services/action-factory.service";
|
||||
import {ActionService} from "../_services/action.service";
|
||||
import {MessageHubService} from "../_services/message-hub.service";
|
||||
import {UtilityService} from "../shared/_services/utility.service";
|
||||
import {PersonService} from "../_services/person.service";
|
||||
import {BrowsePerson} from "../_models/person/browse-person";
|
||||
import {CardItemComponent} from "../cards/card-item/card-item.component";
|
||||
import {JumpbarService} from "../_services/jumpbar.service";
|
||||
import {PersonCardComponent} from "../cards/person-card/person-card.component";
|
||||
import {ImageService} from "../_services/image.service";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {CompactNumberPipe} from "../_pipes/compact-number.pipe";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-browse-authors',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SideNavCompanionBarComponent,
|
||||
TranslocoDirective,
|
||||
CardDetailLayoutComponent,
|
||||
DecimalPipe,
|
||||
CardItemComponent,
|
||||
PersonCardComponent,
|
||||
CompactNumberPipe,
|
||||
],
|
||||
templateUrl: './browse-authors.component.html',
|
||||
styleUrl: './browse-authors.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BrowseAuthorsComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly titleService = inject(Title);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly hubService = inject(MessageHubService);
|
||||
private readonly utilityService = inject(UtilityService);
|
||||
private readonly personService = inject(PersonService);
|
||||
private readonly jumpbarService = inject(JumpbarService);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
|
||||
|
||||
series: Series[] = [];
|
||||
isLoading = false;
|
||||
authors: Array<BrowsePerson> = [];
|
||||
pagination: Pagination = {currentPage: 0, totalPages: 0, totalItems: 0, itemsPerPage: 0};
|
||||
refresh: EventEmitter<void> = new EventEmitter();
|
||||
jumpKeys: Array<JumpKey> = [];
|
||||
trackByIdentity = (index: number, item: BrowsePerson) => `${item.id}`;
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.personService.getAuthorsToBrowse(undefined, undefined).subscribe(d => {
|
||||
this.authors = d.result;
|
||||
this.pagination = d.pagination;
|
||||
this.jumpKeys = this.jumpbarService.getJumpKeys(this.authors, d => d.name);
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
goToPerson(person: BrowsePerson) {
|
||||
this.router.navigate(['person', person.name]);
|
||||
}
|
||||
|
||||
}
|
|
@ -488,12 +488,12 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
|
||||
fetchPeople(role: PersonRole, filter: string) {
|
||||
return this.metadataService.getAllPeople().pipe(map(people => {
|
||||
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
|
||||
return people.filter(p => this.utilityService.filter(p.name, filter));
|
||||
}));
|
||||
}
|
||||
|
||||
createBlankPersonSettings(id: string, role: PersonRole) {
|
||||
var personSettings = new TypeaheadSettings<Person>();
|
||||
const personSettings = new TypeaheadSettings<Person>();
|
||||
personSettings.minCharacters = 0;
|
||||
personSettings.multiple = true;
|
||||
personSettings.showLocked = true;
|
||||
|
@ -508,14 +508,14 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
}
|
||||
|
||||
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
return a.name == b.name;
|
||||
}
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return this.fetchPeople(role, filter).pipe(map(items => personSettings.compareFn(items, filter)));
|
||||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, role: role };
|
||||
return {id: 0, name: title, description: '', coverImageLocked: false };
|
||||
});
|
||||
|
||||
return personSettings;
|
||||
|
|
|
@ -46,6 +46,9 @@ import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
|||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {PromotedIconComponent} from "../../shared/_components/promoted-icon/promoted-icon.component";
|
||||
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
|
||||
import {BrowsePerson} from "../../_models/person/browse-person";
|
||||
|
||||
export type CardEntity = Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter | BrowsePerson;
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-item',
|
||||
|
@ -116,7 +119,7 @@ export class CardItemComponent implements OnInit {
|
|||
/**
|
||||
* This is the entity we are representing. It will be returned if an action is executed.
|
||||
*/
|
||||
@Input({required: true}) entity!: Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter;
|
||||
@Input({required: true}) entity!: CardEntity;
|
||||
/**
|
||||
* If the entity is selected or not.
|
||||
*/
|
||||
|
@ -161,7 +164,7 @@ export class CardItemComponent implements OnInit {
|
|||
* When the card is selected.
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
@Output() readClicked = new EventEmitter<Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter>();
|
||||
@Output() readClicked = new EventEmitter<CardEntity>();
|
||||
@ContentChild('subtitle') subtitleTemplate!: TemplateRef<any>;
|
||||
/**
|
||||
* Library name item belongs to
|
||||
|
|
49
UI/Web/src/app/cards/person-card/person-card.component.html
Normal file
49
UI/Web/src/app/cards/person-card/person-card.component.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<ng-container *transloco="let t; read: 'card-item'">
|
||||
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
|
||||
<div class="overlay" (click)="handleClick($event)">
|
||||
@if(entity.coverImage) {
|
||||
<app-image [imageUrl]="imageUrl" [errorImage]="imageService.noPersonImage" [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="158px" width="158px"></app-image>
|
||||
} @else {
|
||||
<div class="missing-img mx-auto">
|
||||
<i class="fas fa-user fs-2" aria-hidden="true"></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@if (allowSelection) {
|
||||
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)">
|
||||
<input type="checkbox" class="form-check-input" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (count > 1) {
|
||||
<div class="count">
|
||||
<span class="badge bg-primary">{{count}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
</div>
|
||||
@if (title.length > 0 || actions.length > 0) {
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
|
||||
{{title}}
|
||||
</span>
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions float-end">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (subtitleTemplate) {
|
||||
<div style="text-align: center">
|
||||
<ng-container [ngTemplateOutlet]="subtitleTemplate" [ngTemplateOutletContext]="{ $implicit: entity }"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
31
UI/Web/src/app/cards/person-card/person-card.component.scss
Normal file
31
UI/Web/src/app/cards/person-card/person-card.component.scss
Normal file
|
@ -0,0 +1,31 @@
|
|||
|
||||
$image-height: 160px;
|
||||
@use '../../../card-item-common';
|
||||
|
||||
// Override so we can have square cards
|
||||
|
||||
.bulk-mode {
|
||||
&.always-show {
|
||||
height: $image-height;
|
||||
}
|
||||
}
|
||||
|
||||
.card-item-container {
|
||||
.overlay {
|
||||
height: $image-height;
|
||||
|
||||
/* TODO: Robbie fix this hack */
|
||||
.missing-img {
|
||||
position: absolute;
|
||||
left: 43%;
|
||||
top: 25%;
|
||||
}
|
||||
}
|
||||
.card-overlay {
|
||||
height: $image-height;
|
||||
}
|
||||
.card-body {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
157
UI/Web/src/app/cards/person-card/person-card.component.ts
Normal file
157
UI/Web/src/app/cards/person-card/person-card.component.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, ContentChild,
|
||||
DestroyRef, EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input, Output, TemplateRef
|
||||
} from '@angular/core';
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {BulkSelectionService} from "../bulk-selection.service";
|
||||
import {LibraryService} from "../../_services/library.service";
|
||||
import {DownloadService} from "../../shared/_services/download.service";
|
||||
import {UtilityService} from "../../shared/_services/utility.service";
|
||||
import {MessageHubService} from "../../_services/message-hub.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {NgTemplateOutlet} from "@angular/common";
|
||||
import {BrowsePerson} from "../../_models/person/browse-person";
|
||||
import {Person} from "../../_models/metadata/person";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-person-card',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgbTooltip,
|
||||
CardActionablesComponent,
|
||||
NgTemplateOutlet,
|
||||
FormsModule,
|
||||
ImageComponent,
|
||||
TranslocoDirective
|
||||
],
|
||||
templateUrl: './person-card.component.html',
|
||||
styleUrl: './person-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PersonCardComponent {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
public readonly imageService = inject(ImageService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly scrollService = inject(ScrollService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
/**
|
||||
* Card item url. Will internally handle error and missing covers
|
||||
*/
|
||||
@Input() imageUrl = '';
|
||||
/**
|
||||
* Name of the card
|
||||
*/
|
||||
@Input() title = '';
|
||||
/**
|
||||
* If the entity is selected or not.
|
||||
*/
|
||||
@Input() selected: boolean = false;
|
||||
/**
|
||||
* Any actions to perform on the card
|
||||
*/
|
||||
@Input() actions: ActionItem<any>[] = [];
|
||||
/**
|
||||
* This is the entity we are representing. It will be returned if an action is executed.
|
||||
*/
|
||||
@Input({required: true}) entity!: BrowsePerson | Person;
|
||||
/**
|
||||
* If the entity should show selection code
|
||||
*/
|
||||
@Input() allowSelection: boolean = false;
|
||||
/**
|
||||
* The number of updates/items within the card. If less than 2, will not be shown.
|
||||
*/
|
||||
@Input() count: number = 0;
|
||||
/**
|
||||
* Event emitted when item is clicked
|
||||
*/
|
||||
@Output() clicked = new EventEmitter<string>();
|
||||
/**
|
||||
* When the card is selected.
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
@ContentChild('subtitle') subtitleTemplate!: TemplateRef<any>;
|
||||
|
||||
tooltipTitle: string = this.title;
|
||||
/**
|
||||
* Handles touch events for selection on mobile devices
|
||||
*/
|
||||
prevTouchTime: number = 0;
|
||||
/**
|
||||
* Handles touch events for selection on mobile devices to ensure you aren't touch scrolling
|
||||
*/
|
||||
prevOffset: number = 0;
|
||||
selectionInProgress: boolean = false;
|
||||
|
||||
@HostListener('touchmove', ['$event'])
|
||||
onTouchMove(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
|
||||
this.selectionInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
onTouchStart(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
|
||||
this.prevTouchTime = event.timeStamp;
|
||||
this.prevOffset = this.scrollService.scrollPosition;
|
||||
this.selectionInProgress = true;
|
||||
}
|
||||
|
||||
@HostListener('touchend', ['$event'])
|
||||
onTouchEnd(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
const delta = event.timeStamp - this.prevTouchTime;
|
||||
const verticalOffset = this.scrollService.scrollPosition;
|
||||
|
||||
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset) && this.selectionInProgress) {
|
||||
this.handleSelection();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
this.prevTouchTime = 0;
|
||||
this.selectionInProgress = false;
|
||||
}
|
||||
|
||||
|
||||
handleClick(event?: any) {
|
||||
if (this.bulkSelectionService.hasSelections()) {
|
||||
this.handleSelection();
|
||||
return;
|
||||
}
|
||||
this.clicked.emit(this.title);
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.entity);
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(event?: any) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.selection.emit(this.selected);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
}
|
|
@ -14,20 +14,20 @@
|
|||
<app-loading [absolute]="true" [loading]="bulkLoader"></app-loading>
|
||||
@if (filter) {
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
[refresh]="refresh"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
[refresh]="refresh"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [series]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
}
|
||||
|
|
|
@ -47,11 +47,11 @@ import {LoadingComponent} from "../shared/loading/loading.component";
|
|||
import {debounceTime, ReplaySubject, tap} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-detail',
|
||||
templateUrl: './library-detail.component.html',
|
||||
styleUrls: ['./library-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
selector: 'app-library-detail',
|
||||
templateUrl: './library-detail.component.html',
|
||||
styleUrls: ['./library-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent,
|
||||
CardDetailLayoutComponent, SeriesCardComponent, BulkOperationsComponent, NgbNavOutlet, DecimalPipe, SentenceCasePipe, TranslocoDirective, LoadingComponent]
|
||||
})
|
||||
|
@ -321,11 +321,11 @@ export class LibraryDetailComponent implements OnInit {
|
|||
|
||||
this.seriesService.getSeriesForLibraryV2(undefined, undefined, this.filter)
|
||||
.subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.id}_${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
|
|
@ -119,9 +119,7 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
|
||||
@HostListener('window:click', ['$event'])
|
||||
handleDocumentClick(event: MouseEvent) {
|
||||
console.log('click: ', event)
|
||||
this.close();
|
||||
|
||||
}
|
||||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
|
|
|
@ -108,15 +108,18 @@
|
|||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #personTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToPerson(item.role, item.id)">
|
||||
<div class="ms-1">
|
||||
|
||||
<div [innerHTML]="item.name"></div>
|
||||
<div class="text-light fst-italic">{{item.role | personRole}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #personTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToPerson(item)">
|
||||
<div style="width: 24px" class="me-1">
|
||||
<app-image class="me-3 search-result"
|
||||
[styles]="{'background': 'none', 'max-height': '24px', 'height': '24px', 'width': '24px', 'border-radius': '50%'}"
|
||||
width="24px" [imageUrl]="imageService.getPersonImage(item.id)" [errorImage]="imageService.noPersonImage"></app-image>
|
||||
</div>
|
||||
<div class="ms-1">
|
||||
<div>{{item.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #genreTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToOther(FilterField.Genres, item.id)">
|
||||
|
|
|
@ -17,7 +17,7 @@ import {Chapter} from 'src/app/_models/chapter';
|
|||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import {Library} from 'src/app/_models/library/library';
|
||||
import {MangaFile} from 'src/app/_models/manga-file';
|
||||
import {PersonRole} from 'src/app/_models/metadata/person';
|
||||
import {Person, PersonRole} from 'src/app/_models/metadata/person';
|
||||
import {ReadingList} from 'src/app/_models/reading-list';
|
||||
import {SearchResult} from 'src/app/_models/search/search-result';
|
||||
import {SearchResultGroup} from 'src/app/_models/search/search-result-group';
|
||||
|
@ -178,56 +178,9 @@ export class NavHeaderComponent implements OnInit {
|
|||
this.goTo({field, comparison: FilterComparison.Equal, value: value + ''});
|
||||
}
|
||||
|
||||
goToPerson(role: PersonRole, filter: any) {
|
||||
goToPerson(person: Person) {
|
||||
this.clearSearch();
|
||||
filter = filter + '';
|
||||
switch(role) {
|
||||
case PersonRole.Other:
|
||||
break;
|
||||
case PersonRole.Writer:
|
||||
this.goTo({field: FilterField.Writers, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Artist:
|
||||
this.goTo({field: FilterField.CoverArtist, comparison: FilterComparison.Equal, value: filter}); // TODO: What is this supposed to be?
|
||||
break;
|
||||
case PersonRole.Character:
|
||||
this.goTo({field: FilterField.Characters, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
this.goTo({field: FilterField.Colorist, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Editor:
|
||||
this.goTo({field: FilterField.Editor, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Inker:
|
||||
this.goTo({field: FilterField.Inker, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.CoverArtist:
|
||||
this.goTo({field: FilterField.CoverArtist, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Letterer:
|
||||
this.goTo({field: FilterField.Letterer, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Penciller:
|
||||
this.goTo({field: FilterField.Penciller, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Publisher:
|
||||
this.goTo({field: FilterField.Publisher, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
this.goTo({field: FilterField.Imprint, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Team:
|
||||
this.goTo({field: FilterField.Team, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Location:
|
||||
this.goTo({field: FilterField.Location, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.goTo({field: FilterField.Translators, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
|
||||
}
|
||||
this.router.navigate(['person', person.name]);
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
<ng-container *transloco="let t; read: 'edit-person-modal'">
|
||||
@if (person !== undefined) {
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
{{t('title', {personName: this.person.name})}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<form [formGroup]="editForm">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{t(TabID.General)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
@if (editForm.get('name'); as formControl) {
|
||||
<app-setting-item [title]="t('name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<input id="name" class="form-control" formControlName="name" type="text" readonly
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
@if (formControl.errors; as errors) {
|
||||
@if (errors.required) {
|
||||
<div class="invalid-feedback">{{t('required-field')}}</div>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3 col-md-6 col-xs-12 pe-2">
|
||||
@if (editForm.get('malId'); as formControl) {
|
||||
<app-setting-item [title]="t('mal-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<input id="mal-id" class="form-control" formControlName="malId" type="number"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-md-6 col-xs-12">
|
||||
@if (editForm.get('aniListId'); as formControl) {
|
||||
<app-setting-item [title]="t('anilist-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<input id="anilist-id" class="form-control" formControlName="aniListId" type="number"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3 col-md-6 col-xs-12 pe-2">
|
||||
@if (editForm.get('hardcoverId'); as formControl) {
|
||||
<app-setting-item [title]="t('hardcover-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<input id="hardcover-id" class="form-control" formControlName="hardcoverId" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-md-6 col-xs-12">
|
||||
@if (editForm.get('asin'); as formControl) {
|
||||
<app-setting-item [title]="t('asin-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<input id="asin" class="form-control" formControlName="asin" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<app-setting-item [title]="t('description-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<div class="input-group">
|
||||
<textarea id="description" class="form-control" formControlName="description" rows="4"></textarea>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
{{t('cover-image-description')}}
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
(imageSelected)="updateSelectedIndex($event)"
|
||||
(selectedBase64Url)="updateSelectedImage($event)"
|
||||
[showReset]="person.coverImageLocked"
|
||||
(resetClicked)="handleReset()">
|
||||
</app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!editForm.valid" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
|
@ -0,0 +1,151 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {NgTemplateOutlet} from "@angular/common";
|
||||
import {PersonRolePipe} from "../../../_pipes/person-role.pipe";
|
||||
import {Person, PersonRole} from "../../../_models/metadata/person";
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem, NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavOutlet
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {PersonService} from "../../../_services/person.service";
|
||||
import { TranslocoDirective } from '@jsverse/transloco';
|
||||
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {UploadService} from "../../../_services/upload.service";
|
||||
import {CompactNumberPipe} from "../../../_pipes/compact-number.pipe";
|
||||
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {User} from "../../../_models/user";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-person-modal',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgTemplateOutlet,
|
||||
PersonRolePipe,
|
||||
NgbNav,
|
||||
NgbNavItem,
|
||||
TranslocoDirective,
|
||||
NgbNavLinkBase,
|
||||
NgbNavContent,
|
||||
NgbNavOutlet,
|
||||
CoverImageChooserComponent,
|
||||
CompactNumberPipe,
|
||||
SettingItemComponent,
|
||||
NgbNavLink
|
||||
],
|
||||
templateUrl: './edit-person-modal.component.html',
|
||||
styleUrl: './edit-person-modal.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditPersonModalComponent implements OnInit {
|
||||
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
private readonly modal = inject(NgbActiveModal);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly personService = inject(PersonService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
|
||||
@Input({required: true}) person!: Person;
|
||||
|
||||
active = TabID.General;
|
||||
editForm: FormGroup = new FormGroup({
|
||||
name: new FormControl('', [Validators.required]),
|
||||
description: new FormControl('', []),
|
||||
asin: new FormControl('', []),
|
||||
aniListId: new FormControl('', []),
|
||||
malId: new FormControl('', []),
|
||||
hardcoverId: new FormControl('', []),
|
||||
});
|
||||
|
||||
imageUrls: Array<string> = [];
|
||||
selectedCover: string = '';
|
||||
coverImageReset = false;
|
||||
touchedCoverImage = false;
|
||||
|
||||
ngOnInit() {
|
||||
if (this.person) {
|
||||
this.editForm.get('name')!.setValue(this.person.name);
|
||||
this.editForm.get('description')!.setValue(this.person.description);
|
||||
this.editForm.get('asin')!.setValue((this.person.asin || ''));
|
||||
this.editForm.get('aniListId')!.setValue((this.person.aniListId || '') + '') ;
|
||||
this.editForm.get('malId')!.setValue((this.person.malId || '') + '');
|
||||
this.editForm.get('hardcoverId')!.setValue(this.person.hardcoverId || '');
|
||||
|
||||
this.editForm.addControl('coverImageIndex', new FormControl(0, []));
|
||||
this.editForm.addControl('coverImageLocked', new FormControl(this.person.coverImageLocked, []));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
this.modal.close({success: false, coverImageUpdate: false});
|
||||
}
|
||||
|
||||
save() {
|
||||
const apis = [];
|
||||
|
||||
if (this.touchedCoverImage || this.coverImageReset) {
|
||||
apis.push(this.uploadService.updatePersonCoverImage(this.person.id, this.selectedCover, !this.coverImageReset));
|
||||
}
|
||||
|
||||
const person: Person = {
|
||||
id: this.person.id,
|
||||
coverImageLocked: this.person.coverImageLocked,
|
||||
name: this.editForm.get('name')!.value || '',
|
||||
description: this.editForm.get('description')!.value || '',
|
||||
asin: this.editForm.get('asin')!.value || '',
|
||||
// @ts-ignore
|
||||
aniListId: this.editForm.get('aniListId')!.value === '' ? null : parseInt(this.editForm.get('aniListId').value, 10),
|
||||
// @ts-ignore
|
||||
malId: this.editForm.get('malId')!.value === '' ? null : parseInt(this.editForm.get('malId').value, 10),
|
||||
hardcoverId: this.editForm.get('hardcoverId')!.value || '',
|
||||
};
|
||||
apis.push(this.personService.updatePerson(person));
|
||||
|
||||
forkJoin(apis).subscribe(_ => {
|
||||
this.modal.close({success: true, coverImageUpdate: false, person: person});
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.editForm.patchValue({
|
||||
coverImageIndex: index
|
||||
});
|
||||
this.touchedCoverImage = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.touchedCoverImage = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageReset = true;
|
||||
this.editForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.touchedCoverImage = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
95
UI/Web/src/app/person-detail/person-detail.component.html
Normal file
95
UI/Web/src/app/person-detail/person-detail.component.html
Normal file
|
@ -0,0 +1,95 @@
|
|||
<div class="main-container container-fluid">
|
||||
<ng-container *transloco="let t; read: 'person-detail'">
|
||||
@if (person$ | async; as person) {
|
||||
<div #companionBar>
|
||||
<app-side-nav-companion-bar>
|
||||
<ng-container title>
|
||||
<h2 class="title text-break">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="personActions" [labelBy]="person.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<span>{{person.name}}</span>
|
||||
</h2>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
</div>
|
||||
|
||||
<div class="main-container container-fluid pt-2 mb-5">
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mt-2">
|
||||
<app-image [styles]="{'background': 'none', 'max-height': '400px', 'height': '200px', 'width': '200px', 'border-radius': '50%'}"
|
||||
[imageUrl]="imageService.getPersonImage(person.id)"
|
||||
[errorImage]="imageService.noPersonImage"></app-image>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2">
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="person.description || defaultSummaryText"></app-read-more>
|
||||
|
||||
@if (roles$ | async; as roles) {
|
||||
<div class="mt-1">
|
||||
<h5>Roles in Libraries</h5>
|
||||
@for(role of roles; track role) {
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable" (click)="loadFilterByRole(role)">{{role | personRole}}</app-tag-badge>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (works$ | async; as works) {
|
||||
<div class="row mt-2">
|
||||
<app-carousel-reel [items]="works" [title]="t('known-for-title')" (sectionClick)="loadFilterByPerson()">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [entity]="item"
|
||||
[imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
||||
[title]="item.name"
|
||||
[suppressArchiveWarning]="true"
|
||||
(clicked)="navigateToSeries(item)">
|
||||
<ng-template #subtitle>
|
||||
Hello
|
||||
</ng-template>
|
||||
</app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<!-- Individual Works Carousel -->
|
||||
@if (roles$ | async; as roles) {
|
||||
@for(role of roles; track role) {
|
||||
<div class="row mt-2">
|
||||
<app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId"></app-chapter-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Collaborated In Carousel -->
|
||||
<div class="row mt-2">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Might be Interested In... -->
|
||||
<div class="row pt-3">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- External Series including Author -->
|
||||
@if (accountService.hasValidLicense$ | async) {
|
||||
<div class="row mt-2">
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
.main-container {
|
||||
margin-top: 10px;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
209
UI/Web/src/app/person-detail/person-detail.component.ts
Normal file
209
UI/Web/src/app/person-detail/person-detail.component.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
ElementRef,
|
||||
Inject,
|
||||
inject,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {PersonService} from "../_services/person.service";
|
||||
import {Observable, switchMap, tap} from "rxjs";
|
||||
import {Person, PersonRole} from "../_models/metadata/person";
|
||||
import {AsyncPipe, DOCUMENT, NgStyle} from "@angular/common";
|
||||
import {ImageComponent} from "../shared/image/image.component";
|
||||
import {ImageService} from "../_services/image.service";
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||
import {ReadMoreComponent} from "../shared/read-more/read-more.component";
|
||||
import {TagBadgeComponent, TagBadgeCursor} from "../shared/tag-badge/tag-badge.component";
|
||||
import {PersonRolePipe} from "../_pipes/person-role.pipe";
|
||||
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {SeriesCardComponent} from "../cards/series-card/series-card.component";
|
||||
import {FilterComparison} from "../_models/metadata/v2/filter-comparison";
|
||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {allPeople, personRoleForFilterField} from "../_models/metadata/v2/filter-field";
|
||||
import {Series} from "../_models/series";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {FilterCombination} from "../_models/metadata/v2/filter-combination";
|
||||
import {AccountService} from "../_services/account.service";
|
||||
import {CardItemComponent} from "../cards/card-item/card-item.component";
|
||||
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../_services/action-factory.service";
|
||||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {EditPersonModalComponent} from "./_modal/edit-person-modal/edit-person-modal.component";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ChapterCardComponent} from "../cards/chapter-card/chapter-card.component";
|
||||
import {ThemeService} from "../_services/theme.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-person-detail',
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ImageComponent,
|
||||
SideNavCompanionBarComponent,
|
||||
NgStyle,
|
||||
ReadMoreComponent,
|
||||
TagBadgeComponent,
|
||||
PersonRolePipe,
|
||||
CarouselReelComponent,
|
||||
SeriesCardComponent,
|
||||
CardItemComponent,
|
||||
CardActionablesComponent,
|
||||
TranslocoDirective,
|
||||
ChapterCardComponent
|
||||
],
|
||||
templateUrl: './person-detail.component.html',
|
||||
styleUrl: './person-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PersonDetailComponent {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly personService = inject(PersonService);
|
||||
private readonly actionService = inject(ActionFactoryService);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
|
||||
personName!: string;
|
||||
person$: Observable<Person> | null = null;
|
||||
person: Person | null = null;
|
||||
roles$: Observable<PersonRole[]> | null = null;
|
||||
roles: PersonRole[] | null = null;
|
||||
works$: Observable<Series[]> | null = null;
|
||||
defaultSummaryText = 'No information about this Person';
|
||||
filter: SeriesFilterV2 | null = null;
|
||||
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
||||
chaptersByRole: any = {};
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||
this.route.paramMap.subscribe(_ => {
|
||||
const personName = this.route.snapshot.paramMap.get('name');
|
||||
if (personName === null || undefined) {
|
||||
this.router.navigateByUrl('/home');
|
||||
return;
|
||||
}
|
||||
|
||||
this.personName = personName;
|
||||
|
||||
|
||||
this.person$ = this.personService.get(this.personName).pipe(tap(p => {
|
||||
this.person = p;
|
||||
|
||||
this.themeService.setColorScape(this.person.primaryColor || '', this.person.secondaryColor);
|
||||
|
||||
this.roles$ = this.personService.getRolesForPerson(this.personName).pipe(tap(roles => {
|
||||
this.roles = roles;
|
||||
this.filter = this.createFilter(roles);
|
||||
|
||||
for(let role of roles) {
|
||||
this.chaptersByRole[role] = this.personService.getChaptersByRole(this.person!.id, role).pipe(takeUntilDestroyed(this.destroyRef));
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}), takeUntilDestroyed(this.destroyRef));
|
||||
|
||||
|
||||
this.works$ = this.personService.getSeriesMostKnownFor(this.person.id).pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}), takeUntilDestroyed(this.destroyRef));
|
||||
});
|
||||
}
|
||||
|
||||
createFilter(roles: PersonRole[]) {
|
||||
const filter: SeriesFilterV2 = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.combination = FilterCombination.Or;
|
||||
filter.limitTo = 20;
|
||||
|
||||
// I might want to use roles$ to do all this
|
||||
allPeople.forEach(f => {
|
||||
filter.statements.push({comparison: FilterComparison.Contains, value: this.person!.id + '', field: f});
|
||||
});
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
loadFilterByPerson() {
|
||||
const loadPage = (person: Person) => {
|
||||
// Create a filter of all roles with OR
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('person-detail.browse-person-title', {name: person.name});
|
||||
|
||||
const searchFilter = {...this.filter!};
|
||||
searchFilter.limitTo = 0;
|
||||
|
||||
return this.filterUtilityService.applyFilterWithParams(['all-series'], searchFilter, params);
|
||||
};
|
||||
|
||||
|
||||
if (this.person) {
|
||||
loadPage(this.person).subscribe();
|
||||
} else {
|
||||
this.person$?.pipe(switchMap((p: Person) => {
|
||||
return loadPage(p);
|
||||
})).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
loadFilterByRole(role: PersonRole) {
|
||||
const personPipe = new PersonRolePipe();
|
||||
// Create a filter of all roles with OR
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('person-detail.browse-person-by-role-title', {name: this.person!.name, role: personPipe.transform(role)});
|
||||
|
||||
const searchFilter = this.filterUtilityService.createSeriesV2Filter();
|
||||
searchFilter.limitTo = 0;
|
||||
searchFilter.combination = FilterCombination.Or;
|
||||
|
||||
searchFilter.statements.push({comparison: FilterComparison.Contains, value: this.person!.id + '', field: personRoleForFilterField(role)});
|
||||
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], searchFilter, params).subscribe();
|
||||
}
|
||||
|
||||
navigateToSeries(series: Series) {
|
||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
handleAction(action: ActionItem<Person>, person: Person) {
|
||||
switch (action.action) {
|
||||
case(Action.Edit):
|
||||
const ref = this.modalService.open(EditPersonModalComponent, {scrollable: true, size: 'lg', fullscreen: 'md'});
|
||||
ref.componentInstance.person = this.person;
|
||||
|
||||
ref.closed.subscribe(r => {
|
||||
if (r.success) {
|
||||
this.person = {...r.person};
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.person);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -122,7 +122,7 @@
|
|||
[allowToggle]="false"
|
||||
(toggle)="switchTabsToDetail()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Writers, item.id)">{{item.name}}</a>
|
||||
<a routerLink="/person/{{item.name}}/" class="dark-exempt btn-icon">{{item.name}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@ import {UtilityService} from "./utility.service";
|
|||
import {UserCollection} from "../../_models/collection-tag";
|
||||
import {RecentlyAddedItem} from "../../_models/recently-added-item";
|
||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
import {BrowsePerson} from "../../_models/person/browse-person";
|
||||
|
||||
export const DEBOUNCE_TIME = 100;
|
||||
|
||||
|
@ -360,24 +361,28 @@ export class DownloadService {
|
|||
}
|
||||
}
|
||||
|
||||
mapToEntityType(events: DownloadEvent[], entity: Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter) {
|
||||
mapToEntityType(events: DownloadEvent[], entity: Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter | BrowsePerson) {
|
||||
if(this.utilityService.isSeries(entity)) {
|
||||
return events.find(e => e.entityType === 'series' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('series', (entity as Series))) || null;
|
||||
}
|
||||
|
||||
if(this.utilityService.isVolume(entity)) {
|
||||
return events.find(e => e.entityType === 'volume' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('volume', (entity as Volume))) || null;
|
||||
}
|
||||
|
||||
if(this.utilityService.isChapter(entity)) {
|
||||
return events.find(e => e.entityType === 'chapter' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('chapter', (entity as Chapter))) || null;
|
||||
}
|
||||
|
||||
// Is PageBookmark[]
|
||||
if(entity.hasOwnProperty('length')) {
|
||||
return events.find(e => e.entityType === 'bookmark'
|
||||
&& e.subTitle === this.downloadSubtitle('bookmark', [(entity as PageBookmark)])) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,8 @@ export class ImageComponent implements OnChanges {
|
|||
}
|
||||
|
||||
if (this.classes != '') {
|
||||
this.renderer.addClass(this.imgElem.nativeElement, this.classes);
|
||||
const classTokens = this.classes.split(' ');
|
||||
classTokens.forEach(cls => this.renderer.addClass(this.imgElem.nativeElement, cls));
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<div class="tagbadge cursor clickable" *ngIf="person !== undefined">
|
||||
@if (person !== undefined) {
|
||||
<div class="tagbadge cursor clickable">
|
||||
<div class="d-flex flex-column">
|
||||
<ng-container *ngIf="isStaff && staff.imageUrl && !staff.imageUrl.endsWith('default.jpg'); else localPerson">
|
||||
<app-image height="24px" width="24px" [styles]="{'object-fit': 'contain'}"[imageUrl]="staff.imageUrl"></app-image>
|
||||
</ng-container>
|
||||
<ng-template #localPerson>
|
||||
<i class="fa fa-user-circle align-self-center text-center mb-2" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
<div class="flex-grow-1 text-center">
|
||||
<span class="mt-0 mb-0">
|
||||
{{person.name}}
|
||||
</span>
|
||||
</div>
|
||||
@if (HasCoverImage) {
|
||||
<app-image height="24px" width="24px" [styles]="{'object-fit': 'contain'}"
|
||||
classes="align-self-center text-center mb-2"
|
||||
[imageUrl]="ImageUrl"
|
||||
[errorImage]="imageService.noPersonImage">
|
||||
</app-image>
|
||||
} @else {
|
||||
<i class="fas fa-user mx-auto" aria-hidden="true"></i>
|
||||
}
|
||||
|
||||
|
||||
<div class="flex-grow-1 text-center mt-2">
|
||||
@if (isStaff) {
|
||||
<span class="mt-1 mb-0">{{person.name}}</span>
|
||||
} @else {
|
||||
<a class="btn btn-icon p-0" routerLink="/person/{{person.name}}">{{person.name}}</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -3,24 +3,38 @@ import { Person } from '../../_models/metadata/person';
|
|||
import {CommonModule} from "@angular/common";
|
||||
import {SeriesStaff} from "../../_models/series-detail/external-series-detail";
|
||||
import {ImageComponent} from "../image/image.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {RouterLink} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-person-badge',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent],
|
||||
imports: [ImageComponent, RouterLink],
|
||||
templateUrl: './person-badge.component.html',
|
||||
styleUrls: ['./person-badge.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PersonBadgeComponent implements OnInit {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Input({required: true}) person!: Person | SeriesStaff;
|
||||
@Input() isStaff = false;
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
staff!: SeriesStaff;
|
||||
|
||||
get HasCoverImage() {
|
||||
return this.isStaff || (this.person as Person).coverImage;
|
||||
}
|
||||
|
||||
get ImageUrl() {
|
||||
if (this.isStaff && this.staff.imageUrl && !this.staff.imageUrl.endsWith('default.jpg')) {
|
||||
return (this.person as SeriesStaff).imageUrl || '';
|
||||
}
|
||||
return this.imageService.getPersonImage((this.person as Person).id);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.staff = this.person as SeriesStaff;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -56,6 +56,10 @@
|
|||
<app-side-nav-item icon="fa-star" [title]="t('want-to-read')" link="/want-to-read/"></app-side-nav-item>
|
||||
}
|
||||
|
||||
@case (SideNavStreamType.BrowseAuthors) {
|
||||
<app-side-nav-item icon="fa-star" [title]="t('browse-authors')" link="/browse/authors/"></app-side-nav-item>
|
||||
}
|
||||
|
||||
@case (SideNavStreamType.SmartFilter) {
|
||||
<app-side-nav-item icon="fa-bars-staggered" [title]="navStream.name" link="/all-series" [queryParams]="navStream.smartFilterEncoded"></app-side-nav-item>
|
||||
}
|
||||
|
|
|
@ -20,9 +20,4 @@ export class SidenavStreamListItemComponent {
|
|||
@Output() hide: EventEmitter<SideNavStream> = new EventEmitter<SideNavStream>();
|
||||
protected readonly SideNavStreamType = SideNavStreamType;
|
||||
protected readonly baseUrl = inject(APP_BASE_HREF);
|
||||
|
||||
constructor() {
|
||||
console.log('baseUrl', this.baseUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
BIN
UI/Web/src/assets/images/error-person-missing.dark.min.png
Normal file
BIN
UI/Web/src/assets/images/error-person-missing.dark.min.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 637 B |
BIN
UI/Web/src/assets/images/error-person-missing.dark.png
Normal file
BIN
UI/Web/src/assets/images/error-person-missing.dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
UI/Web/src/assets/images/error-person-missing.min.png
Normal file
BIN
UI/Web/src/assets/images/error-person-missing.min.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 578 B |
BIN
UI/Web/src/assets/images/error-person-missing.png
Normal file
BIN
UI/Web/src/assets/images/error-person-missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -901,6 +901,7 @@
|
|||
"collections": "Collections",
|
||||
"reading-lists": "Reading Lists",
|
||||
"bookmarks": "Bookmarks",
|
||||
"browse-authors": "Browse Authors",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"all-series": "All Series",
|
||||
"clear": "{{common.clear}}",
|
||||
|
@ -911,6 +912,19 @@
|
|||
"customize": "{{settings.customize}}"
|
||||
},
|
||||
|
||||
"browse-authors": {
|
||||
"title": "Browse Authors & Writers",
|
||||
"author-count": "{{num}} People",
|
||||
"cover-image-description": "{{edit-series-modal.cover-image-description}}"
|
||||
},
|
||||
|
||||
"person-detail": {
|
||||
"known-for-title": "Known For",
|
||||
"individual-role-title": "As a {{role}}",
|
||||
"browse-person-title": "All Works of {{name}}",
|
||||
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}"
|
||||
},
|
||||
|
||||
"library-settings-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"edit-title": "Edit {{name}}",
|
||||
|
@ -1985,6 +1999,24 @@
|
|||
"cover-image-description": "{{edit-series-modal.cover-image-description}}"
|
||||
},
|
||||
|
||||
"edit-person-modal": {
|
||||
"title": "{{personName}} Details",
|
||||
"general-tab": "{{edit-series-modal.general-tab}}",
|
||||
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
||||
"loading": "{{common.loading}}",
|
||||
"close": "{{common.close}}",
|
||||
"name-label": "{{edit-series-modal.name-label}}",
|
||||
"role-label": "Role",
|
||||
"mal-id-label": "MAL Id",
|
||||
"anilist-id-label": "AniList Id",
|
||||
"hardcover-id-label": "Hardcover Id",
|
||||
"asin-label": "ASIN",
|
||||
"description-label": "Description",
|
||||
"required-field": "{{validations.required-field}}",
|
||||
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
|
||||
"save": "{{common.save}}"
|
||||
},
|
||||
|
||||
"day-breakdown": {
|
||||
"title": "Day Breakdown",
|
||||
"no-data": "No progress, get to reading",
|
||||
|
@ -2197,7 +2229,8 @@
|
|||
"collections": "{{side-nav.collections}}",
|
||||
"reading-lists": "{{side-nav.reading-lists}}",
|
||||
"bookmarks": "{{side-nav.bookmarks}}",
|
||||
"all-series": "{{side-nav.all-series}}"
|
||||
"all-series": "{{side-nav.all-series}}",
|
||||
"browse-authors": "{{side-nav.browse-authors}}"
|
||||
},
|
||||
|
||||
"filter-field-pipe": {
|
||||
|
@ -2205,7 +2238,7 @@
|
|||
"characters": "{{metadata-fields.characters-title}}",
|
||||
"collection-tags": "Collection Tags",
|
||||
"colorist": "Colorist",
|
||||
"cover-artist": "{{person-role-pipe.cover-artist}}",
|
||||
"cover-artist": "{{person-role-pipe.artist}}",
|
||||
"editor": "Editor",
|
||||
"formats": "Formats",
|
||||
"genres": "{{metadata-fields.genres-title}}",
|
||||
|
@ -2555,6 +2588,7 @@
|
|||
"select-all": "Select All",
|
||||
"deselect-all": "Deselect All",
|
||||
"series-count": "{{num}} Series",
|
||||
"author-count": "{{num}} Authors",
|
||||
"item-count": "{{num}} Items",
|
||||
|
||||
"book-num": "Book",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue