Overall Ratings (#2129)

* Corrected tooltip for Cache

* Ensure we sync the DB to what's in appsettings.json for Cache key.

* Change the fingerprinting method for Windows installs exclusively to avoid churn due to how security updates are handled.

* Hooked up the ability to see where reviews are from via an icon on the review card, rather than having to click or know that MAL has "external Review" as title.

* Updated FAQ for Kavita+ to link directly to the FAQ

* Added the ability for all ratings on a series to be shown to the user.

Added favorite count on AL and MAL

* Cleaned up so the check for Kavita+ license doesn't seem like it's running when no license is registered.

* Tweaked the test instance buy link to test new product.
This commit is contained in:
Joe Milazzo 2023-07-14 14:12:03 -05:00 committed by GitHub
parent 34f828e750
commit 49daca943e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 231 additions and 56 deletions

View file

@ -24,8 +24,9 @@ import {UtilityService} from "../shared/_services/utility.service";
import {ReadingList} from "../_models/reading-list";
export enum ScrobbleProvider {
Kavita = 0,
AniList= 1,
Mal = 2
Mal = 2,
}
@Injectable({

View file

@ -32,12 +32,12 @@ export class SeriesService {
paginatedSeriesForTagsResults: PaginatedResult<Series[]> = new PaginatedResult<Series[]>();
constructor(private httpClient: HttpClient, private imageService: ImageService,
private utilityService: UtilityService, private filterUtilitySerivce: FilterUtilitiesService) { }
private utilityService: UtilityService, private filterUtilityService: FilterUtilitiesService) { }
getAllSeries(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
const data = this.filterUtilityService.createSeriesFilter(filter);
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
map((response: any) => {
@ -49,7 +49,7 @@ export class SeriesService {
getSeriesForLibrary(libraryId: number, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
const data = this.filterUtilityService.createSeriesFilter(filter);
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
map((response: any) => {
@ -103,7 +103,7 @@ export class SeriesService {
}
getRecentlyAdded(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
const data = this.filterUtilityService.createSeriesFilter(filter);
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -119,7 +119,7 @@ export class SeriesService {
}
getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter): Observable<PaginatedResult<Series[]>> {
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
const data = this.filterUtilityService.createSeriesFilter(filter);
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -138,7 +138,7 @@ export class SeriesService {
}
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
const data = this.filterUtilitySerivce.createSeriesFilter(filter);
const data = this.filterUtilityService.createSeriesFilter(filter);
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
@ -223,4 +223,7 @@ export class SeriesService {
getRatings(seriesId: number) {
return this.httpClient.get<Array<Rating>>(this.baseUrl + 'rating?seriesId=' + seriesId);
}
getOverallRating(seriesId: number) {
return this.httpClient.get<Rating>(this.baseUrl + 'rating/overall?seriesId=' + seriesId);
}
}

View file

@ -19,9 +19,15 @@
<app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="100" [showToggle]="false"></app-read-more>
</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent text-muted">
<ng-container *ngIf="isMyReview; else normalReview">
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" title="This is your review"></i>
</ng-container>
<ng-template #normalReview>
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
</ng-template>
{{(isMyReview ? '' : review.username | defaultValue:'')}}
<span style="float: right" *ngIf="review.isExternal">Rating {{review.score}}%</span>
</div>

View file

@ -8,7 +8,10 @@
z-index: 20;
top: 38px;
left: 38px;
color: var(--review-card-star-color);
}
.fa-star {
color: var(--review-card-star-color);
}
.card-text {
@ -29,10 +32,6 @@
overflow: hidden;
}
.card-footer {
width: 288px;
}
.card {
cursor: pointer;
}

View file

@ -1,5 +1,5 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CommonModule, NgOptimizedImage} from '@angular/common';
import {UserReview} from "./user-review";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component";
@ -7,11 +7,13 @@ import {AccountService} from "../../_services/account.service";
import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {ImageComponent} from "../../shared/image/image.component";
import {ProviderImagePipe} from "../../pipe/provider-image.pipe";
@Component({
selector: 'app-review-card',
standalone: true,
imports: [CommonModule, ReadMoreComponent, DefaultValuePipe],
imports: [CommonModule, ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe],
templateUrl: './review-card.component.html',
styleUrls: ['./review-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -1,3 +1,5 @@
import {ScrobbleProvider} from "../../_services/scrobbling.service";
export interface UserReview {
seriesId: number;
libraryId: number;
@ -8,4 +10,5 @@ export interface UserReview {
isExternal: boolean;
bodyJustText?: string;
externalUrl?: string;
provider: ScrobbleProvider;
}

View file

@ -36,12 +36,9 @@
<app-manage-tasks-settings></app-manage-tasks-settings>
</ng-container>
<ng-container *ngIf="tab.fragment === TabID.KavitaPlus">
<p>Kavita+ is a premium subscription service which unlocks features for all users on this Kavita instance. Buy a subscription to unlock <a href="https://wiki.kavitareader.com/en/kavita-plus" target="_blank" rel="noreferrer nofollow">premium benefits</a> today! <a href="https://wiki.kavitareader.com/en/kavita-plus" target="_blank" rel="noreferrer nofollow">FAQ</a></p>
<p>Kavita+ is a premium subscription service which unlocks features for all users on this Kavita instance. Buy a subscription to unlock <a href="https://wiki.kavitareader.com/en/kavita-plus" target="_blank" rel="noreferrer nofollow">premium benefits</a> today! <a href="https://wiki.kavitareader.com/en/kavita-plus#faq" target="_blank" rel="noreferrer nofollow">FAQ</a></p>
<app-license></app-license>
</ng-container>
<ng-container *ngIf="tab.fragment === TabID.Plugins">
Nothing here yet. This will be built out in a future update.
</ng-container>
</ng-template>
</li>
</ul>

View file

@ -100,7 +100,7 @@
<div class="row g-0 mb-2 mt-3">
<div class="col-md-4 col-sm-12 pe-2">
<label for="cache-size" class="form-label">Cache Size</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheSizeTooltip" role="button" tabindex="0"></i>
<ng-template #cacheSizeTooltip>The amount of memory allowed for caching heavy APIs. Default is 50MB.</ng-template>
<ng-template #cacheSizeTooltip>The amount of memory allowed for caching heavy APIs. Default is 75MB.</ng-template>
<span class="visually-hidden" id="cache-size-help">The amount of memory allowed for caching heavy APIs. Default is 50MB.</span>
<input id="cache-size" aria-describedby="cache-size-help" class="form-control" formControlName="cacheSize"
type="number" inputmode="numeric" step="5" min="50" onkeypress="return event.charCode >= 48 && event.charCode <= 57"

View file

@ -13,6 +13,8 @@ export class ProviderImagePipe implements PipeTransform {
return 'assets/images/ExternalServices/AniList.png';
case ScrobbleProvider.Mal:
return 'assets/images/ExternalServices/MAL.png';
case ScrobbleProvider.Kavita:
return 'assets/images/logo-32.png';
}
return '';

View file

@ -0,0 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core';
import {ScrobbleProvider} from "../_services/scrobbling.service";
@Pipe({
name: 'providerName',
standalone: true
})
export class ProviderNamePipe implements PipeTransform {
transform(value: ScrobbleProvider): string {
switch (value) {
case ScrobbleProvider.AniList:
return 'AniList';
case ScrobbleProvider.Mal:
return 'MAL';
case ScrobbleProvider.Kavita:
return 'Kavita';
}
return '';
}
}

View file

@ -1,13 +1,16 @@
<div class="row">
<div class="col-auto custom-col" style="cursor: pointer" [ngbPopover]="popContent"
popoverTitle="Series Rating">
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
<span class="badge rounded-pill me-1">
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
{{userRating * 20}}%
{{userRating * 20}}
<ng-container *ngIf="overallRating > 0; else noOverallRating"> + {{overallRating}}%</ng-container>
<ng-template #noOverallRating>%</ng-template>
</span>
</div>
<div class="col-auto custom-col" *ngFor="let rating of ratings">
<div class="col-auto custom-col clickable" *ngFor="let rating of ratings" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
<span class="badge rounded-pill me-1">
<img class="me-1" [ngSrc]="rating.provider | providerImage" width="24" height="24" alt="">
{{rating.averageScore}}%
@ -23,5 +26,9 @@
<ng-template let-fill="fill" let-index="index">
<span class="star" [class.filled]="(index < userRating) && userRating > 0">&#9733;</span>
</ng-template>
</ngb-rating>
</ngb-rating> {{userRating * 20}}%
</ng-template>
<ng-template #externalPopContent let-rating="rating">
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
</ng-template>

View file

@ -2,3 +2,21 @@
padding-left: 0px;
padding-right: 0px;
}
.sm-popover {
width: 150px;
> .popover-body {
padding-top: 0px;
}
}
.md-popover {
width: 214px;
> .popover-body {
padding-top: 0px;
}
}

View file

@ -1,4 +1,12 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
inject,
Input,
OnInit,
ViewEncapsulation
} from '@angular/core';
import {CommonModule, NgOptimizedImage} from '@angular/common';
import {SeriesService} from "../../../_services/series.service";
import {Rating} from "../../../_models/rating";
@ -7,14 +15,16 @@ import {NgbPopover, NgbRating} from "@ng-bootstrap/ng-bootstrap";
import {LoadingComponent} from "../../../shared/loading/loading.component";
import {AccountService} from "../../../_services/account.service";
import {LibraryType} from "../../../_models/library";
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
@Component({
selector: 'app-external-rating',
standalone: true,
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent],
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe],
templateUrl: './external-rating.component.html',
styleUrls: ['./external-rating.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class ExternalRatingComponent implements OnInit {
@Input({required: true}) seriesId!: number;
@ -26,10 +36,13 @@ export class ExternalRatingComponent implements OnInit {
ratings: Array<Rating> = [];
isLoading: boolean = false;
overallRating: number = -1;
ngOnInit() {
this.seriesService.getOverallRating(this.seriesId).subscribe(r => this.overallRating = r.averageScore);
this.accountService.hasValidLicense$.subscribe((res) => {
if (!res) return;
this.isLoading = true;

View file

@ -6,7 +6,7 @@ export const environment = {
production: false,
apiUrl: 'http://localhost:5000/api/',
hubUrl: 'http://localhost:5000/hubs/',
buyLink: 'https://buy.stripe.com/test_8wM4ie2dg5j77o4cMO?prefilled_promo_code=FREETRIAL',
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
};