Extract review tab & add to chapter
This commit is contained in:
parent
a152f16cf6
commit
a3e04f3bc1
13 changed files with 220 additions and 124 deletions
|
|
@ -15,8 +15,8 @@ import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.c
|
|||
import {AccountService} from "../../_services/account.service";
|
||||
import {
|
||||
ReviewSeriesModalCloseEvent,
|
||||
ReviewSeriesModalComponent
|
||||
} from "../review-series-modal/review-series-modal.component";
|
||||
ReviewModalComponent
|
||||
} from "../review-modal/review-modal.component";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
||||
|
|
@ -53,7 +53,7 @@ export class ReviewCardComponent implements OnInit {
|
|||
showModal() {
|
||||
let component;
|
||||
if (this.isMyReview) {
|
||||
component = ReviewSeriesModalComponent;
|
||||
component = ReviewModalComponent;
|
||||
} else {
|
||||
component = ReviewCardModalComponent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
|||
export interface UserReview {
|
||||
seriesId: number;
|
||||
libraryId: number;
|
||||
volumeId?: number;
|
||||
chapterId?: number;
|
||||
score: number;
|
||||
username: string;
|
||||
body: string;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {UserReview} from "../review-card/user-review";
|
|||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ChapterService} from "../../_services/chapter.service";
|
||||
|
||||
export enum ReviewSeriesModalCloseAction {
|
||||
Create,
|
||||
|
|
@ -22,20 +23,22 @@ export interface ReviewSeriesModalCloseEvent {
|
|||
@Component({
|
||||
selector: 'app-review-series-modal',
|
||||
imports: [ReactiveFormsModule, TranslocoDirective],
|
||||
templateUrl: './review-series-modal.component.html',
|
||||
styleUrls: ['./review-series-modal.component.scss'],
|
||||
templateUrl: './review-modal.component.html',
|
||||
styleUrls: ['./review-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReviewSeriesModalComponent implements OnInit {
|
||||
export class ReviewModalComponent implements OnInit {
|
||||
|
||||
protected readonly modal = inject(NgbActiveModal);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly chapterService = inject(ChapterService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly minLength = 5;
|
||||
|
||||
@Input({required: true}) review!: UserReview;
|
||||
@Input() reviewLocation: 'series' | 'chapter' = 'series';
|
||||
reviewGroup!: FormGroup;
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -51,18 +54,26 @@ export class ReviewSeriesModalComponent implements OnInit {
|
|||
|
||||
async delete() {
|
||||
if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return;
|
||||
this.seriesService.deleteReview(this.review.seriesId).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.review-deleted'));
|
||||
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
|
||||
});
|
||||
|
||||
if (this.reviewLocation === 'series') {
|
||||
this.seriesService.deleteReview(this.review.seriesId).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.review-deleted'));
|
||||
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
save() {
|
||||
const model = this.reviewGroup.value;
|
||||
if (model.reviewBody.length < this.minLength) {
|
||||
return;
|
||||
}
|
||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
|
||||
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
|
||||
});
|
||||
|
||||
if (this.reviewLocation === 'series') {
|
||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
|
||||
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
17
UI/Web/src/app/_single-module/reviews/reviews.component.html
Normal file
17
UI/Web/src/app/_single-module/reviews/reviews.component.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<div class="mb-3" *transloco="let t;prefix:'reviews'">
|
||||
<app-carousel-reel [items]="userReviews" [alwaysShow]="true" [title]="t('user-reviews-local')"
|
||||
iconClasses="fa-solid fa-{{getUserReviews().length > 0 ? 'pen' : 'plus'}}"
|
||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" *transloco="let t;prefix:'reviews'">
|
||||
<app-carousel-reel [items]="plusReviews" [alwaysShow]="false" [title]="t('user-reviews-plus')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
106
UI/Web/src/app/_single-module/reviews/reviews.component.ts
Normal file
106
UI/Web/src/app/_single-module/reviews/reviews.component.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnInit} from '@angular/core';
|
||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {ReviewCardComponent} from "../review-card/review-card.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {UserReview} from "../review-card/user-review";
|
||||
import {User} from "../../_models/user";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {
|
||||
ReviewModalComponent, ReviewSeriesModalCloseAction,
|
||||
ReviewSeriesModalCloseEvent
|
||||
} from "../review-modal/review-modal.component";
|
||||
import {DefaultModalOptions} from "../../_models/default-modal-options";
|
||||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {Series} from "../../_models/series";
|
||||
import {Volume} from "../../_models/volume";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reviews',
|
||||
imports: [
|
||||
CarouselReelComponent,
|
||||
ReviewCardComponent,
|
||||
TranslocoDirective
|
||||
],
|
||||
templateUrl: './reviews.component.html',
|
||||
styleUrl: './reviews.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReviewsComponent {
|
||||
|
||||
@Input({required: true}) userReviews!: Array<UserReview>;
|
||||
@Input({required: true}) plusReviews!: Array<UserReview>;
|
||||
@Input({required: true}) series!: Series;
|
||||
@Input() volumeId: number | undefined;
|
||||
@Input() chapter: Chapter | undefined;
|
||||
|
||||
@Input() reviewLocation: 'series' | 'chapter' = 'series';
|
||||
|
||||
user: User | undefined;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private modalService: NgbModal,
|
||||
private cdRef: ChangeDetectorRef) {
|
||||
|
||||
this.accountService.currentUser$.subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openReviewModal() {
|
||||
const userReview = this.getUserReviews();
|
||||
|
||||
const modalRef = this.modalService.open(ReviewModalComponent, DefaultModalOptions);
|
||||
modalRef.componentInstance.reviewLocation = this.reviewLocation;
|
||||
|
||||
if (userReview.length > 0) {
|
||||
modalRef.componentInstance.review = userReview[0];
|
||||
} else {
|
||||
modalRef.componentInstance.review = {
|
||||
seriesId: this.series.id,
|
||||
volumeId: this.volumeId,
|
||||
chapterId: this.chapter?.id,
|
||||
tagline: '',
|
||||
body: ''
|
||||
};
|
||||
}
|
||||
|
||||
modalRef.closed.subscribe((closeResult) => {
|
||||
this.updateOrDeleteReview(closeResult);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateOrDeleteReview(closeResult: ReviewSeriesModalCloseEvent) {
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Close) return;
|
||||
|
||||
const index = this.userReviews.findIndex(r => r.username === closeResult.review!.username);
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Edit) {
|
||||
if (index === -1 ) {
|
||||
this.userReviews = [closeResult.review, ...this.userReviews];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
this.userReviews[index] = closeResult.review;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Delete) {
|
||||
this.userReviews = [...this.userReviews.filter(r => r.username !== closeResult.review!.username)];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getUserReviews() {
|
||||
if (!this.user) {
|
||||
return [];
|
||||
}
|
||||
return this.userReviews.filter(r => r.username === this.user?.username && !r.isExternal);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -175,6 +175,20 @@
|
|||
</li>
|
||||
}
|
||||
|
||||
<li [ngbNavItem]="TabID.Reviews">
|
||||
<a ngbNavLink>
|
||||
{{t(TabID.Reviews)}}
|
||||
<span class="badge rounded-pill text-bg-secondary">{{userReviews.length + plusReviews.length}}</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
@defer (when activeTabId === TabID.Reviews; prefetch on idle) {
|
||||
<app-reviews [userReviews]="userReviews" [plusReviews]="plusReviews"
|
||||
[series]="series" [chapter]="chapter" [volumeId]="chapter.volumeId"
|
||||
reviewLocation="chapter" />
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
@if(readingLists.length > 0) {
|
||||
<li [ngbNavItem]="TabID.Related">
|
||||
<a ngbNavLink>{{t('related-tab')}}</a>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ import {ActionService} from "../_services/action.service";
|
|||
import {DefaultDatePipe} from "../_pipes/default-date.pipe";
|
||||
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
||||
import {DefaultModalOptions} from "../_models/default-modal-options";
|
||||
import {UserReview} from "../_single-module/review-card/user-review";
|
||||
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {ReviewCardComponent} from "../_single-module/review-card/review-card.component";
|
||||
import {User} from "../_models/user";
|
||||
import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.component";
|
||||
import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
|
||||
|
||||
enum TabID {
|
||||
Related = 'related-tab',
|
||||
|
|
@ -74,36 +80,39 @@ enum TabID {
|
|||
|
||||
@Component({
|
||||
selector: 'app-chapter-detail',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
LoadingComponent,
|
||||
NgbDropdown,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdownToggle,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
NgbTooltip,
|
||||
VirtualScrollerModule,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
TranslocoDirective,
|
||||
ReadMoreComponent,
|
||||
NgbNavItem,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
RouterLink,
|
||||
EntityTitleComponent,
|
||||
RelatedTabComponent,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
CoverImageComponent
|
||||
],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
LoadingComponent,
|
||||
NgbDropdown,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdownToggle,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
NgbTooltip,
|
||||
VirtualScrollerModule,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
TranslocoDirective,
|
||||
ReadMoreComponent,
|
||||
NgbNavItem,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
RouterLink,
|
||||
EntityTitleComponent,
|
||||
RelatedTabComponent,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
CoverImageComponent,
|
||||
CarouselReelComponent,
|
||||
ReviewCardComponent,
|
||||
ReviewsComponent
|
||||
],
|
||||
templateUrl: './chapter-detail.component.html',
|
||||
styleUrl: './chapter-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -151,6 +160,8 @@ export class ChapterDetailComponent implements OnInit {
|
|||
series: Series | null = null;
|
||||
libraryType: LibraryType | null = null;
|
||||
hasReadingProgress = false;
|
||||
userReviews: Array<UserReview> = [];
|
||||
plusReviews: Array<UserReview> = [];
|
||||
weblinks: Array<string> = [];
|
||||
activeTabId = TabID.Details;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -315,25 +315,8 @@
|
|||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
@defer (when activeTabId === TabID.Reviews; prefetch on idle) {
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-local')"
|
||||
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
|
||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="plusReviews" [alwaysShow]="false" [title]="t('user-reviews-plus')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
<app-reviews [userReviews]="reviews" [plusReviews]="plusReviews" [series]="series" />
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ import {SeriesService} from 'src/app/_services/series.service';
|
|||
import {
|
||||
ReviewSeriesModalCloseAction,
|
||||
ReviewSeriesModalCloseEvent,
|
||||
ReviewSeriesModalComponent
|
||||
} from '../../../_single-module/review-series-modal/review-series-modal.component';
|
||||
ReviewModalComponent
|
||||
} from '../../../_single-module/review-modal/review-modal.component';
|
||||
import {PageLayoutMode} from 'src/app/_models/page-layout-mode';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {UserReview} from "../../../_single-module/review-card/user-review";
|
||||
|
|
@ -116,6 +116,7 @@ import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
|||
import {LicenseService} from "../../../_services/license.service";
|
||||
import {PageBookmark} from "../../../_models/readers/page-bookmark";
|
||||
import {VolumeRemovedEvent} from "../../../_models/events/volume-removed-event";
|
||||
import {ReviewsComponent} from "../../../_single-module/reviews/reviews.component";
|
||||
|
||||
|
||||
enum TabID {
|
||||
|
|
@ -140,14 +141,14 @@ interface StoryLineItem {
|
|||
templateUrl: './series-detail.component.html',
|
||||
styleUrls: ['./series-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CardActionablesComponent, ReactiveFormsModule, NgStyle,
|
||||
NgbTooltip, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
|
||||
NgbDropdownItem, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
|
||||
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, SeriesCardComponent, ExternalSeriesCardComponent, NgbNavOutlet,
|
||||
TranslocoDirective, NgTemplateOutlet, NextExpectedCardComponent,
|
||||
NgClass, AsyncPipe, DetailsTabComponent, ChapterCardComponent,
|
||||
VolumeCardComponent, DefaultValuePipe, ExternalRatingComponent, ReadMoreComponent, RouterLink, BadgeExpanderComponent,
|
||||
PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent, RelatedTabComponent, CoverImageComponent]
|
||||
imports: [CardActionablesComponent, ReactiveFormsModule, NgStyle,
|
||||
NgbTooltip, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
|
||||
NgbDropdownItem, BulkOperationsComponent,
|
||||
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, SeriesCardComponent, ExternalSeriesCardComponent, NgbNavOutlet,
|
||||
TranslocoDirective, NgTemplateOutlet, NextExpectedCardComponent,
|
||||
NgClass, AsyncPipe, DetailsTabComponent, ChapterCardComponent,
|
||||
VolumeCardComponent, DefaultValuePipe, ExternalRatingComponent, ReadMoreComponent, RouterLink, BadgeExpanderComponent,
|
||||
PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent, RelatedTabComponent, CoverImageComponent, ReviewsComponent]
|
||||
})
|
||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
|
|
@ -1137,56 +1138,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
});
|
||||
}
|
||||
|
||||
getUserReview() {
|
||||
return this.reviews.filter(r => r.username === this.user?.username && !r.isExternal);
|
||||
}
|
||||
|
||||
openReviewModal() {
|
||||
const userReview = this.getUserReview();
|
||||
|
||||
const modalRef = this.modalService.open(ReviewSeriesModalComponent, DefaultModalOptions);
|
||||
modalRef.componentInstance.series = this.series;
|
||||
if (userReview.length > 0) {
|
||||
modalRef.componentInstance.review = userReview[0];
|
||||
} else {
|
||||
modalRef.componentInstance.review = {
|
||||
seriesId: this.series.id,
|
||||
tagline: '',
|
||||
body: ''
|
||||
};
|
||||
}
|
||||
|
||||
modalRef.closed.subscribe((closeResult) => {
|
||||
this.updateOrDeleteReview(closeResult);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateOrDeleteReview(closeResult: ReviewSeriesModalCloseEvent) {
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Close) return;
|
||||
|
||||
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Edit) {
|
||||
if (index === -1 ) {
|
||||
// A new series was added:
|
||||
this.reviews = [closeResult.review, ...this.reviews];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
// An edit occurred
|
||||
this.reviews[index] = closeResult.review;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Delete) {
|
||||
// An edit occurred
|
||||
this.reviews = [...this.reviews.filter(r => r.username !== closeResult.review!.username)];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, click to show"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-local": "Local Reviews",
|
||||
"user-reviews-plus": "External Reviews"
|
||||
},
|
||||
|
||||
"review-series-modal": {
|
||||
"title": "Edit Review",
|
||||
|
|
@ -958,9 +962,6 @@
|
|||
"no-chapters": "There are no chapters to this volume. Cannot read.",
|
||||
"cover-change": "It can take up to a minute for your browser to refresh the image. Until then, the old image may be shown on some pages.",
|
||||
|
||||
"user-reviews-local": "Local Reviews",
|
||||
"user-reviews-plus": "External Reviews",
|
||||
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue