mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
New Year Bugs (#2513)
This commit is contained in:
parent
fcacd67d71
commit
5dfcccba7a
@ -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"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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();
|
||||||
|
@ -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="">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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'],
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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": {
|
||||||
|
17
openapi.json
17
openapi.json
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user