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")]
|
||||
public async Task<ActionResult<string>> ResetApiKey()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) throw new KavitaUnauthenticatedUserException();
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException();
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate,
|
||||
MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
return Ok(user.ApiKey);
|
||||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
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)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var username = User.GetUsername();
|
||||
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();
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
{
|
||||
@ -139,7 +141,7 @@ public class ReviewController : BaseApiController
|
||||
var rating = ratingBuilder
|
||||
.WithBody(dto.Body)
|
||||
.WithSeriesId(dto.SeriesId)
|
||||
.WithTagline(dto.Tagline)
|
||||
.WithTagline(string.Empty)
|
||||
.Build();
|
||||
|
||||
if (rating.Id == 0)
|
||||
@ -152,7 +154,7 @@ public class ReviewController : BaseApiController
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.DTOs.Recommendation;
|
||||
#nullable enable
|
||||
@ -19,4 +20,5 @@ public class ExternalSeriesDetailDto
|
||||
public string? Summary { get; set; }
|
||||
public int? VolumeCount { 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
|
||||
|
||||
public class ExternalSeriesDto
|
||||
@ -9,4 +11,5 @@ public class ExternalSeriesDto
|
||||
public string? Summary { get; set; }
|
||||
public int? AniListId { 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 int SeriesId { get; set; }
|
||||
[MaxLength(120)]
|
||||
public string? Tagline { get; set; }
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ public class UserReviewDto
|
||||
/// <summary>
|
||||
/// A tagline for the review
|
||||
/// </summary>
|
||||
/// <remarks>This is not possible to set as a local user</remarks>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -28,6 +28,7 @@ public class RatingBuilder : IEntityBuilder<AppUserRating>
|
||||
|
||||
public RatingBuilder WithTagline(string? tagline)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tagline)) return this;
|
||||
_rating.Tagline = tagline;
|
||||
return this;
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ public interface IScrobblingService
|
||||
Task CheckExternalAccessTokens();
|
||||
Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider);
|
||||
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 ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead);
|
||||
|
||||
@ -185,8 +185,10 @@ public class ScrobblingService : IScrobblingService
|
||||
} ?? 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;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
} else if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount)
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
break;
|
||||
default:
|
||||
// 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) {
|
||||
this.toast(genericError);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
|
||||
export enum PlusMediaFormat {
|
||||
Manga = 1,
|
||||
Comic = 2,
|
||||
@ -37,5 +39,5 @@ export interface ExternalSeriesDetail {
|
||||
chapterCount?: number;
|
||||
staff: Array<SeriesStaff>;
|
||||
tags: Array<MetadataTagDto>;
|
||||
|
||||
provider: ScrobbleProvider;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
|
||||
export interface ExternalSeries {
|
||||
name: string;
|
||||
coverUrl: string;
|
||||
@ -5,4 +7,5 @@ export interface ExternalSeries {
|
||||
summary: string;
|
||||
aniListId?: number;
|
||||
malId?: number;
|
||||
provider: ScrobbleProvider;
|
||||
}
|
||||
|
@ -213,9 +213,9 @@ export class SeriesService {
|
||||
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', {
|
||||
seriesId, tagline, body
|
||||
seriesId, body
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
<ng-container *transloco="let t; read:'review-card-modal'">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
|
@ -1,24 +1,26 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Component, inject,
|
||||
Inject,
|
||||
Input, ViewChild,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {CommonModule, DOCUMENT, NgOptimizedImage} from '@angular/common';
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {UserReview} from "../review-card/user-review";
|
||||
import {SpoilerComponent} from "../spoiler/spoiler.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-review-card-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective],
|
||||
imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective, DefaultValuePipe, NgOptimizedImage, ProviderImagePipe],
|
||||
templateUrl: './review-card-modal.component.html',
|
||||
styleUrls: ['./review-card-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -26,12 +28,13 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
||||
})
|
||||
export class ReviewCardModalComponent implements AfterViewInit {
|
||||
|
||||
private modal = inject(NgbActiveModal);
|
||||
|
||||
@Input({required: true}) review!: UserReview;
|
||||
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, @Inject(DOCUMENT) private document: Document) {
|
||||
}
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
|
@ -10,11 +10,8 @@
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title" [title]="review.tagline">
|
||||
<ng-container *ngIf="review.tagline && review.tagline.length > 0; else noTagline">{{review.tagline.substring(0, 29)}}{{review.tagline.length > 29 ? '…' : ''}}</ng-container>
|
||||
<ng-template #noTagline>
|
||||
{{review.isExternal ? t('external-review') : t('local-review')}}
|
||||
</ng-template>
|
||||
<h6 class="card-title">
|
||||
{{review.isExternal ? t('external-review') : t('local-review')}}
|
||||
</h6>
|
||||
<p class="card-text no-images">
|
||||
<app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="100" [showToggle]="false"></app-read-more>
|
||||
@ -23,9 +20,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent text-muted">
|
||||
<div class="review-user">
|
||||
<div>
|
||||
<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>
|
||||
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||
{{review.username}}
|
||||
</ng-container>
|
||||
<ng-template #normalReview>
|
||||
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
|
@ -39,7 +39,7 @@
|
||||
.card-footer {
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
max-width: 305px;
|
||||
max-width: 319px;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-review-card',
|
||||
@ -20,9 +21,11 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReviewCardComponent implements OnInit {
|
||||
private readonly accountService = inject(AccountService);
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
@Input({required: true}) review!: UserReview;
|
||||
private readonly accountService = inject(AccountService);
|
||||
|
||||
isMyReview: boolean = false;
|
||||
|
||||
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'});
|
||||
ref.componentInstance.review = this.review;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,14 +8,19 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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">
|
||||
<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>
|
||||
</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 {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
@ -9,22 +9,24 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
||||
@Component({
|
||||
selector: 'app-review-series-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective],
|
||||
imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective],
|
||||
templateUrl: './review-series-modal.component.html',
|
||||
styleUrls: ['./review-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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;
|
||||
reviewGroup!: FormGroup;
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reviewGroup = new FormGroup({
|
||||
tagline: new FormControl(this.review.tagline || '', [Validators.min(20), Validators.max(120)]),
|
||||
reviewBody: new FormControl(this.review.body, [Validators.min(20)]),
|
||||
reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
@ -35,7 +37,10 @@ export class ReviewSeriesModalComponent implements OnInit {
|
||||
|
||||
save() {
|
||||
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});
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
{{name}}
|
||||
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" [attr.aria-label]="t('common.close')" (click)="close()"></button>
|
||||
</div>
|
||||
@ -17,6 +18,14 @@
|
||||
<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})}}
|
||||
</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>
|
||||
|
||||
<div class="mt-3">
|
||||
@ -122,7 +131,7 @@
|
||||
|
||||
<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')}}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
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 {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-preview-drawer',
|
||||
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',
|
||||
styleUrls: ['./series-preview-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -59,7 +61,6 @@ export class SeriesPreviewDrawerComponent implements OnInit {
|
||||
if (this.isExternalSeries) {
|
||||
this.seriesService.getExternalSeriesDetails(this.aniListId, this.malId).subscribe(externalSeries => {
|
||||
this.externalSeries = externalSeries;
|
||||
|
||||
this.isLoading = false;
|
||||
if (this.externalSeries.siteUrl) {
|
||||
this.url = this.externalSeries.siteUrl;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; read: 'bulk-operations'">
|
||||
<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">
|
||||
|
||||
<span class="highlight">
|
||||
|
@ -1,30 +1,35 @@
|
||||
<ng-container *transloco="let t; read: 'card-item'">
|
||||
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
|
||||
<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>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="total === 0 && !suppressArchiveWarning">
|
||||
} @else if (total === 0 && !suppressArchiveWarning) {
|
||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<div class="progress-banner">
|
||||
<p *ngIf="read > 0 && read < total && total > 0 && read !== total" ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read">
|
||||
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
|
||||
</p>
|
||||
@if (read > 0 && read < total && total > 0 && read !== total) {
|
||||
<p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read">
|
||||
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
|
||||
</p>
|
||||
}
|
||||
|
||||
<span class="download">
|
||||
<app-download-indicator [download$]="download$"></app-download-indicator>
|
||||
</span>
|
||||
</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="not-read-badge"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<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}">
|
||||
@ -34,14 +39,13 @@
|
||||
<span class="badge bg-primary">{{count}}</span>
|
||||
</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="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
<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 {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {RouterLink, RouterLinkActive} from "@angular/router";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
@ -61,7 +61,8 @@ import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
SentenceCasePipe,
|
||||
RouterLink,
|
||||
TranslocoModule,
|
||||
SafeHtmlPipe
|
||||
SafeHtmlPipe,
|
||||
RouterLinkActive
|
||||
],
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
|
@ -14,19 +14,21 @@
|
||||
</ng-container>
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-body" *ngIf="data.name.length > 0">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{data.name}}" [ngbTooltip]="data.name" (click)="handleClick()" tabindex="0">
|
||||
{{data.name}}
|
||||
</span>
|
||||
<span class="card-actions float-end">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
|
||||
</span>
|
||||
@if (data.name.length > 0) {
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{data.name}}" [ngbTooltip]="data.name" (click)="handleClick()" tabindex="0">
|
||||
<img class="me-1" [ngSrc]="data.provider | providerImage" width="20" height="20" alt="">
|
||||
{{data.name}}
|
||||
</span>
|
||||
</div>
|
||||
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">{{t('open-external')}}</a>
|
||||
</div>
|
||||
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">{{t('open-external')}}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Input,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {ExternalSeries} from "../../_models/series-detail/external-series";
|
||||
import {RouterLinkActive} from "@angular/router";
|
||||
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 {TranslocoDirective} from "@ngneat/transloco";
|
||||
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({
|
||||
selector: 'app-external-series-card',
|
||||
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',
|
||||
styleUrls: ['./external-series-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -30,6 +32,7 @@ export class ExternalSeriesCardComponent {
|
||||
@Input() previewOnClick: boolean = false;
|
||||
@ViewChild('link', {static: false}) link!: ElementRef<HTMLAnchorElement>;
|
||||
|
||||
|
||||
private readonly offcanvasService = inject(NgbOffcanvas);
|
||||
|
||||
handleClick() {
|
||||
|
@ -1,30 +1,37 @@
|
||||
<ng-container *transloco="let t; read: 'carousel-reel'">
|
||||
<div class="carousel-container" *ngIf="items.length > 0 || alwaysShow">
|
||||
<div>
|
||||
<h3 (click)="sectionClicked($event)" [ngClass]="{'non-selectable': !clickableTitle}">
|
||||
<a href="javascript:void(0)" class="section-title" >{{title}}</a>
|
||||
<i *ngIf="iconClasses !== ''" class="{{iconClasses}} title-icon ms-1" aria-hidden="true"></i>
|
||||
</h3>
|
||||
<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>
|
||||
|
||||
@if(alwaysShow || items && items.length > 0) {
|
||||
<div class="carousel-container">
|
||||
<div>
|
||||
<h3 (click)="sectionClicked($event)" [ngClass]="{'non-selectable': !clickableTitle}">
|
||||
<a href="javascript:void(0)" class="section-title" >{{title}}</a>
|
||||
<i *ngIf="iconClasses !== ''" class="{{iconClasses}} title-icon ms-1" aria-hidden="true"></i>
|
||||
</h3>
|
||||
<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>
|
||||
@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>
|
||||
<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>
|
||||
|
@ -43,6 +43,9 @@
|
||||
</app-side-nav-companion-bar>
|
||||
</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 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">
|
||||
@ -120,7 +123,7 @@
|
||||
</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')"
|
||||
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
|
||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
||||
@ -132,7 +135,7 @@
|
||||
</div>
|
||||
|
||||
<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)">
|
||||
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
|
||||
<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 {
|
||||
AfterContentChecked,
|
||||
ChangeDetectionStrategy,
|
||||
@ -94,6 +104,7 @@ import {
|
||||
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
||||
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
||||
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
@ -121,13 +132,43 @@ interface StoryLineItem {
|
||||
styleUrls: ['./series-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
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 {
|
||||
|
||||
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('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
|
||||
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 navbarHeight = navbar.offsetHeight;
|
||||
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)';
|
||||
}
|
||||
|
||||
@ -308,20 +343,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
return this.currentlyReadingChapter.title;
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute, private seriesService: SeriesService,
|
||||
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
|
||||
) {
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
|
@ -62,10 +62,11 @@
|
||||
|
||||
"review-series-modal": {
|
||||
"title": "Edit Review",
|
||||
"tagline-label": "Tagline",
|
||||
"review-label": "Review",
|
||||
"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": {
|
||||
@ -78,7 +79,7 @@
|
||||
"review-card": {
|
||||
"your-review": "This is your review",
|
||||
"external-review": "External Review",
|
||||
"local-review": "Review",
|
||||
"local-review": "Local Review",
|
||||
"rating-percentage": "Rating {{r}}%"
|
||||
},
|
||||
|
||||
@ -1716,7 +1717,8 @@
|
||||
"view-series": "View Series",
|
||||
"vols-and-chapters": "{{volCount}} Volumes / {{chpCount}} Chapters",
|
||||
"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": {
|
||||
|
17
openapi.json
17
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.11.4"
|
||||
"version": "0.7.11.5"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -14868,6 +14868,16 @@
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"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
|
||||
@ -19547,11 +19557,6 @@
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"tagline": {
|
||||
"maxLength": 120,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
|
Loading…
x
Reference in New Issue
Block a user