Add rating to chapter page & volume (if one chapter)

This commit is contained in:
Amelia 2025-04-26 09:40:56 +02:00
parent e96cb0fde9
commit 8ccc2b5801
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
18 changed files with 226 additions and 47 deletions

View file

@ -28,13 +28,16 @@ public class ChapterController : BaseApiController
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;
private readonly ILogger<ChapterController> _logger; private readonly ILogger<ChapterController> _logger;
private readonly IRatingService _ratingService;
public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger<ChapterController> logger) public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger<ChapterController> logger,
IRatingService ratingService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_localizationService = localizationService; _localizationService = localizationService;
_eventHub = eventHub; _eventHub = eventHub;
_logger = logger; _logger = logger;
_ratingService = ratingService;
} }
/// <summary> /// <summary>
@ -403,4 +406,15 @@ public class ChapterController : BaseApiController
return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId()); return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId());
} }
[HttpPost("update-rating")]
public async Task<ActionResult> UpdateRating(UpdateChapterRatingDto dto)
{
if (await _ratingService.UpdateChapterRating(User.GetUserId(), dto))
{
return Ok();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}
} }

View file

@ -38,4 +38,15 @@ public class RatingController : BaseApiController
FavoriteCount = 0 FavoriteCount = 0
}); });
} }
[HttpGet("overall/chapter")]
public async Task<ActionResult<RatingDto>> GetOverallChapterRating([FromQuery] int chapterId)
{
return Ok(new RatingDto
{
Provider = ScrobbleProvider.Kavita,
AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, User.GetUserId()),
FavoriteCount = 0,
});
}
} }

View file

@ -0,0 +1,7 @@
namespace API.DTOs;
public class UpdateChapterRatingDto
{
public int ChapterId { get; init; }
public float Rating { get; init; }
}

View file

@ -48,6 +48,7 @@ public interface IChapterRepository
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter); Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
IEnumerable<Chapter> GetChaptersForSeries(int seriesId); IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId); Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
Task<int> GetAverageUserRating(int chapterId, int userId);
} }
public class ChapterRepository : IChapterRepository public class ChapterRepository : IChapterRepository
{ {
@ -310,4 +311,20 @@ public class ChapterRepository : IChapterRepository
.ThenInclude(cp => cp.Person) .ThenInclude(cp => cp.Person)
.ToListAsync(); .ToListAsync();
} }
public async Task<int> GetAverageUserRating(int chapterId, int userId)
{
// If there is 0 or 1 rating and that rating is you, return 0 back
var countOfRatingsThatAreUser = await _context.AppUserChapterRating
.Where(r => r.ChapterId == chapterId && r.HasBeenRated)
.CountAsync(u => u.AppUserId == userId);
if (countOfRatingsThatAreUser == 1)
{
return 0;
}
var avg = (await _context.AppUserChapterRating
.Where(r => r.ChapterId == chapterId && r.HasBeenRated)
.AverageAsync(r => (int?) r.Rating));
return avg.HasValue ? (int) (avg.Value * 20) : 0;
}
} }

View file

@ -66,6 +66,7 @@ public interface IUserRepository
Task<bool> IsUserAdminAsync(AppUser? user); Task<bool> IsUserAdminAsync(AppUser? user);
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<AppUserChapterRating?> GetUserChapterRatingAsync(int chapterId, 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>> GetUserRatingDtosForVolumeAsync(int volumeId, int userId);
Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId); Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId);
@ -592,6 +593,12 @@ public class UserRepository : IUserRepository
.Where(r => r.SeriesId == seriesId && r.AppUserId == userId) .Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
.SingleOrDefaultAsync(); .SingleOrDefaultAsync();
} }
public async Task<AppUserChapterRating?> GetUserChapterRatingAsync(int chapterId, int userId)
{
return await _context.AppUserChapterRating
.Where(r => r.ChapterId == chapterId && r.AppUserId == userId)
.FirstOrDefaultAsync();
}
public async Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) public async Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId)
{ {

View file

@ -52,6 +52,7 @@ public static class ApplicationServiceExtensions
services.AddScoped<IMediaErrorService, MediaErrorService>(); services.AddScoped<IMediaErrorService, MediaErrorService>();
services.AddScoped<IMediaConversionService, MediaConversionService>(); services.AddScoped<IMediaConversionService, MediaConversionService>();
services.AddScoped<IStreamService, StreamService>(); services.AddScoped<IStreamService, StreamService>();
services.AddScoped<IRatingService, RatingService>();
services.AddScoped<IScannerService, ScannerService>(); services.AddScoped<IScannerService, ScannerService>();
services.AddScoped<IProcessSeries, ProcessSeries>(); services.AddScoped<IProcessSeries, ProcessSeries>();

View file

@ -0,0 +1,63 @@
using System;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using Microsoft.Extensions.Logging;
namespace API.Services;
public interface IRatingService
{
Task<bool> UpdateChapterRating(int userId, UpdateChapterRatingDto dto);
}
public class RatingService: IRatingService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<RatingService> _logger;
public RatingService(IUnitOfWork unitOfWork, ILogger<RatingService> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<bool> UpdateChapterRating(int userId, UpdateChapterRatingDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ChapterRatings);
if (user == null) throw new UnauthorizedAccessException();
var rating = await _unitOfWork.UserRepository.GetUserChapterRatingAsync(dto.ChapterId, userId) ?? new AppUserChapterRating();
rating.Rating = Math.Clamp(dto.Rating, 0, 5);
rating.HasBeenRated = true;
rating.ChapterId = dto.ChapterId;
if (rating.Id == 0)
{
user.ChapterRatings.Add(rating);
}
_unitOfWork.UserRepository.Update(user);
try
{
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
{
// Scrobble Update?
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception while updating chapter rating");
}
await _unitOfWork.RollbackAsync();
user.ChapterRatings.Remove(rating);
return false;
}
}

View file

@ -4,6 +4,7 @@ import { HttpClient } from "@angular/common/http";
import {Chapter} from "../_models/chapter"; import {Chapter} from "../_models/chapter";
import {TextResonse} from "../_types/text-response"; import {TextResonse} from "../_types/text-response";
import {UserReview} from "../_single-module/review-card/user-review"; import {UserReview} from "../_single-module/review-card/user-review";
import {Rating} from "../_models/rating";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -35,11 +36,19 @@ export class ChapterService {
} }
updateChapterReview(seriesId: number, chapterId: number, body: string, rating: number) { updateChapterReview(seriesId: number, chapterId: number, body: string, rating: number) {
return this.httpClient.post<UserReview>(this.baseUrl + 'review/chapter/'+chapterId, {seriesId, body}); return this.httpClient.post<UserReview>(this.baseUrl + 'review/chapter/'+chapterId, {seriesId, rating, body});
} }
deleteChapterReview(chapterId: number) { deleteChapterReview(chapterId: number) {
return this.httpClient.delete(this.baseUrl + 'review/chapter/'+chapterId); return this.httpClient.delete(this.baseUrl + 'review/chapter/'+chapterId);
} }
overallRating(chapterId: number) {
return this.httpClient.get<Rating>(this.baseUrl + 'rating/overall?chapterId='+chapterId);
}
updateRating(chapterId: number, rating: number) {
return this.httpClient.post(this.baseUrl + 'chapter/update-rating', {chapterId, rating});
}
} }

View file

@ -210,7 +210,7 @@ export class SeriesService {
} }
updateReview(seriesId: number, body: string, rating: number) { updateReview(seriesId: number, body: string, rating: number) {
return this.httpClient.post<UserReview>(this.baseUrl + 'review', { return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
seriesId, body, rating seriesId, rating, body
}); });
} }

View file

@ -14,7 +14,7 @@ import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component"; import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component";
import {AccountService} from "../../_services/account.service"; import {AccountService} from "../../_services/account.service";
import { import {
ReviewSeriesModalCloseEvent, ReviewModalCloseEvent,
ReviewModalComponent ReviewModalComponent
} from "../review-modal/review-modal.component"; } from "../review-modal/review-modal.component";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
@ -35,7 +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;
@Output() refresh = new EventEmitter<ReviewSeriesModalCloseEvent>(); @Output() refresh = new EventEmitter<ReviewModalCloseEvent>();
isMyReview: boolean = false; isMyReview: boolean = false;
@ -60,7 +60,7 @@ export class ReviewCardComponent implements OnInit {
const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'}); const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'});
ref.componentInstance.review = this.review; ref.componentInstance.review = this.review;
ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => { ref.closed.subscribe((res: ReviewModalCloseEvent | undefined) => {
if (res) { if (res) {
this.refresh.emit(res); this.refresh.emit(res);
} }

View file

@ -11,16 +11,16 @@ import {of} from "rxjs";
import {NgxStarsModule} from "ngx-stars"; import {NgxStarsModule} from "ngx-stars";
import {ThemeService} from "../../_services/theme.service"; import {ThemeService} from "../../_services/theme.service";
export enum ReviewSeriesModalCloseAction { export enum ReviewModalCloseAction {
Create, Create,
Edit, Edit,
Delete, Delete,
Close Close
} }
export interface ReviewSeriesModalCloseEvent { export interface ReviewModalCloseEvent {
success: boolean, success: boolean,
review: UserReview; review: UserReview;
action: ReviewSeriesModalCloseAction action: ReviewModalCloseAction
} }
@Component({ @Component({
@ -43,6 +43,7 @@ export class ReviewModalComponent implements OnInit {
@Input({required: true}) review!: UserReview; @Input({required: true}) review!: UserReview;
reviewGroup!: FormGroup; reviewGroup!: FormGroup;
rating: number = 0;
starColor = this.themeService.getCssVariable('--rating-star-color'); starColor = this.themeService.getCssVariable('--rating-star-color');
@ -50,15 +51,16 @@ export class ReviewModalComponent implements OnInit {
this.reviewGroup = new FormGroup({ this.reviewGroup = new FormGroup({
reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]), reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
}); });
this.rating = this.review.rating;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
updateRating($event: number) { updateRating($event: number) {
this.review.rating = $event; this.rating = $event;
} }
close() { close() {
this.modal.close({success: false, review: this.review, action: ReviewSeriesModalCloseAction.Close}); this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close});
} }
async delete() { async delete() {
@ -73,7 +75,7 @@ export class ReviewModalComponent implements OnInit {
obs?.subscribe(() => { obs?.subscribe(() => {
this.toastr.success(translate('toasts.review-deleted')); this.toastr.success(translate('toasts.review-deleted'));
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete}); this.modal.close({success: true, review: this.review, action: ReviewModalCloseAction.Delete});
}); });
} }
@ -85,13 +87,13 @@ export class ReviewModalComponent implements OnInit {
let obs; let obs;
if (!this.review.chapterId) { if (!this.review.chapterId) {
obs = this.seriesService.updateReview(this.review.seriesId, model.reviewBody, this.review.rating); obs = this.seriesService.updateReview(this.review.seriesId, model.reviewBody, this.rating);
} else { } else {
obs = this.chapterService.updateChapterReview(this.review.seriesId, this.review.chapterId, model.reviewBody, this.review.rating); obs = this.chapterService.updateChapterReview(this.review.seriesId, this.review.chapterId, model.reviewBody, this.rating);
} }
obs?.subscribe(review => { obs?.subscribe(review => {
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit}); this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit});
}); });
} }

View file

@ -6,8 +6,8 @@ import {UserReview} from "../review-card/user-review";
import {User} from "../../_models/user"; import {User} from "../../_models/user";
import {AccountService} from "../../_services/account.service"; import {AccountService} from "../../_services/account.service";
import { import {
ReviewModalComponent, ReviewSeriesModalCloseAction, ReviewModalComponent, ReviewModalCloseAction,
ReviewSeriesModalCloseEvent ReviewModalCloseEvent
} from "../review-modal/review-modal.component"; } from "../review-modal/review-modal.component";
import {DefaultModalOptions} from "../../_models/default-modal-options"; import {DefaultModalOptions} from "../../_models/default-modal-options";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
@ -71,11 +71,11 @@ export class ReviewsComponent {
} }
updateOrDeleteReview(closeResult: ReviewSeriesModalCloseEvent) { updateOrDeleteReview(closeResult: ReviewModalCloseEvent) {
if (closeResult.action === ReviewSeriesModalCloseAction.Close) return; if (closeResult.action === ReviewModalCloseAction.Close) return;
const index = this.userReviews.findIndex(r => r.username === closeResult.review!.username); const index = this.userReviews.findIndex(r => r.username === closeResult.review!.username);
if (closeResult.action === ReviewSeriesModalCloseAction.Edit) { if (closeResult.action === ReviewModalCloseAction.Edit) {
if (index === -1 ) { if (index === -1 ) {
this.userReviews = [closeResult.review, ...this.userReviews]; this.userReviews = [closeResult.review, ...this.userReviews];
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -86,7 +86,7 @@ export class ReviewsComponent {
return; return;
} }
if (closeResult.action === ReviewSeriesModalCloseAction.Delete) { if (closeResult.action === ReviewModalCloseAction.Delete) {
this.userReviews = [...this.userReviews.filter(r => r.username !== closeResult.review!.username)]; this.userReviews = [...this.userReviews.filter(r => r.username !== closeResult.review!.username)];
this.cdRef.markForCheck(); this.cdRef.markForCheck();
return; return;

View file

@ -27,15 +27,17 @@
<!-- Rating goes here (after I implement support for rating individual issues --> <div class="mt-2 mb-2">
<!-- <div class="mt-2 mb-2">--> @let rating = userRating();
<!-- <app-external-rating [seriesId]="series.id"--> <app-external-rating [seriesId]="series.id"
<!-- [ratings]="[]"--> [ratings]="[]"
<!-- [userRating]="series.userRating"--> [userRating]="rating?.rating || 0"
<!-- [hasUserRated]="series.hasUserRated"--> [hasUserRated]="rating !== undefined"
<!-- [libraryType]="libraryType!">--> [libraryType]="libraryType!"
<!-- </app-external-rating>--> [chapterId]="chapterId"
<!-- </div>--> >
</app-external-rating>
</div>
<div class="mt-3 mb-3"> <div class="mt-3 mb-3">
<div class="row g-0"> <div class="row g-0">

View file

@ -71,6 +71,8 @@ import {ReviewCardComponent} from "../_single-module/review-card/review-card.com
import {User} from "../_models/user"; import {User} from "../_models/user";
import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.component"; import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.component";
import {ReviewsComponent} from "../_single-module/reviews/reviews.component"; import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
import {Rating} from "../_models/rating";
enum TabID { enum TabID {
Related = 'related-tab', Related = 'related-tab',
@ -111,7 +113,8 @@ enum TabID {
CoverImageComponent, CoverImageComponent,
CarouselReelComponent, CarouselReelComponent,
ReviewCardComponent, ReviewCardComponent,
ReviewsComponent ReviewsComponent,
ExternalRatingComponent
], ],
templateUrl: './chapter-detail.component.html', templateUrl: './chapter-detail.component.html',
styleUrl: './chapter-detail.component.scss', styleUrl: './chapter-detail.component.scss',
@ -174,6 +177,7 @@ export class ChapterDetailComponent implements OnInit {
mobileSeriesImgBackground: string | undefined; mobileSeriesImgBackground: string | undefined;
chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
user: User | undefined;
get ScrollingBlockHeight() { get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)'; if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
@ -188,6 +192,12 @@ export class ChapterDetailComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.accountService.currentUser$.subscribe(user => {
if (user) {
this.user = user;
}
});
const seriesId = this.route.snapshot.paramMap.get('seriesId'); const seriesId = this.route.snapshot.paramMap.get('seriesId');
const libraryId = this.route.snapshot.paramMap.get('libraryId'); const libraryId = this.route.snapshot.paramMap.get('libraryId');
const chapterId = this.route.snapshot.paramMap.get('chapterId'); const chapterId = this.route.snapshot.paramMap.get('chapterId');
@ -376,6 +386,10 @@ export class ChapterDetailComponent implements OnInit {
} }
} }
userRating() {
return this.userReviews.find(r => r.username == this.user?.username && !r.isExternal)
}
protected readonly LibraryType = LibraryType; protected readonly LibraryType = LibraryType;
protected readonly encodeURIComponent = encodeURIComponent; protected readonly encodeURIComponent = encodeURIComponent;
} }

View file

@ -24,6 +24,7 @@ import {ImageService} from "../../../_services/image.service";
import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common"; import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common";
import {RatingModalComponent} from "../rating-modal/rating-modal.component"; import {RatingModalComponent} from "../rating-modal/rating-modal.component";
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe"; import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
import {ChapterService} from "../../../_services/chapter.service";
@Component({ @Component({
selector: 'app-external-rating', selector: 'app-external-rating',
@ -38,6 +39,7 @@ export class ExternalRatingComponent implements OnInit {
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly seriesService = inject(SeriesService); private readonly seriesService = inject(SeriesService);
private readonly chapterService = inject(ChapterService);
private readonly themeService = inject(ThemeService); private readonly themeService = inject(ThemeService);
public readonly utilityService = inject(UtilityService); public readonly utilityService = inject(UtilityService);
public readonly destroyRef = inject(DestroyRef); public readonly destroyRef = inject(DestroyRef);
@ -47,6 +49,7 @@ export class ExternalRatingComponent implements OnInit {
protected readonly Breakpoint = Breakpoint; protected readonly Breakpoint = Breakpoint;
@Input({required: true}) seriesId!: number; @Input({required: true}) seriesId!: number;
@Input() chapterId: number | undefined;
@Input({required: true}) userRating!: number; @Input({required: true}) userRating!: number;
@Input({required: true}) hasUserRated!: boolean; @Input({required: true}) hasUserRated!: boolean;
@Input({required: true}) libraryType!: LibraryType; @Input({required: true}) libraryType!: LibraryType;
@ -58,11 +61,24 @@ export class ExternalRatingComponent implements OnInit {
starColor = this.themeService.getCssVariable('--rating-star-color'); starColor = this.themeService.getCssVariable('--rating-star-color');
ngOnInit() { ngOnInit() {
this.seriesService.getOverallRating(this.seriesId).subscribe(r => this.overallRating = r.averageScore); let obs;
if (this.chapterId) {
obs = this.chapterService.overallRating(this.chapterId);
} else {
obs = this.seriesService.getOverallRating(this.seriesId);
}
obs?.subscribe(r => this.overallRating = r.averageScore);
} }
updateRating(rating: number) { updateRating(rating: number) {
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => { let obs;
if (this.chapterId) {
obs = this.chapterService.updateRating(this.chapterId, rating);
} else {
obs = this.seriesService.updateRating(this.seriesId, rating);
}
obs?.subscribe(() => {
this.userRating = rating; this.userRating = rating;
this.hasUserRated = true; this.hasUserRated = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View file

@ -62,8 +62,8 @@ import {ReadingListService} from 'src/app/_services/reading-list.service';
import {ScrollService} from 'src/app/_services/scroll.service'; import {ScrollService} from 'src/app/_services/scroll.service';
import {SeriesService} from 'src/app/_services/series.service'; import {SeriesService} from 'src/app/_services/series.service';
import { import {
ReviewSeriesModalCloseAction, ReviewModalCloseAction,
ReviewSeriesModalCloseEvent, ReviewModalCloseEvent,
ReviewModalComponent ReviewModalComponent
} from '../../../_single-module/review-modal/review-modal.component'; } from '../../../_single-module/review-modal/review-modal.component';
import {PageLayoutMode} from 'src/app/_models/page-layout-mode'; import {PageLayoutMode} from 'src/app/_models/page-layout-mode';

View file

@ -29,17 +29,18 @@
[mangaFormat]="series.format"> [mangaFormat]="series.format">
</app-metadata-detail-row> </app-metadata-detail-row>
<!-- Rating goes here (after I implement support for rating individual issues --> @if (libraryType !== null && series && volume.chapters.length === 1) {
<!-- @if (libraryType !== null && series) {--> <div class="mt-2 mb-2">
<!-- <div class="mt-2 mb-2">--> @let rating = userRating();
<!-- <app-external-rating [seriesId]="series.id"--> <app-external-rating [seriesId]="series.id"
<!-- [ratings]="[]"--> [ratings]="[]"
<!-- [userRating]="series.userRating"--> [userRating]="rating?.rating || 0"
<!-- [hasUserRated]="series.hasUserRated"--> [hasUserRated]="rating !== undefined"
<!-- [libraryType]="libraryType">--> [libraryType]="libraryType"
<!-- </app-external-rating>--> [chapterId]="volume.chapters[0].id"
<!-- </div>--> />
<!-- }--> </div>
}
<div class="mt-2 mb-3"> <div class="mt-2 mb-3">
<div class="row g-0"> <div class="row g-0">

View file

@ -80,6 +80,8 @@ import {CoverImageComponent} from "../_single-module/cover-image/cover-image.com
import {DefaultModalOptions} from "../_models/default-modal-options"; import {DefaultModalOptions} from "../_models/default-modal-options";
import {UserReview} from "../_single-module/review-card/user-review"; import {UserReview} from "../_single-module/review-card/user-review";
import {ReviewsComponent} from "../_single-module/reviews/reviews.component"; import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
import {User} from "../_models/user";
enum TabID { enum TabID {
@ -150,7 +152,8 @@ interface VolumeCast extends IHasCast {
CardActionablesComponent, CardActionablesComponent,
BulkOperationsComponent, BulkOperationsComponent,
CoverImageComponent, CoverImageComponent,
ReviewsComponent ReviewsComponent,
ExternalRatingComponent
], ],
templateUrl: './volume-detail.component.html', templateUrl: './volume-detail.component.html',
styleUrl: './volume-detail.component.scss', styleUrl: './volume-detail.component.scss',
@ -204,6 +207,8 @@ export class VolumeDetailComponent implements OnInit {
mobileSeriesImgBackground: string | undefined; mobileSeriesImgBackground: string | undefined;
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
user: User | undefined;
volumeActions: Array<ActionItem<Volume>> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this)); volumeActions: Array<ActionItem<Volume>> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this));
chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
@ -337,6 +342,12 @@ export class VolumeDetailComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.accountService.currentUser$.subscribe(user => {
if (user) {
this.user = user;
}
})
const seriesId = this.route.snapshot.paramMap.get('seriesId'); const seriesId = this.route.snapshot.paramMap.get('seriesId');
const libraryId = this.route.snapshot.paramMap.get('libraryId'); const libraryId = this.route.snapshot.paramMap.get('libraryId');
const volumeId = this.route.snapshot.paramMap.get('volumeId'); const volumeId = this.route.snapshot.paramMap.get('volumeId');
@ -675,5 +686,9 @@ export class VolumeDetailComponent implements OnInit {
} }
} }
userRating() {
return this.userReviews.find(r => r.username === this.user?.username && !r.isExternal);
}
protected readonly encodeURIComponent = encodeURIComponent; protected readonly encodeURIComponent = encodeURIComponent;
} }