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}}",