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 System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
|
@ -81,4 +83,15 @@ public class VolumeController : BaseApiController
|
||||||
|
|
||||||
return Ok(true);
|
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
|
/// The series this is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
public int? VolumeId { get; set; }
|
||||||
|
public int? ChapterId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The library this series belongs in
|
/// The library this series belongs in
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ public interface IUserRepository
|
||||||
Task<IList<string>> GetRoles(int userId);
|
Task<IList<string>> GetRoles(int userId);
|
||||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(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<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId);
|
||||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||||
|
|
@ -604,6 +605,18 @@ public class UserRepository : IUserRepository
|
||||||
.ProjectTo<UserReviewDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<UserReviewDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.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)
|
public async Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,12 @@ public class AutoMapperProfiles : Profile
|
||||||
.ForMember(dest => dest.LibraryId,
|
.ForMember(dest => dest.LibraryId,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.Series.LibraryId))
|
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,
|
.ForMember(dest => dest.Body,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.Review))
|
opt.MapFrom(src => src.Review))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {environment} from "../../environments/environment";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import {Volume} from "../_models/volume";
|
import {Volume} from "../_models/volume";
|
||||||
import {TextResonse} from "../_types/text-response";
|
import {TextResonse} from "../_types/text-response";
|
||||||
|
import {UserReview} from "../_single-module/review-card/user-review";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -28,4 +29,9 @@ export class VolumeService {
|
||||||
updateVolume(volume: any) {
|
updateVolume(volume: any) {
|
||||||
return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse);
|
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;
|
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||||
|
|
||||||
@Input({required: true}) review!: UserReview;
|
@Input({required: true}) review!: UserReview;
|
||||||
|
@Input() reviewLocation: 'series' | 'chapter' = 'series';
|
||||||
@Output() refresh = new EventEmitter<ReviewSeriesModalCloseEvent>();
|
@Output() refresh = new EventEmitter<ReviewSeriesModalCloseEvent>();
|
||||||
|
|
||||||
isMyReview: boolean = false;
|
isMyReview: boolean = false;
|
||||||
|
|
@ -44,7 +45,7 @@ export class ReviewCardComponent implements OnInit {
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.accountService.currentUser$.subscribe(u => {
|
this.accountService.currentUser$.subscribe(u => {
|
||||||
if (u) {
|
if (u) {
|
||||||
this.isMyReview = this.review.username === u.username;
|
this.isMyReview = this.review.username === u.username && !this.review.isExternal;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -58,6 +59,11 @@ export class ReviewCardComponent implements OnInit {
|
||||||
component = ReviewCardModalComponent;
|
component = ReviewCardModalComponent;
|
||||||
}
|
}
|
||||||
const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'});
|
const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'});
|
||||||
|
|
||||||
|
if (this.isMyReview) {
|
||||||
|
ref.componentInstance.reviewLocation = this.reviewLocation;
|
||||||
|
}
|
||||||
|
|
||||||
ref.componentInstance.review = this.review;
|
ref.componentInstance.review = this.review;
|
||||||
ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => {
|
ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<div class="mb-3" *transloco="let t;prefix:'reviews'">
|
<div class="mb-3" *transloco="let t;prefix:'reviews'">
|
||||||
<app-carousel-reel [items]="userReviews" [alwaysShow]="true" [title]="t('user-reviews-local')"
|
<app-carousel-reel [items]="userReviews" [alwaysShow]="true" [title]="t('user-reviews-local')"
|
||||||
iconClasses="fa-solid fa-{{getUserReviews().length > 0 ? 'pen' : 'plus'}}"
|
[iconClasses]=iconClasses()
|
||||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
[clickableTitle]="canEditOrAdd()" (sectionClick)="openReviewModal()">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<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>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
</div>
|
</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() {
|
openReviewModal() {
|
||||||
const userReview = this.getUserReviews();
|
const userReview = this.getUserReviews();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,20 @@
|
||||||
</li>
|
</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">
|
<li [ngbNavItem]="TabID.Details" id="details-tab">
|
||||||
<a ngbNavLink>{{t('details-tab')}}</a>
|
<a ngbNavLink>{{t('details-tab')}}</a>
|
||||||
<ng-template ngbNavContent>
|
<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 {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
|
||||||
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
||||||
import {DefaultModalOptions} from "../_models/default-modal-options";
|
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 {
|
enum TabID {
|
||||||
|
|
||||||
|
|
@ -119,36 +121,37 @@ interface VolumeCast extends IHasCast {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-volume-detail',
|
selector: 'app-volume-detail',
|
||||||
imports: [
|
imports: [
|
||||||
LoadingComponent,
|
LoadingComponent,
|
||||||
NgbNavOutlet,
|
NgbNavOutlet,
|
||||||
DetailsTabComponent,
|
DetailsTabComponent,
|
||||||
NgbNavItem,
|
NgbNavItem,
|
||||||
NgbNavLink,
|
NgbNavLink,
|
||||||
NgbNavContent,
|
NgbNavContent,
|
||||||
NgbNav,
|
NgbNav,
|
||||||
ReadMoreComponent,
|
ReadMoreComponent,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NgbDropdownItem,
|
NgbDropdownItem,
|
||||||
NgbDropdownMenu,
|
NgbDropdownMenu,
|
||||||
NgbDropdown,
|
NgbDropdown,
|
||||||
NgbDropdownToggle,
|
NgbDropdownToggle,
|
||||||
EntityTitleComponent,
|
EntityTitleComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
NgbTooltip,
|
NgbTooltip,
|
||||||
NgStyle,
|
NgStyle,
|
||||||
NgClass,
|
NgClass,
|
||||||
TranslocoDirective,
|
TranslocoDirective,
|
||||||
VirtualScrollerModule,
|
VirtualScrollerModule,
|
||||||
ChapterCardComponent,
|
ChapterCardComponent,
|
||||||
RelatedTabComponent,
|
RelatedTabComponent,
|
||||||
BadgeExpanderComponent,
|
BadgeExpanderComponent,
|
||||||
MetadataDetailRowComponent,
|
MetadataDetailRowComponent,
|
||||||
DownloadButtonComponent,
|
DownloadButtonComponent,
|
||||||
CardActionablesComponent,
|
CardActionablesComponent,
|
||||||
BulkOperationsComponent,
|
BulkOperationsComponent,
|
||||||
CoverImageComponent
|
CoverImageComponent,
|
||||||
],
|
ReviewsComponent
|
||||||
|
],
|
||||||
templateUrl: './volume-detail.component.html',
|
templateUrl: './volume-detail.component.html',
|
||||||
styleUrl: './volume-detail.component.scss',
|
styleUrl: './volume-detail.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|
@ -196,6 +199,8 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
libraryType: LibraryType | null = null;
|
libraryType: LibraryType | null = null;
|
||||||
activeTabId = TabID.Chapters;
|
activeTabId = TabID.Chapters;
|
||||||
readingLists: ReadingList[] = [];
|
readingLists: ReadingList[] = [];
|
||||||
|
userReviews: Array<UserReview> = [];
|
||||||
|
plusReviews: Array<UserReview> = [];
|
||||||
mobileSeriesImgBackground: string | undefined;
|
mobileSeriesImgBackground: string | undefined;
|
||||||
downloadInProgress: boolean = false;
|
downloadInProgress: boolean = false;
|
||||||
|
|
||||||
|
|
@ -374,7 +379,8 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
forkJoin({
|
forkJoin({
|
||||||
series: this.seriesService.getSeries(this.seriesId),
|
series: this.seriesService.getSeries(this.seriesId),
|
||||||
volume: this.volumeService.getVolumeMetadata(this.volumeId),
|
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 => {
|
}).subscribe(results => {
|
||||||
|
|
||||||
if (results.volume === null) {
|
if (results.volume === null) {
|
||||||
|
|
@ -385,6 +391,8 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
this.series = results.series;
|
this.series = results.series;
|
||||||
this.volume = results.volume;
|
this.volume = results.volume;
|
||||||
this.libraryType = results.libraryType;
|
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);
|
this.themeService.setColorScape(this.volume!.primaryColor, this.volume!.secondaryColor);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue