Display chapter reviews in volume page
This commit is contained in:
parent
85b6f107bc
commit
e0b27f464f
10 changed files with 124 additions and 36 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all reviews related to this volume, that is, the union of reviews of this volumes chapters
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("review")]
|
||||
public async Task<IList<UserReviewDto>> VolumeReviews([FromQuery] int volumeId)
|
||||
{
|
||||
return await _unitOfWork.UserRepository.GetUserRatingDtosForVolumeAsync(volumeId, User.GetUserId());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public class UserReviewDto
|
|||
/// The series this is for
|
||||
/// </summary>
|
||||
public int SeriesId { get; set; }
|
||||
public int? VolumeId { get; set; }
|
||||
public int? ChapterId { get; set; }
|
||||
/// <summary>
|
||||
/// The library this series belongs in
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ public interface IUserRepository
|
|||
Task<IList<string>> GetRoles(int userId);
|
||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForVolumeAsync(int volumeId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId);
|
||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||
|
|
@ -604,6 +605,18 @@ public class UserRepository : IUserRepository
|
|||
.ProjectTo<UserReviewDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
public async Task<IList<UserReviewDto>> 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<UserReviewDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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<UserReview[]>(this.baseUrl + 'volume/review?volumeId='+volumeId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReviewSeriesModalCloseEvent>();
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<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()">
|
||||
[iconClasses]=iconClasses()
|
||||
[clickableTitle]="canEditOrAdd()" (sectionClick)="openReviewModal()">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
|
||||
<app-review-card [reviewLocation]="reviewLocation" [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -191,6 +191,20 @@
|
|||
</li>
|
||||
}
|
||||
|
||||
<li [ngbNavItem]="TabID.Reviews">
|
||||
<a ngbNavLink>
|
||||
{{t('reviews-tab')}}
|
||||
<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" [volumeId]="volumeId"
|
||||
reviewLocation="chapter" />
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Details" id="details-tab">
|
||||
<a ngbNavLink>{{t('details-tab')}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
@ -147,7 +149,8 @@ interface VolumeCast extends IHasCast {
|
|||
DownloadButtonComponent,
|
||||
CardActionablesComponent,
|
||||
BulkOperationsComponent,
|
||||
CoverImageComponent
|
||||
CoverImageComponent,
|
||||
ReviewsComponent
|
||||
],
|
||||
templateUrl: './volume-detail.component.html',
|
||||
styleUrl: './volume-detail.component.scss',
|
||||
|
|
@ -196,6 +199,8 @@ export class VolumeDetailComponent implements OnInit {
|
|||
libraryType: LibraryType | null = null;
|
||||
activeTabId = TabID.Chapters;
|
||||
readingLists: ReadingList[] = [];
|
||||
userReviews: Array<UserReview> = [];
|
||||
plusReviews: Array<UserReview> = [];
|
||||
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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue