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
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue