From a3e04f3bc113ffbfe6d9aef243d4bae29e1a1aae Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:48:54 +0200 Subject: [PATCH] Extract review tab & add to chapter --- .../review-card/review-card.component.ts | 6 +- .../_single-module/review-card/user-review.ts | 2 + .../review-modal.component.html} | 0 .../review-modal.component.scss} | 0 .../review-modal.component.ts} | 31 +++-- .../reviews/reviews.component.html | 17 +++ .../reviews/reviews.component.scss | 0 .../reviews/reviews.component.ts | 106 ++++++++++++++++++ .../chapter-detail.component.html | 14 +++ .../chapter-detail.component.ts | 71 +++++++----- .../series-detail.component.html | 19 +--- .../series-detail/series-detail.component.ts | 71 ++---------- UI/Web/src/assets/langs/en.json | 7 +- 13 files changed, 220 insertions(+), 124 deletions(-) rename UI/Web/src/app/_single-module/{review-series-modal/review-series-modal.component.html => review-modal/review-modal.component.html} (100%) rename UI/Web/src/app/_single-module/{review-series-modal/review-series-modal.component.scss => review-modal/review-modal.component.scss} (100%) rename UI/Web/src/app/_single-module/{review-series-modal/review-series-modal.component.ts => review-modal/review-modal.component.ts} (67%) create mode 100644 UI/Web/src/app/_single-module/reviews/reviews.component.html create mode 100644 UI/Web/src/app/_single-module/reviews/reviews.component.scss create mode 100644 UI/Web/src/app/_single-module/reviews/reviews.component.ts diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.ts b/UI/Web/src/app/_single-module/review-card/review-card.component.ts index 55216b169..61967c3b1 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.ts +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.ts @@ -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; } diff --git a/UI/Web/src/app/_single-module/review-card/user-review.ts b/UI/Web/src/app/_single-module/review-card/user-review.ts index 1b5771463..f638bd95d 100644 --- a/UI/Web/src/app/_single-module/review-card/user-review.ts +++ b/UI/Web/src/app/_single-module/review-card/user-review.ts @@ -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; diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html b/UI/Web/src/app/_single-module/review-modal/review-modal.component.html similarity index 100% rename from UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html rename to UI/Web/src/app/_single-module/review-modal/review-modal.component.html diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.scss b/UI/Web/src/app/_single-module/review-modal/review-modal.component.scss similarity index 100% rename from UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.scss rename to UI/Web/src/app/_single-module/review-modal/review-modal.component.scss diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts b/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts similarity index 67% rename from UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts rename to UI/Web/src/app/_single-module/review-modal/review-modal.component.ts index d15c3076c..5b3dd0241 100644 --- a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts +++ b/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts @@ -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}); + }); + } + } } diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.html b/UI/Web/src/app/_single-module/reviews/reviews.component.html new file mode 100644 index 000000000..cec5deb8d --- /dev/null +++ b/UI/Web/src/app/_single-module/reviews/reviews.component.html @@ -0,0 +1,17 @@ +
+ + + + + +
+ +
+ + + + + +
diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.scss b/UI/Web/src/app/_single-module/reviews/reviews.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.ts b/UI/Web/src/app/_single-module/reviews/reviews.component.ts new file mode 100644 index 000000000..231db4bed --- /dev/null +++ b/UI/Web/src/app/_single-module/reviews/reviews.component.ts @@ -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; + @Input({required: true}) plusReviews!: Array; + @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); + } + +} diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 55f073c69..1e90262f0 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -175,6 +175,20 @@ } +
  • + + {{t(TabID.Reviews)}} + {{userReviews.length + plusReviews.length}} + + + @defer (when activeTabId === TabID.Reviews; prefetch on idle) { + + } + +
  • + @if(readingLists.length > 0) {
  • {{t('related-tab')}} diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts index 0b4e8cfa4..b3e46aef2 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts @@ -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 = []; + plusReviews: Array = []; weblinks: Array = []; activeTabId = TabID.Details; /** diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index db1db1e43..450f74aa2 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -315,25 +315,8 @@ @defer (when activeTabId === TabID.Reviews; prefetch on idle) { -
    - - - - - -
    - -
    - - - - - -
    + } -
  • diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 68d2ac4dd..77d035751 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -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) { if (typeof action.callback === 'function') { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index c63967973..4af461f45 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -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}}",