New Scanner + People Pages (#3286)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2024-10-23 15:11:18 -07:00 committed by GitHub
parent 1ed0eae22d
commit ba20ad4ecc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 17529 additions and 3038 deletions

View file

@ -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;

View file

@ -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

View 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>

View 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;
}
}

View 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();
}
}