Hooked up average rating for the issue, external ratings for individual issues (cbr only), and some polish.

Show Issues not Chapters for CBR matches.
This commit is contained in:
Joseph Milazzo 2025-04-29 10:05:01 -05:00
parent 6d4dfcda67
commit da99c97813
21 changed files with 231 additions and 40 deletions

View file

@ -423,6 +423,8 @@ public class ChapterController : BaseApiController
ret.Reviews = userReviews; ret.Reviews = userReviews;
ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapterId);
return ret; return ret;
} }

View file

@ -9,6 +9,6 @@ public class ChapterDetailPlusDto
public float Rating { get; set; } public float Rating { get; set; }
public bool HasBeenRated { get; set; } public bool HasBeenRated { get; set; }
public List<UserReviewDto> Reviews { get; set; } public IList<UserReviewDto> Reviews { get; set; } = [];
public List<RatingDto>? Ratings { get; set; } public IList<RatingDto> Ratings { get; set; } = [];
} }

View file

@ -57,5 +57,8 @@ public class UserReviewDto
/// If this review is External, which Provider did it come from /// If this review is External, which Provider did it come from
/// </summary> /// </summary>
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita; public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita;
/// <summary>
/// Source of the Rating
/// </summary>
public RatingAuthority Authority { get; set; } = RatingAuthority.User; public RatingAuthority Authority { get; set; } = RatingAuthority.User;
} }

View file

@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations namespace API.Data.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20250428180534_ChapterRating")] [Migration("20250429150140_ChapterRatingAndReviews")]
partial class ChapterRating partial class ChapterRatingAndReviews
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -790,6 +790,9 @@ namespace API.Data.Migrations
b.Property<string>("AlternateSeries") b.Property<string>("AlternateSeries")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<float>("AverageExternalRating")
.HasColumnType("REAL");
b.Property<float>("AvgHoursToRead") b.Property<float>("AvgHoursToRead")
.HasColumnType("REAL"); .HasColumnType("REAL");
@ -1354,9 +1357,15 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<int>("AverageScore") b.Property<int>("AverageScore")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("FavoriteCount") b.Property<int>("FavoriteCount")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1371,6 +1380,8 @@ namespace API.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("ExternalRating"); b.ToTable("ExternalRating");
}); });
@ -2978,6 +2989,13 @@ namespace API.Data.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{
b.HasOne("API.Entities.Chapter", null)
.WithMany("ExternalRatings")
.HasForeignKey("ChapterId");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
{ {
b.HasOne("API.Entities.Chapter", null) b.HasOne("API.Entities.Chapter", null)
@ -3445,6 +3463,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b => modelBuilder.Entity("API.Entities.Chapter", b =>
{ {
b.Navigation("ExternalRatings");
b.Navigation("ExternalReviews"); b.Navigation("ExternalReviews");
b.Navigation("Files"); b.Navigation("Files");

View file

@ -5,7 +5,7 @@
namespace API.Data.Migrations namespace API.Data.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class ChapterRating : Migration public partial class ChapterRatingAndReviews : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@ -23,6 +23,26 @@ namespace API.Data.Migrations
type: "INTEGER", type: "INTEGER",
nullable: true); nullable: true);
migrationBuilder.AddColumn<int>(
name: "Authority",
table: "ExternalRating",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "ChapterId",
table: "ExternalRating",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<float>(
name: "AverageExternalRating",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AppUserChapterRating", name: "AppUserChapterRating",
columns: table => new columns: table => new
@ -64,6 +84,11 @@ namespace API.Data.Migrations
table: "ExternalReview", table: "ExternalReview",
column: "ChapterId"); column: "ChapterId");
migrationBuilder.CreateIndex(
name: "IX_ExternalRating_ChapterId",
table: "ExternalRating",
column: "ChapterId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AppUserChapterRating_AppUserId", name: "IX_AppUserChapterRating_AppUserId",
table: "AppUserChapterRating", table: "AppUserChapterRating",
@ -79,6 +104,13 @@ namespace API.Data.Migrations
table: "AppUserChapterRating", table: "AppUserChapterRating",
column: "SeriesId"); column: "SeriesId");
migrationBuilder.AddForeignKey(
name: "FK_ExternalRating_Chapter_ChapterId",
table: "ExternalRating",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id");
migrationBuilder.AddForeignKey( migrationBuilder.AddForeignKey(
name: "FK_ExternalReview_Chapter_ChapterId", name: "FK_ExternalReview_Chapter_ChapterId",
table: "ExternalReview", table: "ExternalReview",
@ -90,6 +122,10 @@ namespace API.Data.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropForeignKey(
name: "FK_ExternalRating_Chapter_ChapterId",
table: "ExternalRating");
migrationBuilder.DropForeignKey( migrationBuilder.DropForeignKey(
name: "FK_ExternalReview_Chapter_ChapterId", name: "FK_ExternalReview_Chapter_ChapterId",
table: "ExternalReview"); table: "ExternalReview");
@ -101,6 +137,10 @@ namespace API.Data.Migrations
name: "IX_ExternalReview_ChapterId", name: "IX_ExternalReview_ChapterId",
table: "ExternalReview"); table: "ExternalReview");
migrationBuilder.DropIndex(
name: "IX_ExternalRating_ChapterId",
table: "ExternalRating");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "Authority", name: "Authority",
table: "ExternalReview"); table: "ExternalReview");
@ -108,6 +148,18 @@ namespace API.Data.Migrations
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "ChapterId", name: "ChapterId",
table: "ExternalReview"); table: "ExternalReview");
migrationBuilder.DropColumn(
name: "Authority",
table: "ExternalRating");
migrationBuilder.DropColumn(
name: "ChapterId",
table: "ExternalRating");
migrationBuilder.DropColumn(
name: "AverageExternalRating",
table: "Chapter");
} }
} }
} }

View file

@ -787,6 +787,9 @@ namespace API.Data.Migrations
b.Property<string>("AlternateSeries") b.Property<string>("AlternateSeries")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<float>("AverageExternalRating")
.HasColumnType("REAL");
b.Property<float>("AvgHoursToRead") b.Property<float>("AvgHoursToRead")
.HasColumnType("REAL"); .HasColumnType("REAL");
@ -1351,9 +1354,15 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<int>("AverageScore") b.Property<int>("AverageScore")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("FavoriteCount") b.Property<int>("FavoriteCount")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1368,6 +1377,8 @@ namespace API.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("ExternalRating"); b.ToTable("ExternalRating");
}); });
@ -2975,6 +2986,13 @@ namespace API.Data.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{
b.HasOne("API.Entities.Chapter", null)
.WithMany("ExternalRatings")
.HasForeignKey("ChapterId");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
{ {
b.HasOne("API.Entities.Chapter", null) b.HasOne("API.Entities.Chapter", null)
@ -3442,6 +3460,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b => modelBuilder.Entity("API.Entities.Chapter", b =>
{ {
b.Navigation("ExternalRatings");
b.Navigation("ExternalReviews"); b.Navigation("ExternalReviews");
b.Navigation("Files"); b.Navigation("Files");

View file

@ -52,6 +52,7 @@ public interface IChapterRepository
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId); Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
Task<int> GetAverageUserRating(int chapterId, int userId); Task<int> GetAverageUserRating(int chapterId, int userId);
Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId); Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId);
Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId);
} }
public class ChapterRepository : IChapterRepository public class ChapterRepository : IChapterRepository
{ {
@ -340,4 +341,13 @@ public class ChapterRepository : IChapterRepository
.Select(r => _mapper.Map<UserReviewDto>(r)) .Select(r => _mapper.Map<UserReviewDto>(r))
.ToListAsync(); .ToListAsync();
} }
public async Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId)
{
return await _context.Chapter
.Where(c => c.Id == chapterId)
.SelectMany(c => c.ExternalRatings)
.ProjectTo<RatingDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
} }

View file

@ -126,6 +126,11 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
public string WebLinks { get; set; } = string.Empty; public string WebLinks { get; set; } = string.Empty;
public string ISBN { get; set; } = string.Empty; public string ISBN { get; set; } = string.Empty;
/// <summary>
/// (Kavita+) Average rating from Kavita+ metadata
/// </summary>
public float AverageExternalRating { get; set; } = 0f;
#region Locks #region Locks
public bool AgeRatingLocked { get; set; } public bool AgeRatingLocked { get; set; }
@ -171,6 +176,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
public int VolumeId { get; set; } public int VolumeId { get; set; }
public ICollection<ExternalReview> ExternalReviews { get; set; } = []; public ICollection<ExternalReview> ExternalReviews { get; set; } = [];
public ICollection<ExternalRating> ExternalRatings { get; set; } = null!;
public void UpdateFrom(ParserInfo info) public void UpdateFrom(ParserInfo info)
{ {
@ -196,8 +202,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
/// <returns></returns> /// <returns></returns>
public string GetNumberTitle() public string GetNumberTitle()
{ {
// BUG: TODO: On non-english locales, for floats, the range will be 20,5 but the NumberTitle will return 20.5
// Have I fixed this with TryParse CultureInvariant
try try
{ {
if (MinNumber.Is(MaxNumber)) if (MinNumber.Is(MaxNumber))

View file

@ -1,7 +1,17 @@
using System.ComponentModel;
namespace API.Entities.Enums; namespace API.Entities.Enums;
public enum RatingAuthority public enum RatingAuthority
{ {
/// <summary>
/// Rating was from a User (internet or local)
/// </summary>
[Description("User")]
User = 0, User = 0,
/// <summary>
/// Rating was from Professional Critics
/// </summary>
[Description("Critic")]
Critic = 1, Critic = 1,
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities.Enums;
using API.Services.Plus; using API.Services.Plus;
namespace API.Entities.Metadata; namespace API.Entities.Metadata;
@ -10,8 +11,13 @@ public class ExternalRating
public int AverageScore { get; set; } public int AverageScore { get; set; }
public int FavoriteCount { get; set; } public int FavoriteCount { get; set; }
public ScrobbleProvider Provider { get; set; } public ScrobbleProvider Provider { get; set; }
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
public string? ProviderUrl { get; set; } public string? ProviderUrl { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
/// <summary>
/// This can be null when for a series-rating
/// </summary>
public int? ChapterId { get; set; }
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!; public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
} }

View file

@ -1085,7 +1085,7 @@ public class ExternalMetadataService : IExternalMetadataService
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification;
madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification;
madeModification = await UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification; madeModification = UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification;
_unitOfWork.ChapterRepository.Update(chapter); _unitOfWork.ChapterRepository.Update(chapter);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
@ -1094,20 +1094,20 @@ public class ExternalMetadataService : IExternalMetadataService
return madeModification; return madeModification;
} }
private async Task<bool> UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) private bool UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata)
{ {
if (!settings.Enabled) return false; if (!settings.Enabled) return false;
if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0) if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0)
{ {
_logger.LogDebug("No external reviews found for chapter {ChapterID}", chapter.Id);
return false; return false;
} }
var madeModification = false;
#region Review
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews); _unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews);
List<ExternalReview> externalReviews = []; List<ExternalReview> externalReviews = [];
externalReviews.AddRange(metadata.CriticReviews externalReviews.AddRange(metadata.CriticReviews
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
.Select(r => .Select(r =>
@ -1115,6 +1115,7 @@ public class ExternalMetadataService : IExternalMetadataService
var review = _mapper.Map<ExternalReview>(r); var review = _mapper.Map<ExternalReview>(r);
review.ChapterId = chapter.Id; review.ChapterId = chapter.Id;
review.Authority = RatingAuthority.Critic; review.Authority = RatingAuthority.Critic;
CleanCbrReview(ref review);
return review; return review;
})); }));
externalReviews.AddRange(metadata.UserReviews externalReviews.AddRange(metadata.UserReviews
@ -1124,13 +1125,55 @@ public class ExternalMetadataService : IExternalMetadataService
var review = _mapper.Map<ExternalReview>(r); var review = _mapper.Map<ExternalReview>(r);
review.ChapterId = chapter.Id; review.ChapterId = chapter.Id;
review.Authority = RatingAuthority.User; review.Authority = RatingAuthority.User;
CleanCbrReview(ref review);
return review; return review;
})); }));
chapter.ExternalReviews = externalReviews; chapter.ExternalReviews = externalReviews;
madeModification = externalReviews.Count > 0;
_logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id); _logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id);
return true; #endregion
#region Rating
var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating);
var averageUserRating = metadata.UserReviews.Average(r => r.Rating);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalRatings);
chapter.ExternalRatings =
[
new ExternalRating
{
AverageScore = (int) averageUserRating,
Provider = ScrobbleProvider.Cbr,
Authority = RatingAuthority.User,
ProviderUrl = metadata.IssueUrl,
},
new ExternalRating
{
AverageScore = (int) averageCriticRating,
Provider = ScrobbleProvider.Cbr,
Authority = RatingAuthority.Critic,
ProviderUrl = metadata.IssueUrl,
},
];
chapter.AverageExternalRating = averageUserRating;
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
#endregion
return madeModification;
}
private static void CleanCbrReview(ref ExternalReview review)
{
// CBR has Read Full Review which links to site, but we already have that
review.Body = review.Body.Replace("Read Full Review", string.Empty).TrimEnd();
review.RawBody = review.RawBody.Replace("Read Full Review", string.Empty).TrimEnd();
review.BodyJustText = review.BodyJustText.Replace("Read Full Review", string.Empty).TrimEnd();
} }

View file

@ -1,9 +1,15 @@
import {ScrobbleProvider} from "../_services/scrobbling.service"; import {ScrobbleProvider} from "../_services/scrobbling.service";
export enum RatingAuthority {
User = 0,
Critic = 1,
}
export interface Rating { export interface Rating {
averageScore: number; averageScore: number;
meanScore: number; meanScore: number;
favoriteCount: number; favoriteCount: number;
provider: ScrobbleProvider; provider: ScrobbleProvider;
providerUrl: string | undefined; providerUrl: string | undefined;
authority: RatingAuthority;
} }

View file

@ -34,7 +34,11 @@
<div class="d-flex pt-3 justify-content-between"> <div class="d-flex pt-3 justify-content-between">
@if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) { @if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) {
<span class="me-1">{{t('volume-count', {num: item.series.volumes})}}</span> <span class="me-1">{{t('volume-count', {num: item.series.volumes})}}</span>
@if (item.series.plusMediaFormat === PlusMediaFormat.Comic) {
<span class="me-1">{{t('issue-count', {num: item.series.chapters})}}</span>
} @else {
<span class="me-1">{{t('chapter-count', {num: item.series.chapters})}}</span> <span class="me-1">{{t('chapter-count', {num: item.series.chapters})}}</span>
}
} @else { } @else {
<span class="me-1">{{t('releasing')}}</span> <span class="me-1">{{t('releasing')}}</span>
} }

View file

@ -14,6 +14,7 @@ import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {TranslocoDirective} from "@jsverse/transloco"; import {TranslocoDirective} from "@jsverse/transloco";
import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe"; import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe";
import {LoadingComponent} from "../../shared/loading/loading.component"; import {LoadingComponent} from "../../shared/loading/loading.component";
import {PlusMediaFormat} from "../../_models/series-detail/external-series-detail";
@Component({ @Component({
selector: 'app-match-series-result-item', selector: 'app-match-series-result-item',
@ -47,4 +48,5 @@ export class MatchSeriesResultItemComponent {
this.selected.emit(this.item); this.selected.emit(this.item);
} }
protected readonly PlusMediaFormat = PlusMediaFormat;
} }

View file

@ -1,9 +1,6 @@
import {ScrobbleProvider} from "../../_services/scrobbling.service"; import {ScrobbleProvider} from "../../_services/scrobbling.service";
import {RatingAuthority} from "../../_models/rating";
export enum RatingAuthority {
User = 0,
Critic = 1
}
export interface UserReview { export interface UserReview {
seriesId: number; seriesId: number;

View file

@ -29,7 +29,7 @@
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">
<app-external-rating [seriesId]="series.id" <app-external-rating [seriesId]="series.id"
[ratings]="[]" [ratings]="ratings"
[userRating]="rating" [userRating]="rating"
[hasUserRated]="hasBeenRated" [hasUserRated]="hasBeenRated"
[libraryType]="libraryType!" [libraryType]="libraryType!"

View file

@ -1,23 +1,28 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, DestroyRef, Component,
DestroyRef,
ElementRef, ElementRef,
inject, inject,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {AsyncPipe, DOCUMENT, NgStyle, NgClass, DatePipe, Location} from "@angular/common"; import {AsyncPipe, DatePipe, DOCUMENT, Location, NgClass, NgStyle} from "@angular/common";
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component"; import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
import {LoadingComponent} from "../shared/loading/loading.component"; import {LoadingComponent} from "../shared/loading/loading.component";
import { import {
NgbDropdown, NgbDropdown,
NgbDropdownItem, NgbDropdownItem,
NgbDropdownMenu, NgbDropdownMenu,
NgbDropdownToggle, NgbModal, NgbDropdownToggle,
NgbNav, NgbNavChangeEvent, NgbModal,
NgbNavContent, NgbNavItem, NgbNav,
NgbNavLink, NgbNavOutlet, NgbNavChangeEvent,
NgbNavContent,
NgbNavItem,
NgbNavLink,
NgbNavOutlet,
NgbTooltip NgbTooltip
} from "@ng-bootstrap/ng-bootstrap"; } from "@ng-bootstrap/ng-bootstrap";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller"; import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
@ -66,14 +71,10 @@ import {DefaultDatePipe} from "../_pipes/default-date.pipe";
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 {UserReview} from "../_single-module/review-card/user-review";
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
import {ReviewCardComponent} from "../_single-module/review-card/review-card.component";
import {User} from "../_models/user"; import {User} from "../_models/user";
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 {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
import {Rating} from "../_models/rating"; import {Rating} from "../_models/rating";
import {ReviewService} from "../_services/review.service";
enum TabID { enum TabID {
Related = 'related-tab', Related = 'related-tab',
@ -149,6 +150,8 @@ export class ChapterDetailComponent implements OnInit {
protected readonly TabID = TabID; protected readonly TabID = TabID;
protected readonly FilterField = FilterField; protected readonly FilterField = FilterField;
protected readonly Breakpoint = Breakpoint; protected readonly Breakpoint = Breakpoint;
protected readonly LibraryType = LibraryType;
protected readonly encodeURIComponent = encodeURIComponent;
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined; @ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined; @ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
@ -165,6 +168,7 @@ export class ChapterDetailComponent implements OnInit {
userReviews: Array<UserReview> = []; userReviews: Array<UserReview> = [];
plusReviews: Array<UserReview> = []; plusReviews: Array<UserReview> = [];
rating: number = 0; rating: number = 0;
ratings: Array<Rating> = [];
hasBeenRated: boolean = false; hasBeenRated: boolean = false;
weblinks: Array<string> = []; weblinks: Array<string> = [];
@ -251,6 +255,7 @@ export class ChapterDetailComponent implements OnInit {
this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal); this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal);
this.rating = results.chapterDetail.rating; this.rating = results.chapterDetail.rating;
this.hasBeenRated = results.chapterDetail.hasBeenRated; this.hasBeenRated = results.chapterDetail.hasBeenRated;
this.ratings = results.chapterDetail.ratings;
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor); this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
@ -389,7 +394,4 @@ export class ChapterDetailComponent implements OnInit {
break; break;
} }
} }
protected readonly LibraryType = LibraryType;
protected readonly encodeURIComponent = encodeURIComponent;
} }

View file

@ -17,7 +17,7 @@
@for (rating of ratings; track rating.provider + rating.averageScore) { @for (rating of ratings; track rating.provider + rating.averageScore) {
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}" <div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
[popoverTitle]="rating.provider | scrobbleProviderName" popoverClass="sm-popover"> [popoverTitle]="(rating.provider | scrobbleProviderName) + getAuthorityTitle(rating)" popoverClass="sm-popover">
<span class="badge rounded-pill me-1"> <span class="badge rounded-pill me-1">
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true"> <img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
{{rating.averageScore}}% {{rating.averageScore}}%
@ -70,6 +70,7 @@
</div> </div>
} }
@if (rating.providerUrl) { @if (rating.providerUrl) {
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a> <a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
} }

View file

@ -8,8 +8,7 @@ import {
OnInit, OnInit,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import {SeriesService} from "../../../_services/series.service"; import {Rating, RatingAuthority} from "../../../_models/rating";
import {Rating} from "../../../_models/rating";
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap"; import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap";
import {LoadingComponent} from "../../../shared/loading/loading.component"; import {LoadingComponent} from "../../../shared/loading/loading.component";
@ -18,13 +17,12 @@ import {NgxStarsModule} from "ngx-stars";
import {ThemeService} from "../../../_services/theme.service"; import {ThemeService} from "../../../_services/theme.service";
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
import {ImageComponent} from "../../../shared/image/image.component"; import {ImageComponent} from "../../../shared/image/image.component";
import {TranslocoDirective} from "@jsverse/transloco"; import {translate, TranslocoDirective} from "@jsverse/transloco";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {ImageService} from "../../../_services/image.service"; 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";
import {ReviewService} from "../../../_services/review.service"; import {ReviewService} from "../../../_services/review.service";
@Component({ @Component({
@ -86,4 +84,14 @@ export class ExternalRatingComponent implements OnInit {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
getAuthorityTitle(rating: Rating) {
if (rating.authority === RatingAuthority.Critic) {
return ` (${translate('external-rating.critic')})`;
}
return '';
}
protected readonly RatingAuthority = RatingAuthority;
} }

View file

@ -36,7 +36,6 @@
[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 -->
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">
<app-external-rating [seriesId]="series.id" <app-external-rating [seriesId]="series.id"
[ratings]="ratings" [ratings]="ratings"

View file

@ -1011,6 +1011,7 @@
"match-series-result-item": { "match-series-result-item": {
"volume-count": "{{server-stats.volume-count}}", "volume-count": "{{server-stats.volume-count}}",
"chapter-count": "{{common.chapter-count}}", "chapter-count": "{{common.chapter-count}}",
"issue-count": "{{common.issue-count}}",
"releasing": "Releasing", "releasing": "Releasing",
"details": "View page", "details": "View page",
"updating-metadata-status": "Updating Metadata" "updating-metadata-status": "Updating Metadata"
@ -1048,7 +1049,8 @@
"entry-label": "See Details", "entry-label": "See Details",
"kavita-tooltip": "Your Rating + Overall", "kavita-tooltip": "Your Rating + Overall",
"kavita-rating-title": "Your Rating", "kavita-rating-title": "Your Rating",
"close": "{{common.close}}" "close": "{{common.close}}",
"critic": "Critic"
}, },
"badge-expander": { "badge-expander": {