From e0b27f464f1db071956c309034b99acf8e21d82f Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:57:45 +0200 Subject: [PATCH] Display chapter reviews in volume page --- API/Controllers/VolumeController.cs | 15 +++- API/DTOs/SeriesDetail/UserReviewDto.cs | 2 + API/Data/Repositories/UserRepository.cs | 13 ++++ API/Helpers/AutoMapperProfiles.cs | 6 ++ UI/Web/src/app/_services/volume.service.ts | 6 ++ .../review-card/review-card.component.ts | 8 ++- .../reviews/reviews.component.html | 6 +- .../reviews/reviews.component.ts | 20 ++++++ .../volume-detail.component.html | 14 ++++ .../volume-detail/volume-detail.component.ts | 70 +++++++++++-------- 10 files changed, 124 insertions(+), 36 deletions(-) diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs index db1381d9d..0e1f57b0b 100644 --- a/API/Controllers/VolumeController.cs +++ b/API/Controllers/VolumeController.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.SeriesDetail; using API.Extensions; using API.Services; using API.SignalR; @@ -81,4 +83,15 @@ public class VolumeController : BaseApiController return Ok(true); } + + /// + /// Returns all reviews related to this volume, that is, the union of reviews of this volumes chapters + /// + /// + /// + [HttpGet("review")] + public async Task> VolumeReviews([FromQuery] int volumeId) + { + return await _unitOfWork.UserRepository.GetUserRatingDtosForVolumeAsync(volumeId, User.GetUserId()); + } } diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/API/DTOs/SeriesDetail/UserReviewDto.cs index 0e080d43f..8b3d7e4c3 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UserReviewDto.cs @@ -26,6 +26,8 @@ public class UserReviewDto /// The series this is for /// public int SeriesId { get; set; } + public int? VolumeId { get; set; } + public int? ChapterId { get; set; } /// /// The library this series belongs in /// diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 8efe4093f..9872b96f7 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -67,6 +67,7 @@ public interface IUserRepository Task> GetRoles(int userId); Task GetUserRatingAsync(int seriesId, int userId); Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); + Task> GetUserRatingDtosForVolumeAsync(int volumeId, int userId); Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId); Task GetPreferencesAsync(string username); Task> GetBookmarkDtosForSeries(int userId, int seriesId); @@ -604,6 +605,18 @@ public class UserRepository : IUserRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } + public async Task> GetUserRatingDtosForVolumeAsync(int volumeId, int userId) + { + return await _context.AppUserChapterRating + .Include(r => r.AppUser) + .Where(r => r.VolumeId == volumeId) + .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) + .OrderBy(r => r.AppUserId == userId) + .ThenBy(r => r.Rating) + .AsSplitQuery() + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) { diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 334403ab3..b725834b2 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -101,6 +101,12 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Series.LibraryId)) + .ForMember(dest => dest.VolumeId, + opt => + opt.MapFrom(src => src.VolumeId)) + .ForMember(dest => dest.ChapterId, + opt => + opt.MapFrom(src => src.ChapterId)) .ForMember(dest => dest.Body, opt => opt.MapFrom(src => src.Review)) diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts index f53a20543..777f1ed61 100644 --- a/UI/Web/src/app/_services/volume.service.ts +++ b/UI/Web/src/app/_services/volume.service.ts @@ -3,6 +3,7 @@ import {environment} from "../../environments/environment"; import { HttpClient } from "@angular/common/http"; import {Volume} from "../_models/volume"; import {TextResonse} from "../_types/text-response"; +import {UserReview} from "../_single-module/review-card/user-review"; @Injectable({ providedIn: 'root' @@ -28,4 +29,9 @@ export class VolumeService { updateVolume(volume: any) { return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse); } + + volumeReviews(volumeId: number) { + return this.httpClient.get(this.baseUrl + 'volume/review?volumeId='+volumeId); + } + } 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 61967c3b1..f609248fa 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 @@ -35,6 +35,7 @@ export class ReviewCardComponent implements OnInit { protected readonly ScrobbleProvider = ScrobbleProvider; @Input({required: true}) review!: UserReview; + @Input() reviewLocation: 'series' | 'chapter' = 'series'; @Output() refresh = new EventEmitter(); isMyReview: boolean = false; @@ -44,7 +45,7 @@ export class ReviewCardComponent implements OnInit { ngOnInit() { this.accountService.currentUser$.subscribe(u => { if (u) { - this.isMyReview = this.review.username === u.username; + this.isMyReview = this.review.username === u.username && !this.review.isExternal; this.cdRef.markForCheck(); } }); @@ -58,6 +59,11 @@ export class ReviewCardComponent implements OnInit { component = ReviewCardModalComponent; } const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'}); + + if (this.isMyReview) { + ref.componentInstance.reviewLocation = this.reviewLocation; + } + ref.componentInstance.review = this.review; ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => { if (res) { diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.html b/UI/Web/src/app/_single-module/reviews/reviews.component.html index cec5deb8d..add498ab9 100644 --- a/UI/Web/src/app/_single-module/reviews/reviews.component.html +++ b/UI/Web/src/app/_single-module/reviews/reviews.component.html @@ -1,9 +1,9 @@
+ [iconClasses]=iconClasses() + [clickableTitle]="canEditOrAdd()" (sectionClick)="openReviewModal()"> - +
diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.ts b/UI/Web/src/app/_single-module/reviews/reviews.component.ts index 231db4bed..ef72021a4 100644 --- a/UI/Web/src/app/_single-module/reviews/reviews.component.ts +++ b/UI/Web/src/app/_single-module/reviews/reviews.component.ts @@ -50,6 +50,26 @@ export class ReviewsComponent { }); } + iconClasses(): string { + let classes = 'fa-solid'; + if (this.canEditOrAdd()) { + classes += 'fa-' + (this.getUserReviews().length > 0 ? 'pen' : 'plus'); + } + return classes; + } + + canEditOrAdd(): boolean { + if (this.reviewLocation === 'series') { + return true; + } + + if (this.reviewLocation === 'chapter') { + return this.chapter !== undefined; + } + + return false; + } + openReviewModal() { const userReview = this.getUserReviews(); diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index e704646fb..bfe75a827 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -191,6 +191,20 @@ } +
  • + + {{t('reviews-tab')}} + {{userReviews.length + plusReviews.length}} + + + @defer (when activeTabId === TabID.Reviews; prefetch on idle) { + + } + +
  • +
  • {{t('details-tab')}} diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index 19cf13a27..1f0206d73 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -78,6 +78,8 @@ import {EditChapterModalComponent} from "../_single-module/edit-chapter-modal/ed import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component"; 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 {ReviewsComponent} from "../_single-module/reviews/reviews.component"; enum TabID { @@ -119,36 +121,37 @@ interface VolumeCast extends IHasCast { @Component({ selector: 'app-volume-detail', - imports: [ - LoadingComponent, - NgbNavOutlet, - DetailsTabComponent, - NgbNavItem, - NgbNavLink, - NgbNavContent, - NgbNav, - ReadMoreComponent, - AsyncPipe, - NgbDropdownItem, - NgbDropdownMenu, - NgbDropdown, - NgbDropdownToggle, - EntityTitleComponent, - RouterLink, - NgbTooltip, - NgStyle, - NgClass, - TranslocoDirective, - VirtualScrollerModule, - ChapterCardComponent, - RelatedTabComponent, - BadgeExpanderComponent, - MetadataDetailRowComponent, - DownloadButtonComponent, - CardActionablesComponent, - BulkOperationsComponent, - CoverImageComponent - ], + imports: [ + LoadingComponent, + NgbNavOutlet, + DetailsTabComponent, + NgbNavItem, + NgbNavLink, + NgbNavContent, + NgbNav, + ReadMoreComponent, + AsyncPipe, + NgbDropdownItem, + NgbDropdownMenu, + NgbDropdown, + NgbDropdownToggle, + EntityTitleComponent, + RouterLink, + NgbTooltip, + NgStyle, + NgClass, + TranslocoDirective, + VirtualScrollerModule, + ChapterCardComponent, + RelatedTabComponent, + BadgeExpanderComponent, + MetadataDetailRowComponent, + DownloadButtonComponent, + CardActionablesComponent, + BulkOperationsComponent, + CoverImageComponent, + ReviewsComponent + ], templateUrl: './volume-detail.component.html', styleUrl: './volume-detail.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -196,6 +199,8 @@ export class VolumeDetailComponent implements OnInit { libraryType: LibraryType | null = null; activeTabId = TabID.Chapters; readingLists: ReadingList[] = []; + userReviews: Array = []; + plusReviews: Array = []; mobileSeriesImgBackground: string | undefined; downloadInProgress: boolean = false; @@ -374,7 +379,8 @@ export class VolumeDetailComponent implements OnInit { forkJoin({ series: this.seriesService.getSeries(this.seriesId), volume: this.volumeService.getVolumeMetadata(this.volumeId), - libraryType: this.libraryService.getLibraryType(this.libraryId) + libraryType: this.libraryService.getLibraryType(this.libraryId), + reviews: this.volumeService.volumeReviews(this.volumeId), }).subscribe(results => { if (results.volume === null) { @@ -385,6 +391,8 @@ export class VolumeDetailComponent implements OnInit { this.series = results.series; this.volume = results.volume; this.libraryType = results.libraryType; + this.userReviews = results.reviews.filter(r => !r.isExternal); + this.plusReviews = results.reviews.filter(r => r.isExternal); this.themeService.setColorScape(this.volume!.primaryColor, this.volume!.secondaryColor);