Add rating to chapter page & volume (if one chapter)
This commit is contained in:
parent
e96cb0fde9
commit
8ccc2b5801
18 changed files with 226 additions and 47 deletions
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
API/DTOs/UpdateChapterRatingDto.cs
Normal file
7
API/DTOs/UpdateChapterRatingDto.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
public class UpdateChapterRatingDto
|
||||||
|
{
|
||||||
|
public int ChapterId { get; init; }
|
||||||
|
public float Rating { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
63
API/Services/RatingService.cs
Normal file
63
API/Services/RatingService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue