UX Overhaul Part 2 (#3112)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2024-08-16 19:37:12 -05:00 committed by GitHub
parent 0247bc5012
commit 3d8aa2ad24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
192 changed files with 14808 additions and 1874 deletions

View file

@ -1,15 +1,15 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter, inject,
Component, DestroyRef,
EventEmitter, HostListener, inject,
Input,
OnChanges,
OnInit,
Output
} from '@angular/core';
import {Router} from '@angular/router';
import {NgbModal, NgbOffcanvas} from '@ng-bootstrap/ng-bootstrap';
import {Router, RouterLink} from '@angular/router';
import {NgbModal, NgbOffcanvas, NgbProgressbar, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {Series} from 'src/app/_models/series';
import {ImageService} from 'src/app/_services/image.service';
@ -22,8 +22,23 @@ import {CommonModule} from "@angular/common";
import {CardItemComponent} from "../card-item/card-item.component";
import {RelationshipPipe} from "../../_pipes/relationship.pipe";
import {Device} from "../../_models/device/device";
import {translate, TranslocoService} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {DownloadIndicatorComponent} from "../download-indicator/download-indicator.component";
import {EntityTitleComponent} from "../entity-title/entity-title.component";
import {FormsModule} from "@angular/forms";
import {ImageComponent} from "../../shared/image/image.component";
import {DownloadEvent, DownloadService} from "../../shared/_services/download.service";
import {Observable} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {map} from "rxjs/operators";
import {AccountService} from "../../_services/account.service";
import {BulkSelectionService} from "../bulk-selection.service";
import {User} from "../../_models/user";
import {ScrollService} from "../../_services/scroll.service";
import {ReaderService} from "../../_services/reader.service";
function deepClone(obj: any): any {
if (obj === null || typeof obj !== 'object') {
@ -52,13 +67,30 @@ function deepClone(obj: any): any {
@Component({
selector: 'app-series-card',
standalone: true,
imports: [CommonModule, CardItemComponent, RelationshipPipe],
imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective],
templateUrl: './series-card.component.html',
styleUrls: ['./series-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesCardComponent implements OnInit, OnChanges {
@Input({required: true}) data!: Series;
private readonly offcanvasService = inject(NgbOffcanvas);
private readonly router = inject(Router);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly seriesService = inject(SeriesService);
private readonly toastr = inject(ToastrService);
private readonly modalService = inject(NgbModal);
protected readonly imageService = inject(ImageService);
private readonly actionFactoryService = inject(ActionFactoryService);
private readonly actionService = inject(ActionService);
private readonly accountService = inject(AccountService);
private readonly destroyRef = inject(DestroyRef);
protected readonly bulkSelectionService = inject(BulkSelectionService);
private readonly downloadService = inject(DownloadService);
private readonly scrollService = inject(ScrollService);
private readonly readerService = inject(ReaderService);
@Input({required: true}) series!: Series;
@Input() libraryId = 0;
@Input() suppressLibraryLink = false;
/**
@ -93,27 +125,75 @@ export class SeriesCardComponent implements OnInit, OnChanges {
*/
@Output() selection = new EventEmitter<boolean>();
count: number = 0;
actions: ActionItem<Series>[] = [];
imageUrl: string = '';
/**
* This is the download we get from download service.
*/
download$: Observable<DownloadEvent | null> | null = null;
/**
* 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;
private user: User | undefined;
private readonly offcanvasService = inject(NgbOffcanvas);
constructor(private router: Router, private cdRef: ChangeDetectorRef,
private seriesService: SeriesService, private toastr: ToastrService,
private modalService: NgbModal, private imageService: ImageService,
private actionFactoryService: ActionFactoryService,
private actionService: ActionService) {}
@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;
}
ngOnInit(): void {
if (this.data) {
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id);
if (this.series) {
this.imageUrl = this.imageService.getSeriesCoverImage(this.series.id);
this.cdRef.markForCheck();
}
}
ngOnChanges(changes: any) {
if (this.data) {
if (this.series) {
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
this.user = user;
});
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
return this.downloadService.mapToEntityType(events, this.series);
}));
this.actions = [...this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series))];
if (this.isOnDeck) {
const othersIndex = this.actions.findIndex(obj => obj.title === 'others');
@ -194,7 +274,7 @@ export class SeriesCardComponent implements OnInit, OnChanges {
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
if (closeResult.success) {
this.seriesService.getSeries(data.id).subscribe(series => {
this.data = series;
this.series = series;
this.cdRef.markForCheck();
this.reload.emit(series.id);
this.dataChanged.emit(series);
@ -223,8 +303,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
markAsUnread(series: Series) {
this.actionService.markSeriesAsUnread(series, () => {
if (this.data) {
this.data.pagesRead = 0;
if (this.series) {
this.series.pagesRead = 0;
this.cdRef.markForCheck();
}
@ -234,8 +314,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
markAsRead(series: Series) {
this.actionService.markSeriesAsRead(series, () => {
if (this.data) {
this.data.pagesRead = series.pages;
if (this.series) {
this.series.pagesRead = series.pages;
this.cdRef.markForCheck();
}
this.dataChanged.emit(series);
@ -244,15 +324,34 @@ export class SeriesCardComponent implements OnInit, OnChanges {
handleClick() {
if (this.previewOnClick) {
const ref = this.offcanvasService.open(SeriesPreviewDrawerComponent, {position: 'end', panelClass: 'navbar-offset'});
const ref = this.offcanvasService.open(SeriesPreviewDrawerComponent, {position: 'end', panelClass: ''});
ref.componentInstance.isExternalSeries = false;
ref.componentInstance.seriesId = this.data.id;
ref.componentInstance.libraryId = this.data.libraryId;
ref.componentInstance.name = this.data.name;
ref.componentInstance.seriesId = this.series.id;
ref.componentInstance.libraryId = this.series.libraryId;
ref.componentInstance.name = this.series.name;
return;
}
this.clicked.emit(this.data);
this.router.navigate(['library', this.libraryId, 'series', this.data?.id]);
this.clicked.emit(this.series);
this.router.navigate(['library', this.libraryId, 'series', this.series?.id]);
}
handleSelection(event?: any) {
if (event) {
event.stopPropagation();
}
this.selection.emit(this.selected);
this.cdRef.detectChanges();
}
read(event: any) {
event.stopPropagation();
if (this.bulkSelectionService.hasSelections()) return;
// Get Continue Reading point and open directly
this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => {
this.readerService.readChapter(this.libraryId, this.series.id, chapter, false);
});
}
}