New Year Bugs (#2513)

This commit is contained in:
Joe Milazzo 2024-01-02 18:53:10 -06:00 committed by GitHub
parent fcacd67d71
commit 5dfcccba7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 230 additions and 142 deletions

View File

@ -318,19 +318,18 @@ public class AccountController : BaseApiController
[HttpPost("reset-api-key")] [HttpPost("reset-api-key")]
public async Task<ActionResult<string>> ResetApiKey() public async Task<ActionResult<string>> ResetApiKey()
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException();
if (user == null) throw new KavitaUnauthenticatedUserException();
user.ApiKey = HashUtil.ApiKey(); user.ApiKey = HashUtil.ApiKey();
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{ {
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate,
MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
return Ok(user.ApiKey); return Ok(user.ApiKey);
} }
await _unitOfWork.RollbackAsync(); await _unitOfWork.RollbackAsync();
return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key"));
} }

View File

@ -55,8 +55,10 @@ public class ReviewController : BaseApiController
public async Task<ActionResult<IEnumerable<UserReviewDto>>> GetReviews(int seriesId) public async Task<ActionResult<IEnumerable<UserReviewDto>>> GetReviews(int seriesId)
{ {
var userId = User.GetUserId(); var userId = User.GetUserId();
var username = User.GetUsername();
var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId)) var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId))
.Where(r => !string.IsNullOrEmpty(r.Body) && !string.IsNullOrEmpty(r.Tagline)) .Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(username) ? 1 : 0)
.ToList(); .ToList();
if (!await _licenseService.HasActiveLicense()) if (!await _licenseService.HasActiveLicense())
{ {
@ -139,7 +141,7 @@ public class ReviewController : BaseApiController
var rating = ratingBuilder var rating = ratingBuilder
.WithBody(dto.Body) .WithBody(dto.Body)
.WithSeriesId(dto.SeriesId) .WithSeriesId(dto.SeriesId)
.WithTagline(dto.Tagline) .WithTagline(string.Empty)
.Build(); .Build();
if (rating.Id == 0) if (rating.Id == 0)
@ -152,7 +154,7 @@ public class ReviewController : BaseApiController
BackgroundJob.Enqueue(() => BackgroundJob.Enqueue(() =>
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, dto.Tagline, dto.Body)); _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
return Ok(_mapper.Map<UserReviewDto>(rating)); return Ok(_mapper.Map<UserReviewDto>(rating));
} }
} }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Scrobbling; using API.DTOs.Scrobbling;
using API.Services.Plus;
namespace API.DTOs.Recommendation; namespace API.DTOs.Recommendation;
#nullable enable #nullable enable
@ -19,4 +20,5 @@ public class ExternalSeriesDetailDto
public string? Summary { get; set; } public string? Summary { get; set; }
public int? VolumeCount { get; set; } public int? VolumeCount { get; set; }
public int? ChapterCount { get; set; } public int? ChapterCount { get; set; }
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
} }

View File

@ -1,4 +1,6 @@
namespace API.DTOs.Recommendation; using API.Services.Plus;
namespace API.DTOs.Recommendation;
#nullable enable #nullable enable
public class ExternalSeriesDto public class ExternalSeriesDto
@ -9,4 +11,5 @@ public class ExternalSeriesDto
public string? Summary { get; set; } public string? Summary { get; set; }
public int? AniListId { get; set; } public int? AniListId { get; set; }
public long? MalId { get; set; } public long? MalId { get; set; }
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
} }

View File

@ -6,7 +6,5 @@ namespace API.DTOs.SeriesDetail;
public class UpdateUserReviewDto public class UpdateUserReviewDto
{ {
public int SeriesId { get; set; } public int SeriesId { get; set; }
[MaxLength(120)]
public string? Tagline { get; set; }
public string Body { get; set; } public string Body { get; set; }
} }

View File

@ -12,6 +12,7 @@ public class UserReviewDto
/// <summary> /// <summary>
/// A tagline for the review /// A tagline for the review
/// </summary> /// </summary>
/// <remarks>This is not possible to set as a local user</remarks>
public string? Tagline { get; set; } public string? Tagline { get; set; }
/// <summary> /// <summary>

View File

@ -28,6 +28,7 @@ public class RatingBuilder : IEntityBuilder<AppUserRating>
public RatingBuilder WithTagline(string? tagline) public RatingBuilder WithTagline(string? tagline)
{ {
if (string.IsNullOrEmpty(tagline)) return this;
_rating.Tagline = tagline; _rating.Tagline = tagline;
return this; return this;
} }

View File

@ -42,7 +42,7 @@ public interface IScrobblingService
Task CheckExternalAccessTokens(); Task CheckExternalAccessTokens();
Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider); Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider);
Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); Task ScrobbleRatingUpdate(int userId, int seriesId, float rating);
Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody); Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody);
Task ScrobbleReadingUpdate(int userId, int seriesId); Task ScrobbleReadingUpdate(int userId, int seriesId);
Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead);
@ -185,8 +185,10 @@ public class ScrobblingService : IScrobblingService
} ?? string.Empty; } ?? string.Empty;
} }
public async Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody) public async Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody)
{ {
// Currently disabled until at least hardcover is implemented
return;
if (!await _licenseService.HasActiveLicense()) return; if (!await _licenseService.HasActiveLicense()) return;
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);

View File

@ -295,9 +295,9 @@ public class ProcessSeries : IProcessSeries
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
{ {
series.Metadata.MaxCount = 1; series.Metadata.MaxCount = 1;
} else if (series.Metadata.TotalCount == 1 && chapters.Count == 1 && chapters[0].IsSpecial) } else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial)
{ {
// If a series has a TotalCount of 1 and there is only a Special, mark it as Complete // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete
series.Metadata.MaxCount = series.Metadata.TotalCount; series.Metadata.MaxCount = series.Metadata.TotalCount;
} else if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) } else if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount)
{ {

View File

@ -41,7 +41,7 @@ export class ErrorInterceptor implements HttpInterceptor {
break; break;
default: default:
// Don't throw multiple Something unexpected went wrong // Don't throw multiple Something unexpected went wrong
let genericError = translate('errors.generic'); const genericError = translate('errors.generic');
if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.' && this.toastr.previousToastMessage !== genericError) { if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.' && this.toastr.previousToastMessage !== genericError) {
this.toast(genericError); this.toast(genericError);
} }

View File

@ -1,3 +1,5 @@
import {ScrobbleProvider} from "../../_services/scrobbling.service";
export enum PlusMediaFormat { export enum PlusMediaFormat {
Manga = 1, Manga = 1,
Comic = 2, Comic = 2,
@ -37,5 +39,5 @@ export interface ExternalSeriesDetail {
chapterCount?: number; chapterCount?: number;
staff: Array<SeriesStaff>; staff: Array<SeriesStaff>;
tags: Array<MetadataTagDto>; tags: Array<MetadataTagDto>;
provider: ScrobbleProvider;
} }

View File

@ -1,3 +1,5 @@
import {ScrobbleProvider} from "../../_services/scrobbling.service";
export interface ExternalSeries { export interface ExternalSeries {
name: string; name: string;
coverUrl: string; coverUrl: string;
@ -5,4 +7,5 @@ export interface ExternalSeries {
summary: string; summary: string;
aniListId?: number; aniListId?: number;
malId?: number; malId?: number;
provider: ScrobbleProvider;
} }

View File

@ -213,9 +213,9 @@ export class SeriesService {
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId); return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId);
} }
updateReview(seriesId: number, tagline: string, body: string) { updateReview(seriesId: number, body: string) {
return this.httpClient.post<UserReview>(this.baseUrl + 'review', { return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
seriesId, tagline, body seriesId, body
}); });
} }

View File

@ -1,7 +1,9 @@
<ng-container *transloco="let t; read:'review-card-modal'"> <ng-container *transloco="let t; read:'review-card-modal'">
<div> <div>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('user-review', {username: review.username})}} {{review.isExternal ? t('external-mod') : ''}}</h4> <h4 class="modal-title" id="modal-basic-title">
{{t('user-review', {username: review.username})}} @if(review.isExternal) {<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">}
</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button> <button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div> </div>
<div class="modal-body scrollable-modal"> <div class="modal-body scrollable-modal">

View File

@ -1,24 +1,26 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component, inject,
Inject, Inject,
Input, ViewChild, Input, ViewChild,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import {CommonModule, DOCUMENT} from '@angular/common'; import {CommonModule, DOCUMENT, NgOptimizedImage} from '@angular/common';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import {UserReview} from "../review-card/user-review"; import {UserReview} from "../review-card/user-review";
import {SpoilerComponent} from "../spoiler/spoiler.component"; import {SpoilerComponent} from "../spoiler/spoiler.component";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
@Component({ @Component({
selector: 'app-review-card-modal', selector: 'app-review-card-modal',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective], imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective, DefaultValuePipe, NgOptimizedImage, ProviderImagePipe],
templateUrl: './review-card-modal.component.html', templateUrl: './review-card-modal.component.html',
styleUrls: ['./review-card-modal.component.scss'], styleUrls: ['./review-card-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -26,12 +28,13 @@ import {TranslocoDirective} from "@ngneat/transloco";
}) })
export class ReviewCardModalComponent implements AfterViewInit { export class ReviewCardModalComponent implements AfterViewInit {
private modal = inject(NgbActiveModal);
@Input({required: true}) review!: UserReview; @Input({required: true}) review!: UserReview;
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef; @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
constructor(private modal: NgbActiveModal, @Inject(DOCUMENT) private document: Document) { constructor(@Inject(DOCUMENT) private document: Document) {}
}
close() { close() {
this.modal.close(); this.modal.close();

View File

@ -10,11 +10,8 @@
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div class="card-body"> <div class="card-body">
<h6 class="card-title" [title]="review.tagline"> <h6 class="card-title">
<ng-container *ngIf="review.tagline && review.tagline.length > 0; else noTagline">{{review.tagline.substring(0, 29)}}{{review.tagline.length > 29 ? '…' : ''}}</ng-container> {{review.isExternal ? t('external-review') : t('local-review')}}
<ng-template #noTagline>
{{review.isExternal ? t('external-review') : t('local-review')}}
</ng-template>
</h6> </h6>
<p class="card-text no-images"> <p class="card-text no-images">
<app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="100" [showToggle]="false"></app-read-more> <app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="100" [showToggle]="false"></app-read-more>
@ -23,9 +20,11 @@
</div> </div>
<div class="card-footer bg-transparent text-muted"> <div class="card-footer bg-transparent text-muted">
<div class="review-user"> <div>
<ng-container *ngIf="isMyReview; else normalReview"> <ng-container *ngIf="isMyReview; else normalReview">
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i> <i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
{{review.username}}
</ng-container> </ng-container>
<ng-template #normalReview> <ng-template #normalReview>
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt=""> <img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">

View File

@ -39,7 +39,7 @@
.card-footer { .card-footer {
font-size: 13px; font-size: 13px;
display: flex; display: flex;
max-width: 305px; max-width: 319px;
justify-content: space-between; justify-content: space-between;
margin: 0 auto; margin: 0 auto;
} }

View File

@ -10,6 +10,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {ImageComponent} from "../../shared/image/image.component"; import {ImageComponent} from "../../shared/image/image.component";
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe"; import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {ScrobbleProvider} from "../../_services/scrobbling.service";
@Component({ @Component({
selector: 'app-review-card', selector: 'app-review-card',
@ -20,9 +21,11 @@ import {TranslocoDirective} from "@ngneat/transloco";
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReviewCardComponent implements OnInit { export class ReviewCardComponent implements OnInit {
private readonly accountService = inject(AccountService);
protected readonly ScrobbleProvider = ScrobbleProvider;
@Input({required: true}) review!: UserReview; @Input({required: true}) review!: UserReview;
private readonly accountService = inject(AccountService);
isMyReview: boolean = false; isMyReview: boolean = false;
constructor(private readonly modalService: NgbModal, private readonly cdRef: ChangeDetectorRef) {} constructor(private readonly modalService: NgbModal, private readonly cdRef: ChangeDetectorRef) {}
@ -46,5 +49,4 @@ 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;
} }
} }

View File

@ -8,14 +8,19 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form [formGroup]="reviewGroup"> <form [formGroup]="reviewGroup">
<div class="row g-0">
<label for="tagline" class="form-label">{{t('tagline-label')}}</label>
<input id="tagline" class="form-control" formControlName="tagline" />
</div>
<div class="row g-0 mt-2"> <div class="row g-0 mt-2">
<label for="review" class="form-label">{{t('review-label')}}</label> <label for="review" class="form-label">{{t('review-label')}}</label>
<textarea id="review" class="form-control" formControlName="reviewBody" rows="3" ></textarea> <textarea id="review" class="form-control" formControlName="reviewBody" rows="3" [minlength]="minLength"
[class.is-invalid]="reviewGroup.get('reviewBody')?.invalid && reviewGroup.get('reviewBody')?.touched" aria-describedby="body-validations"
></textarea>
<div id="body-validations" class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
@if (reviewGroup.get('reviewBody')?.errors?.required) {
<div>{{t('required')}}</div>
}
@if (reviewGroup.get('reviewBody')?.errors?.minlength) {
<div>{{t('min-length', {count: minLength})}}</div>
}
</div>
</div> </div>
</form> </form>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap'; import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
@ -9,22 +9,24 @@ import {TranslocoDirective} from "@ngneat/transloco";
@Component({ @Component({
selector: 'app-review-series-modal', selector: 'app-review-series-modal',
standalone: true, standalone: true,
imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective], imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective],
templateUrl: './review-series-modal.component.html', templateUrl: './review-series-modal.component.html',
styleUrls: ['./review-series-modal.component.scss'], styleUrls: ['./review-series-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReviewSeriesModalComponent implements OnInit { export class ReviewSeriesModalComponent implements OnInit {
protected readonly modal = inject(NgbActiveModal);
private readonly seriesService = inject(SeriesService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly minLength = 20;
@Input({required: true}) review!: UserReview; @Input({required: true}) review!: UserReview;
reviewGroup!: FormGroup; reviewGroup!: FormGroup;
constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void { ngOnInit(): void {
this.reviewGroup = new FormGroup({ this.reviewGroup = new FormGroup({
tagline: new FormControl(this.review.tagline || '', [Validators.min(20), Validators.max(120)]), reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
reviewBody: new FormControl(this.review.body, [Validators.min(20)]),
}); });
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -35,7 +37,10 @@ export class ReviewSeriesModalComponent implements OnInit {
save() { save() {
const model = this.reviewGroup.value; const model = this.reviewGroup.value;
this.seriesService.updateReview(this.review.seriesId, model.tagline, model.reviewBody).subscribe(() => { if (model.reviewBody.length < this.minLength) {
return;
}
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(() => {
this.modal.close({success: true}); this.modal.close({success: true});
}); });
} }

View File

@ -2,6 +2,7 @@
<div class="offcanvas-header"> <div class="offcanvas-header">
<h5 class="offcanvas-title"> <h5 class="offcanvas-title">
{{name}} {{name}}
</h5> </h5>
<button type="button" class="btn-close text-reset" [attr.aria-label]="t('common.close')" (click)="close()"></button> <button type="button" class="btn-close text-reset" [attr.aria-label]="t('common.close')" (click)="close()"></button>
</div> </div>
@ -17,6 +18,14 @@
<div *ngIf="(externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0" class="text-muted muted mb-2"> <div *ngIf="(externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0" class="text-muted muted mb-2">
{{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}} {{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}}
</div> </div>
@if(isExternalSeries && externalSeries) {
<div class="text-muted muted mb-2">
{{t('series-preview-drawer.provided-by-label')}}
<img class="ms-1" [ngSrc]="externalSeries.provider | providerImage" width="20" height="20" alt="">
</div>
}
<app-read-more *ngIf="externalSeries.summary" [maxLength]="300" [text]="externalSeries.summary"></app-read-more> <app-read-more *ngIf="externalSeries.summary" [maxLength]="300" [text]="externalSeries.summary"></app-read-more>
<div class="mt-3"> <div class="mt-3">
@ -122,7 +131,7 @@
<app-loading [loading]="isLoading"></app-loading> <app-loading [loading]="isLoading"></app-loading>
<a class="btn btn-primary col-12 " [href]="url" target="_blank" rel="noopener noreferrer"> <a class="btn btn-primary col-12 mt-2" [href]="url" target="_blank" rel="noopener noreferrer">
{{t('series-preview-drawer.view-series')}} {{t('series-preview-drawer.view-series')}}
</a> </a>
</div> </div>

View File

@ -1,5 +1,5 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/external-series-detail"; import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/external-series-detail";
@ -16,11 +16,13 @@ import {PublicationStatusPipe} from "../../_pipes/publication-status.pipe";
import {SeriesMetadata} from "../../_models/metadata/series-metadata"; import {SeriesMetadata} from "../../_models/metadata/series-metadata";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {ActionService} from "../../_services/action.service"; import {ActionService} from "../../_services/action.service";
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
import {ScrobbleProvider} from "../../_services/scrobbling.service";
@Component({ @Component({
selector: 'app-series-preview-drawer', selector: 'app-series-preview-drawer',
standalone: true, standalone: true,
imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip], imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip, NgOptimizedImage, ProviderImagePipe],
templateUrl: './series-preview-drawer.component.html', templateUrl: './series-preview-drawer.component.html',
styleUrls: ['./series-preview-drawer.component.scss'], styleUrls: ['./series-preview-drawer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@ -59,7 +61,6 @@ export class SeriesPreviewDrawerComponent implements OnInit {
if (this.isExternalSeries) { if (this.isExternalSeries) {
this.seriesService.getExternalSeriesDetails(this.aniListId, this.malId).subscribe(externalSeries => { this.seriesService.getExternalSeriesDetails(this.aniListId, this.malId).subscribe(externalSeries => {
this.externalSeries = externalSeries; this.externalSeries = externalSeries;
this.isLoading = false; this.isLoading = false;
if (this.externalSeries.siteUrl) { if (this.externalSeries.siteUrl) {
this.url = this.externalSeries.siteUrl; this.url = this.externalSeries.siteUrl;

View File

@ -1,6 +1,6 @@
<ng-container *transloco="let t; read: 'bulk-operations'"> <ng-container *transloco="let t; read: 'bulk-operations'">
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount"> <ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top}}" [ngStyle]="{'margin-top': topOffset + 'px'}"> <div *ngIf="selectionCount > 0" class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top'}}" [ngStyle]="{'margin-top': topOffset + 'px'}">
<div class="d-flex justify-content-around align-items-center"> <div class="d-flex justify-content-around align-items-center">
<span class="highlight"> <span class="highlight">

View File

@ -1,30 +1,35 @@
<ng-container *transloco="let t; read: 'card-item'"> <ng-container *transloco="let t; read: 'card-item'">
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}"> <div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick($event)"> <div class="overlay" (click)="handleClick($event)">
<ng-container *ngIf="total > 0 || suppressArchiveWarning"> @if (total > 0 || suppressArchiveWarning) {
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageUrl"></app-image> <app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageUrl"></app-image>
</ng-container> } @else if (total === 0 && !suppressArchiveWarning) {
<ng-container *ngIf="total === 0 && !suppressArchiveWarning">
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image> <app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
</ng-container> }
<div class="progress-banner"> <div class="progress-banner">
<p *ngIf="read > 0 && read < total && total > 0 && read !== total" ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read"> @if (read > 0 && read < total && total > 0 && read !== total) {
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar> <p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read">
</p> <ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
</p>
}
<span class="download"> <span class="download">
<app-download-indicator [download$]="download$"></app-download-indicator> <app-download-indicator [download$]="download$"></app-download-indicator>
</span> </span>
</div> </div>
<div class="error-banner" *ngIf="total === 0 && !suppressArchiveWarning">
{{t('cannot-read')}}
</div>
<ng-container *ngIf="read === 0 && total > 0"> @if(total === 0 && !suppressArchiveWarning) {
<div class="error-banner">
{{t('cannot-read')}}
</div>
}
@if (read === 0 && total > 0) {
<div class="badge-container"> <div class="badge-container">
<div class="not-read-badge"></div> <div class="not-read-badge"></div>
</div> </div>
</ng-container> }
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection"> <div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection">
<input type="checkbox" class="form-check-input" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}"> <input type="checkbox" class="form-check-input" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
@ -34,14 +39,13 @@
<span class="badge bg-primary">{{count}}</span> <span class="badge bg-primary">{{count}}</span>
</div> </div>
<div class="card-overlay"></div> <div class="card-overlay"></div>
<ng-container *ngIf="overlayInformation | safeHtml as info"> @if (overlayInformation | safeHtml; as info) {
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="info !== '' || info !== undefined"> <div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="info !== '' || info !== undefined">
<div class="position-relative"> <div class="position-relative">
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span> <span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
</div> </div>
</div> </div>
</ng-container> }
</div> </div>
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0"> <div class="card-body" *ngIf="title.length > 0 || actions.length > 0">

View File

@ -38,7 +38,7 @@ import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe"; import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe"; import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {CommonModule} from "@angular/common"; import {CommonModule} from "@angular/common";
import {RouterLink} from "@angular/router"; import {RouterLink, RouterLinkActive} from "@angular/router";
import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoModule} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component"; import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
@ -61,7 +61,8 @@ import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
SentenceCasePipe, SentenceCasePipe,
RouterLink, RouterLink,
TranslocoModule, TranslocoModule,
SafeHtmlPipe SafeHtmlPipe,
RouterLinkActive
], ],
templateUrl: './card-item.component.html', templateUrl: './card-item.component.html',
styleUrls: ['./card-item.component.scss'], styleUrls: ['./card-item.component.scss'],

View File

@ -14,19 +14,21 @@
</ng-container> </ng-container>
<div class="card-overlay"></div> <div class="card-overlay"></div>
</div> </div>
<div class="card-body" *ngIf="data.name.length > 0"> @if (data.name.length > 0) {
<div> <div class="card-body">
<span class="card-title" placement="top" id="{{data.name}}" [ngbTooltip]="data.name" (click)="handleClick()" tabindex="0"> <div>
{{data.name}} <span class="card-title" placement="top" id="{{data.name}}" [ngbTooltip]="data.name" (click)="handleClick()" tabindex="0">
</span> <img class="me-1" [ngSrc]="data.provider | providerImage" width="20" height="20" alt="">
<span class="card-actions float-end"> {{data.name}}
<i class="fa fa-external-link-alt" aria-hidden="true"></i> </span>
</span> </div>
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">{{t('open-external')}}</a>
</div> </div>
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">{{t('open-external')}}</a> }
</div>
</div> </div>
</ng-container> </ng-container>

View File

@ -5,7 +5,7 @@ import {
Input, Input,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {ExternalSeries} from "../../_models/series-detail/external-series"; import {ExternalSeries} from "../../_models/series-detail/external-series";
import {RouterLinkActive} from "@angular/router"; import {RouterLinkActive} from "@angular/router";
import {ImageComponent} from "../../shared/image/image.component"; import {ImageComponent} from "../../shared/image/image.component";
@ -13,11 +13,13 @@ import {NgbOffcanvas, NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstr
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component"; import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
@Component({ @Component({
selector: 'app-external-series-card', selector: 'app-external-series-card',
standalone: true, standalone: true,
imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReactiveFormsModule, RouterLinkActive, TranslocoDirective], imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReactiveFormsModule, RouterLinkActive, TranslocoDirective, NgOptimizedImage, ProviderImagePipe, SafeHtmlPipe],
templateUrl: './external-series-card.component.html', templateUrl: './external-series-card.component.html',
styleUrls: ['./external-series-card.component.scss'], styleUrls: ['./external-series-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@ -30,6 +32,7 @@ export class ExternalSeriesCardComponent {
@Input() previewOnClick: boolean = false; @Input() previewOnClick: boolean = false;
@ViewChild('link', {static: false}) link!: ElementRef<HTMLAnchorElement>; @ViewChild('link', {static: false}) link!: ElementRef<HTMLAnchorElement>;
private readonly offcanvasService = inject(NgbOffcanvas); private readonly offcanvasService = inject(NgbOffcanvas);
handleClick() { handleClick() {

View File

@ -1,30 +1,37 @@
<ng-container *transloco="let t; read: 'carousel-reel'"> <ng-container *transloco="let t; read: 'carousel-reel'">
<div class="carousel-container" *ngIf="items.length > 0 || alwaysShow">
<div> @if(alwaysShow || items && items.length > 0) {
<h3 (click)="sectionClicked($event)" [ngClass]="{'non-selectable': !clickableTitle}"> <div class="carousel-container">
<a href="javascript:void(0)" class="section-title" >{{title}}</a> <div>
<i *ngIf="iconClasses !== ''" class="{{iconClasses}} title-icon ms-1" aria-hidden="true"></i> <h3 (click)="sectionClicked($event)" [ngClass]="{'non-selectable': !clickableTitle}">
</h3> <a href="javascript:void(0)" class="section-title" >{{title}}</a>
<div class="float-end" *ngIf="swiper"> <i *ngIf="iconClasses !== ''" class="{{iconClasses}} title-icon ms-1" aria-hidden="true"></i>
<button class="btn btn-icon" [disabled]="swiper.isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">{{t('prev-items')}}</span></button> </h3>
<button class="btn btn-icon" [disabled]="swiper.isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">{{t('next-items')}}</span></button> <div class="float-end" *ngIf="swiper">
<button class="btn btn-icon" [disabled]="swiper.isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">{{t('prev-items')}}</span></button>
<button class="btn btn-icon" [disabled]="swiper.isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">{{t('next-items')}}</span></button>
</div>
</div> </div>
@if (items.length > 0) {
<div>
<swiper
[slidesPerView]="'auto'"
(init)="onSwiper($event)"
[freeMode]="true">
<ng-template *ngFor="let item of items; index as i;" swiperSlide>
<ng-container [ngTemplateOutlet]="carouselItemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</ng-template>
<ng-container *ngIf="alwaysShow && items.length === 0">
<ng-template swiperSlide>
<ng-container [ngTemplateOutlet]="promptToAddTemplate"></ng-container>
</ng-template>
</ng-container>
</swiper>
</div>
}
</div> </div>
<div> }
<swiper
[slidesPerView]="'auto'"
(init)="onSwiper($event)"
[freeMode]="true">
<ng-template *ngFor="let item of items; index as i;" swiperSlide>
<ng-container [ngTemplateOutlet]="carouselItemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</ng-template>
<ng-container *ngIf="alwaysShow && items.length === 0">
<ng-template swiperSlide>
<ng-container [ngTemplateOutlet]="promptToAddTemplate"></ng-container>
</ng-template>
</ng-container>
</swiper>
</div>
</div>
</ng-container> </ng-container>

View File

@ -43,6 +43,9 @@
</app-side-nav-companion-bar> </app-side-nav-companion-bar>
</div> </div>
<app-bulk-operations [actionCallback]="bulkActionCallback" [topOffset]="56"></app-bulk-operations>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock> <div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
<div class="row mb-0 mb-xl-3 info-container"> <div class="row mb-0 mb-xl-3 info-container">
<div class="image-container col-4 col-sm-6 col-md-4 col-lg-4 col-xl-2 col-xxl-2 d-none d-sm-block mt-2"> <div class="image-container col-4 col-sm-6 col-md-4 col-lg-4 col-xl-2 col-xxl-2 d-none d-sm-block mt-2">
@ -120,7 +123,7 @@
</div> </div>
</div> </div>
<div class="row pt-4"> <div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata?.summary?.length === 0}">
<app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-alt')" <app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-alt')"
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}" iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
[clickableTitle]="true" (sectionClick)="openReviewModal()"> [clickableTitle]="true" (sectionClick)="openReviewModal()">
@ -132,7 +135,7 @@
</div> </div>
<ng-container *ngIf="series"> <ng-container *ngIf="series">
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)"> <ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)"> <li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
<a ngbNavLink>{{t('storyline-tab')}}</a> <a ngbNavLink>{{t('storyline-tab')}}</a>

View File

@ -1,4 +1,14 @@
import {DecimalPipe, DOCUMENT, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common'; import {
DecimalPipe,
DOCUMENT,
NgClass,
NgFor,
NgIf, NgOptimizedImage,
NgStyle,
NgSwitch,
NgSwitchCase,
NgTemplateOutlet
} from '@angular/common';
import { import {
AfterContentChecked, AfterContentChecked,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -94,6 +104,7 @@ import {
import {PublicationStatus} from "../../../_models/metadata/publication-status"; import {PublicationStatus} from "../../../_models/metadata/publication-status";
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter"; import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component"; import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
interface RelatedSeriesPair { interface RelatedSeriesPair {
series: Series; series: Series;
@ -121,13 +132,43 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'], styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent] imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent, NgClass, NgOptimizedImage, ProviderImagePipe]
}) })
export class SeriesDetailComponent implements OnInit, AfterContentChecked { export class SeriesDetailComponent implements OnInit, AfterContentChecked {
private readonly destroyRef = inject(DestroyRef);
private readonly route = inject(ActivatedRoute);
private readonly seriesService = inject(SeriesService);
private readonly router = inject(Router);
private readonly modalService = inject(NgbModal);
private readonly toastr = inject(ToastrService);
private readonly accountService = inject(AccountService);
private readonly actionFactoryService = inject(ActionFactoryService);
private readonly libraryService = inject(LibraryService);
private readonly titleService = inject(Title);
private readonly downloadService = inject(DownloadService);
private readonly actionService = inject(ActionService);
private readonly messageHub = inject(MessageHubService);
private readonly readingListService = inject(ReadingListService);
private readonly offcanvasService = inject(NgbOffcanvas);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly scrollService = inject(ScrollService);
private readonly deviceService = inject(DeviceService);
private readonly translocoService = inject(TranslocoService);
protected readonly bulkSelectionService = inject(BulkSelectionService);
protected readonly utilityService = inject(UtilityService);
protected readonly imageService = inject(ImageService);
protected readonly navService = inject(NavService);
protected readonly readerService = inject(ReaderService);
protected readonly LibraryType = LibraryType;
protected readonly PageLayoutMode = PageLayoutMode;
protected readonly TabID = TabID;
protected readonly TagBadgeCursor = TagBadgeCursor;
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined; @ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined; @ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
private readonly destroyRef = inject(DestroyRef);
/** /**
* Series Id. Set at load before UI renders * Series Id. Set at load before UI renders
@ -269,14 +310,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} }
} }
get TagBadgeCursor() { return TagBadgeCursor; }
get TabID() { return TabID; }
get PageLayoutMode() { return PageLayoutMode; }
get LibraryType() { return LibraryType; }
get ScrollingBlockHeight() { get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)'; if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement; const navbar = this.document.querySelector('.navbar') as HTMLElement;
@ -285,6 +318,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
const companionHeight = this.companionBar!.nativeElement.offsetHeight; const companionHeight = this.companionBar!.nativeElement.offsetHeight;
const navbarHeight = navbar.offsetHeight; const navbarHeight = navbar.offsetHeight;
const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding
console.log('compainionHeight: ', companionHeight)
console.log('navbarHeight: ', navbarHeight)
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)'; return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
} }
@ -308,20 +343,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
return this.currentlyReadingChapter.title; return this.currentlyReadingChapter.title;
} }
constructor(private route: ActivatedRoute, private seriesService: SeriesService, constructor(@Inject(DOCUMENT) private document: Document) {
private router: Router, public bulkSelectionService: BulkSelectionService,
private modalService: NgbModal, public readerService: ReaderService,
public utilityService: UtilityService, private toastr: ToastrService,
private accountService: AccountService, public imageService: ImageService,
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService,
private messageHub: MessageHubService, private readingListService: ReadingListService,
public navService: NavService,
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
private cdRef: ChangeDetectorRef, private scrollService: ScrollService,
private deviceService: DeviceService, private translocoService: TranslocoService
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) { if (user) {

View File

@ -62,10 +62,11 @@
"review-series-modal": { "review-series-modal": {
"title": "Edit Review", "title": "Edit Review",
"tagline-label": "Tagline",
"review-label": "Review", "review-label": "Review",
"close": "{{common.close}}", "close": "{{common.close}}",
"save": "{{common.save}}" "save": "{{common.save}}",
"min-length": "Review must be at least {{count}} characters",
"required": "{{validation.required-field}}"
}, },
"review-card-modal": { "review-card-modal": {
@ -78,7 +79,7 @@
"review-card": { "review-card": {
"your-review": "This is your review", "your-review": "This is your review",
"external-review": "External Review", "external-review": "External Review",
"local-review": "Review", "local-review": "Local Review",
"rating-percentage": "Rating {{r}}%" "rating-percentage": "Rating {{r}}%"
}, },
@ -1716,7 +1717,8 @@
"view-series": "View Series", "view-series": "View Series",
"vols-and-chapters": "{{volCount}} Volumes / {{chpCount}} Chapters", "vols-and-chapters": "{{volCount}} Volumes / {{chpCount}} Chapters",
"remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}", "remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}",
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}" "add-to-want-to-read": "{{actionable.add-to-want-to-read}}",
"provided-by-label": "Provided by"
}, },
"next-expected-card": { "next-expected-card": {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.11.4" "version": "0.7.11.5"
}, },
"servers": [ "servers": [
{ {
@ -14868,6 +14868,16 @@
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
"nullable": true "nullable": true
},
"provider": {
"enum": [
0,
1,
2
],
"type": "integer",
"description": "Misleading name but is the source of data (like a review coming from AniList)",
"format": "int32"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -19547,11 +19557,6 @@
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
}, },
"tagline": {
"maxLength": 120,
"type": "string",
"nullable": true
},
"body": { "body": {
"type": "string", "type": "string",
"nullable": true "nullable": true