Fixed a bug with double clicking manga reader on mobile no longer triggering bookmarking
This commit is contained in:
parent
2a43deea24
commit
0a7bc4b3f6
16 changed files with 250 additions and 203 deletions
|
@ -0,0 +1 @@
|
|||
<app-image [imageUrl]="imageUrl" height="32px" width="32px" classes="clickable" ngbTooltip="{{rating | ageRating}}" (click)="openRating()"></app-image>
|
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import {AgeRating} from "../../_models/metadata/age-rating";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {AgeRatingPipe} from "../../_pipes/age-rating.pipe";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
|
||||
const basePath = './assets/images/ratings/';
|
||||
|
||||
@Component({
|
||||
selector: 'app-age-rating-image',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ImageComponent,
|
||||
NgbTooltip,
|
||||
AgeRatingPipe,
|
||||
AsyncPipe
|
||||
],
|
||||
templateUrl: './age-rating-image.component.html',
|
||||
styleUrl: './age-rating-image.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AgeRatingImageComponent implements OnInit, OnChanges {
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
|
||||
protected readonly AgeRating = AgeRating;
|
||||
|
||||
@Input({required: true}) rating: AgeRating = AgeRating.Unknown;
|
||||
|
||||
imageUrl: string = 'unknown-rating.png';
|
||||
|
||||
ngOnInit() {
|
||||
this.setImage();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.setImage();
|
||||
}
|
||||
|
||||
setImage() {
|
||||
switch (this.rating) {
|
||||
case AgeRating.Unknown:
|
||||
this.imageUrl = basePath + 'unknown-rating.png';
|
||||
break;
|
||||
case AgeRating.RatingPending:
|
||||
this.imageUrl = basePath + 'rating-pending-rating.png';
|
||||
break;
|
||||
case AgeRating.EarlyChildhood:
|
||||
this.imageUrl = basePath + 'early-childhood-rating.png';
|
||||
break;
|
||||
case AgeRating.Everyone:
|
||||
this.imageUrl = basePath + 'everyone-rating.png';
|
||||
break;
|
||||
case AgeRating.G:
|
||||
this.imageUrl = basePath + 'g-rating.png';
|
||||
break;
|
||||
case AgeRating.Everyone10Plus:
|
||||
this.imageUrl = basePath + 'everyone-10+-rating.png';
|
||||
break;
|
||||
case AgeRating.PG:
|
||||
this.imageUrl = basePath + 'pg-rating.png';
|
||||
break;
|
||||
case AgeRating.KidsToAdults:
|
||||
this.imageUrl = basePath + 'kids-to-adults-rating.png';
|
||||
break;
|
||||
case AgeRating.Teen:
|
||||
this.imageUrl = basePath + 'teen-rating.png';
|
||||
break;
|
||||
case AgeRating.Mature15Plus:
|
||||
this.imageUrl = basePath + 'ma15+-rating.png';
|
||||
break;
|
||||
case AgeRating.Mature17Plus:
|
||||
this.imageUrl = basePath + 'mature-17+-rating.png';
|
||||
break;
|
||||
case AgeRating.Mature:
|
||||
this.imageUrl = basePath + 'm-rating.png';
|
||||
break;
|
||||
case AgeRating.R18Plus:
|
||||
this.imageUrl = basePath + 'r18+-rating.png';
|
||||
break;
|
||||
case AgeRating.AdultsOnly:
|
||||
this.imageUrl = basePath + 'adults-only-18+-rating.png';
|
||||
break;
|
||||
case AgeRating.X18Plus:
|
||||
this.imageUrl = basePath + 'x18+-rating.png';
|
||||
break;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
openRating() {
|
||||
this.filterUtilityService.applyFilter(['all-series'], FilterField.AgeRating, FilterComparison.Equal, `${this.rating}`).subscribe();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
@if (publishers.length > 0) {
|
||||
<div class="publisher-wrapper">
|
||||
<div class="publisher-flipper" [class.is-flipped]="isFlipped">
|
||||
<div class="publisher-side publisher-front">
|
||||
<div class="publisher-img-container d-inline-flex align-items-center me-2 position-relative">
|
||||
<app-image
|
||||
[imageUrl]="imageService.getPublisherImage(currentPublisher!.name)"
|
||||
[classes]="'me-2'"
|
||||
[hideOnError]="true"
|
||||
width="32px"
|
||||
height="32px"
|
||||
aria-hidden="true">
|
||||
</app-image>
|
||||
<div class="position-relative d-inline-block"
|
||||
(click)="openPublisher(currentPublisher!.id)">
|
||||
{{currentPublisher!.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="publisher-side publisher-back">
|
||||
<div class="publisher-img-container d-inline-flex align-items-center me-2 position-relative">
|
||||
<app-image
|
||||
[imageUrl]="imageService.getPublisherImage(nextPublisher!.name)"
|
||||
[classes]="'me-2'"
|
||||
[hideOnError]="true"
|
||||
width="32px"
|
||||
height="32px"
|
||||
aria-hidden="true">
|
||||
</app-image>
|
||||
<div class="position-relative d-inline-block"
|
||||
(click)="openPublisher(nextPublisher!.id)">
|
||||
{{nextPublisher!.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
//.publisher-img-container {
|
||||
// background-color: var(--card-bg-color);
|
||||
// border-radius: 3px;
|
||||
// padding: 2px 5px;
|
||||
// font-size: 0.8rem;
|
||||
// vertical-align: middle;
|
||||
//
|
||||
// div {
|
||||
// min-height: 32px;
|
||||
// line-height: 32px;
|
||||
// }
|
||||
//}
|
||||
|
||||
// Animation code
|
||||
|
||||
.publisher-wrapper {
|
||||
perspective: 1000px;
|
||||
height: 32px;
|
||||
|
||||
background-color: var(--card-bg-color);
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
font-size: 0.8rem;
|
||||
vertical-align: middle;
|
||||
|
||||
div {
|
||||
min-height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.publisher-flipper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
transition: transform 0.6s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.publisher-flipper.is-flipped {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.publisher-side {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.publisher-front {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.publisher-back {
|
||||
transform: rotateX(180deg);
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
AfterViewChecked,
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {Person} from "../../_models/metadata/person";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
const ANIMATION_TIME = 3000;
|
||||
|
||||
@Component({
|
||||
selector: 'app-publisher-flipper',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ImageComponent
|
||||
],
|
||||
templateUrl: './publisher-flipper.component.html',
|
||||
styleUrl: './publisher-flipper.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PublisherFlipperComponent implements OnInit, OnDestroy, AfterViewInit, AfterViewChecked {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
@Input() publishers: Array<Person> = [];
|
||||
|
||||
|
||||
currentPublisher: Person | undefined = undefined;
|
||||
nextPublisher: Person | undefined = undefined;
|
||||
|
||||
currentIndex = 0;
|
||||
isFlipped = false;
|
||||
private intervalId: any;
|
||||
|
||||
ngOnInit() {
|
||||
if (this.publishers.length > 0) {
|
||||
this.currentPublisher = this.publishers[0];
|
||||
this.nextPublisher = this.publishers[1] || this.publishers[0];
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.publishers.length > 1) {
|
||||
this.startFlipping(); // Start flipping cycle once the view is initialized
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
// This lifecycle hook will be called after Angular performs change detection in each cycle
|
||||
if (this.isFlipped) {
|
||||
// Only update publishers after the flip is complete
|
||||
this.currentIndex = (this.currentIndex + 1) % this.publishers.length;
|
||||
this.currentPublisher = this.publishers[this.currentIndex];
|
||||
this.nextPublisher = this.publishers[(this.currentIndex + 1) % this.publishers.length];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
}
|
||||
|
||||
private startFlipping() {
|
||||
this.intervalId = setInterval(() => {
|
||||
// Toggle flip state, initiating the flip animation
|
||||
this.isFlipped = !this.isFlipped;
|
||||
this.cdRef.detectChanges(); // Explicitly detect changes to trigger re-render
|
||||
}, ANIMATION_TIME);
|
||||
}
|
||||
|
||||
openPublisher(filter: string | number) {
|
||||
// TODO: once we build out publisher person-detail page, we can redirect there
|
||||
this.filterUtilityService.applyFilter(['all-series'], FilterField.Publisher, FilterComparison.Equal, `${filter}`).subscribe();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<ng-container *transloco="let t; read: 'related-tab'">
|
||||
<div style="padding-bottom: 1rem;">
|
||||
@if (relations.length > 0) {
|
||||
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card class="col-auto mt-2 mb-2" [series]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
|
||||
@if (collections.length > 0) {
|
||||
<app-carousel-reel [items]="collections" [title]="t('collections-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [title]="item.title" [entity]="item"
|
||||
[count]="item.itemCount"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
||||
(clicked)="openCollection(item)" [linkUrl]="'/collections/' + item.id" [showFormat]="false"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
|
||||
|
||||
@if (readingLists.length > 0) {
|
||||
<app-carousel-reel [items]="readingLists" [title]="t('reading-lists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [title]="item.title" [entity]="item"
|
||||
[count]="item.itemCount"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
||||
(clicked)="openReadingList(item)" [linkUrl]="'/lists/' + item.id" [showFormat]="false"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,48 @@
|
|||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
||||
import {ReadingList} from "../../_models/reading-list";
|
||||
import {CardItemComponent} from "../../cards/card-item/card-item.component";
|
||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {UserCollection} from "../../_models/collection-tag";
|
||||
import {Router} from "@angular/router";
|
||||
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
|
||||
import {Series} from "../../_models/series";
|
||||
import {RelationKind} from "../../_models/series-detail/relation-kind";
|
||||
|
||||
export interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
relation: RelationKind;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-related-tab',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CardItemComponent,
|
||||
CarouselReelComponent,
|
||||
TranslocoDirective,
|
||||
SeriesCardComponent
|
||||
],
|
||||
templateUrl: './related-tab.component.html',
|
||||
styleUrl: './related-tab.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RelatedTabComponent {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly router = inject(Router);
|
||||
|
||||
@Input() readingLists: Array<ReadingList> = [];
|
||||
@Input() collections: Array<UserCollection> = [];
|
||||
@Input() relations: Array<RelatedSeriesPair> = [];
|
||||
|
||||
openReadingList(readingList: ReadingList) {
|
||||
this.router.navigate(['lists', readingList.id]);
|
||||
}
|
||||
|
||||
openCollection(collection: UserCollection) {
|
||||
this.router.navigate(['collections', collection.id]);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue