Polish Round 2 (#2411)

This commit is contained in:
Joe Milazzo 2023-11-07 17:42:17 -06:00 committed by GitHub
parent ba3e760b31
commit a2fd87c454
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 213 additions and 95 deletions

View File

@ -687,7 +687,7 @@ public class AccountController : BaseApiController
if (!_emailService.IsValidEmail(dto.Email)) if (!_emailService.IsValidEmail(dto.Email))
{ {
_logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email); _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email.Replace(Environment.NewLine, string.Empty));
return Ok(new InviteUserResponse return Ok(new InviteUserResponse
{ {
EmailLink = emailLink, EmailLink = emailLink,

View File

@ -59,7 +59,7 @@ public class CollectionController : BaseApiController
/// <returns></returns> /// <returns></returns>
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpGet("search")] [HttpGet("search")]
public async Task<ActionResult<IEnumerable<CollectionTagDto>>> SearchTags(string queryString) public async Task<ActionResult<IEnumerable<CollectionTagDto>>> SearchTags(string? queryString)
{ {
queryString ??= string.Empty; queryString ??= string.Empty;
queryString = queryString.Replace(@"%", string.Empty); queryString = queryString.Replace(@"%", string.Empty);

View File

@ -134,7 +134,7 @@ public class LibraryController : BaseApiController
/// <returns></returns> /// <returns></returns>
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpGet("list")] [HttpGet("list")]
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path) public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string? path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
@ -385,7 +385,7 @@ public class LibraryController : BaseApiController
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, await _localizationService.Translate(User.GetUserId(), "generic-library")); _logger.LogError(ex, "There was a critical issue. Please try again");
await _unitOfWork.RollbackAsync(); await _unitOfWork.RollbackAsync();
return Ok(false); return Ok(false);
} }
@ -441,7 +441,7 @@ public class LibraryController : BaseApiController
// Override Scrobbling for Comic libraries since there are no providers to scrobble to // Override Scrobbling for Comic libraries since there are no providers to scrobble to
if (library.Type == LibraryType.Comic) if (library.Type == LibraryType.Comic)
{ {
_logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name); _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty));
library.AllowScrobbling = false; library.AllowScrobbling = false;
} }
@ -471,7 +471,11 @@ public class LibraryController : BaseApiController
} }
/// <summary>
/// Returns the type of the underlying library
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
[HttpGet("type")] [HttpGet("type")]
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId) public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
{ {

View File

@ -46,7 +46,7 @@ public class PluginController : BaseApiController
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0) if (userId <= 0)
{ {
_logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", Uri.EscapeDataString(pluginName), new _logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new
{ {
IpAddress = ipAddress, IpAddress = ipAddress,
UserAgent = userAgent, UserAgent = userAgent,
@ -55,7 +55,7 @@ public class PluginController : BaseApiController
throw new KavitaUnauthenticatedUserException(); throw new KavitaUnauthenticatedUserException();
} }
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", Uri.EscapeDataString(pluginName), user!.UserName, userId); _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
return new UserDto return new UserDto
{ {
Username = user.UserName!, Username = user.UserName!,

View File

@ -230,6 +230,8 @@ public class ReaderController : BaseApiController
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan")); if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan"));
var mangaFile = chapter.Files.First(); var mangaFile = chapter.Files.First();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, User.GetUserId());
var info = new ChapterInfoDto() var info = new ChapterInfoDto()
{ {
ChapterNumber = dto.ChapterNumber, ChapterNumber = dto.ChapterNumber,
@ -242,6 +244,8 @@ public class ReaderController : BaseApiController
LibraryId = dto.LibraryId, LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial, IsSpecial = dto.IsSpecial,
Pages = dto.Pages, Pages = dto.Pages,
SeriesTotalPages = series?.Pages ?? 0,
SeriesTotalPagesRead = series?.PagesRead ?? 0,
ChapterTitle = dto.ChapterTitle ?? string.Empty, ChapterTitle = dto.ChapterTitle ?? string.Empty,
Subtitle = string.Empty, Subtitle = string.Empty,
Title = dto.SeriesName, Title = dto.SeriesName,
@ -266,8 +270,7 @@ public class ReaderController : BaseApiController
} }
else else
{ {
//info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber); info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
info.Subtitle = $"Volume {info.VolumeNumber}";
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter)) if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
{ {
info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) + info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) +

View File

@ -143,7 +143,7 @@ public class SeriesController : BaseApiController
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto) public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
{ {
var username = User.GetUsername(); var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); _logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(); if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();

View File

@ -65,6 +65,14 @@ public class ChapterInfoDto : IChapterInfoDto
/// </summary> /// </summary>
/// <remarks>Usually just series name, but can include chapter title</remarks> /// <remarks>Usually just series name, but can include chapter title</remarks>
public string Title { get; set; } = default!; public string Title { get; set; } = default!;
/// <summary>
/// Total pages for the series
/// </summary>
public int SeriesTotalPages { get; set; }
/// <summary>
/// Total pages read for the series
/// </summary>
public int SeriesTotalPagesRead { get; set; }
/// <summary> /// <summary>
/// List of all files with their inner archive structure maintained in filename and dimensions /// List of all files with their inner archive structure maintained in filename and dimensions
@ -76,5 +84,4 @@ public class ChapterInfoDto : IChapterInfoDto
/// </summary> /// </summary>
/// <remarks>This is optionally returned by includeDimensions</remarks> /// <remarks>This is optionally returned by includeDimensions</remarks>
public IDictionary<int, int>? DoublePairs { get; set; } public IDictionary<int, int>? DoublePairs { get; set; }
} }

View File

@ -369,6 +369,7 @@ public class DirectoryService : IDirectoryService
/// <returns></returns> /// <returns></returns>
public void ClearDirectory(string directoryPath) public void ClearDirectory(string directoryPath)
{ {
directoryPath = directoryPath.Replace(Environment.NewLine, string.Empty);
var di = FileSystem.DirectoryInfo.New(directoryPath); var di = FileSystem.DirectoryInfo.New(directoryPath);
if (!di.Exists) return; if (!di.Exists) return;
try try

View File

@ -219,7 +219,7 @@ public class ImageService : IImageService
{ {
// Parse the URL to get the domain (including subdomain) // Parse the URL to get the domain (including subdomain)
var uri = new Uri(url); var uri = new Uri(url);
var domain = uri.Host; var domain = uri.Host.Replace(Environment.NewLine, string.Empty);
var baseUrl = uri.Scheme + "://" + uri.Host; var baseUrl = uri.Scheme + "://" + uri.Host;

View File

@ -158,10 +158,14 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
{ {
// This compares if it's changed since a file scan only // This compares if it's changed since a file scan only
var firstFile = chapter.Files.FirstOrDefault(); var firstFile = chapter.Files.FirstOrDefault();
if (firstFile == null) return; if (firstFile == null || !_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis,
if (!_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate, forceUpdate,
firstFile)) firstFile))
{
volume.WordCount += chapter.WordCount;
series.WordCount += chapter.WordCount;
continue; continue;
}
if (series.Format == MangaFormat.Epub) if (series.Format == MangaFormat.Epub)
{ {

View File

@ -306,7 +306,7 @@ public class ProcessSeries : IProcessSeries
if (!series.Metadata.PublicationStatusLocked) if (!series.Metadata.PublicationStatusLocked)
{ {
series.Metadata.PublicationStatus = PublicationStatus.OnGoing; series.Metadata.PublicationStatus = PublicationStatus.OnGoing;
if (series.Metadata.MaxCount >= series.Metadata.TotalCount && series.Metadata.TotalCount > 0) if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0)
{ {
series.Metadata.PublicationStatus = PublicationStatus.Completed; series.Metadata.PublicationStatus = PublicationStatus.Completed;
} else if (series.Metadata.TotalCount > 0) } else if (series.Metadata.TotalCount > 0)

View File

@ -9,7 +9,7 @@
</ng-container> </ng-container>
<div class="progress-banner"> <div class="progress-banner">
<p *ngIf="read < total && total > 0 && read !== total" ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read"> <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> <ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
</p> </p>
<span class="download"> <span class="download">

View File

@ -38,6 +38,7 @@ $image-width: 160px;
display: block; display: block;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0px; margin-bottom: 0px;
text-align: center;
} }
.selected-highlight { .selected-highlight {

View File

@ -229,7 +229,7 @@ export class CardItemComponent implements OnInit {
if (nextDate.expectedDate) { if (nextDate.expectedDate) {
const utcPipe = new UtcToLocalTimePipe(); const utcPipe = new UtcToLocalTimePipe();
this.title = utcPipe.transform(nextDate.expectedDate); this.title = utcPipe.transform(nextDate.expectedDate, 'shortDate');
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -13,11 +13,13 @@
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly"> <div class="d-flex justify-content-evenly">
<a style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a> <a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span> <span class="ps-1 pe-1"></span>
<span style="padding-right:0px" href="javascript:void(0)">{{t('drag-n-drop')}}</span> <span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1" style="padding-right:0px"></span> <span class="ps-1 pe-1"></span>
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a> <a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnInit,
Output
} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop'; import {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop';
import { fromEvent, Subject } from 'rxjs'; import { fromEvent, Subject } from 'rxjs';
@ -25,7 +35,14 @@ import {translate, TranslocoModule} from "@ngneat/transloco";
styleUrls: ['./cover-image-chooser.component.scss'], styleUrls: ['./cover-image-chooser.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CoverImageChooserComponent implements OnInit, OnDestroy { export class CoverImageChooserComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly cdRef = inject(ChangeDetectorRef);
public readonly imageService = inject(ImageService);
public readonly fb = inject(FormBuilder);
public readonly toastr = inject(ToastrService);
public readonly uploadService = inject(UploadService);
/** /**
* If buttons show under images to allow immediate selection of cover images. * If buttons show under images to allow immediate selection of cover images.
@ -70,10 +87,8 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(','); acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(',');
mode: 'file' | 'url' | 'all' = 'all'; mode: 'file' | 'url' | 'all' = 'all';
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private fb: FormBuilder, private toastr: ToastrService, private uploadService: UploadService, constructor(@Inject(DOCUMENT) private document: Document) { }
@Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.form = this.fb.group({ this.form = this.fb.group({
@ -83,10 +98,6 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
/** /**
* Generates a base64 encoding for an Image. Used in manual file upload flow. * Generates a base64 encoding for an Image. Used in manual file upload flow.

View File

@ -46,6 +46,14 @@ import {TranslocoDirective} from "@ngneat/transloco";
}) })
export class SeriesInfoCardsComponent implements OnInit, OnChanges { export class SeriesInfoCardsComponent implements OnInit, OnChanges {
private readonly destroyRef = inject(DestroyRef);
public readonly utilityService = inject(UtilityService);
private readonly readerService = inject(ReaderService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly messageHub = inject(MessageHubService);
public readonly accountService = inject(AccountService);
private readonly scrobbleService = inject(ScrobblingService);
@Input({required: true}) series!: Series; @Input({required: true}) series!: Series;
@Input({required: true}) seriesMetadata!: SeriesMetadata; @Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false; @Input() hasReadingProgress: boolean = false;
@ -59,19 +67,13 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges {
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0}; readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
isScrobbling: boolean = true; isScrobbling: boolean = true;
libraryAllowsScrobbling: boolean = true; libraryAllowsScrobbling: boolean = true;
private readonly destroyRef = inject(DestroyRef);
get MangaFormat() {
return MangaFormat;
}
get FilterField() { protected readonly MangaFormat = MangaFormat;
return FilterField; protected readonly FilterField = FilterField;
}
constructor(public utilityService: UtilityService, private readerService: ReaderService,
private readonly cdRef: ChangeDetectorRef, private messageHub: MessageHubService, constructor() {
public accountService: AccountService, private scrobbleService: ScrobblingService) {
// Listen for progress events and re-calculate getTimeLeft // Listen for progress events and re-calculate getTimeLeft
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate), this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
map(evt => evt.payload as UserProgressUpdateEvent), map(evt => evt.payload as UserProgressUpdateEvent),

View File

@ -26,12 +26,14 @@
<span class="visually-hidden">{{t('continuous-reading-prev-chapter-alt')}}</span> <span class="visually-hidden">{{t('continuous-reading-prev-chapter-alt')}}</span>
</div> </div>
</div> </div>
<ng-container *ngFor="let item of webtoonImages | async; let index = index;"> <ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block" <img src="{{item.src}}" style="display: block"
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}} {{initFinished ? '' : 'full-opacity'}}" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}} {{initFinished ? '' : 'full-opacity'}}"
*ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" *ngIf="item.page >= pageNum - bufferPages && item.page <= pageNum + bufferPages" rel="nofollow" alt="image"
(load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;"> (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
</ng-container> </ng-container>
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()"> <div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
<div> <div>
<button class="btn btn-icon mx-auto"> <button class="btn btn-icon mx-auto">

View File

@ -24,6 +24,7 @@ import { WebtoonImage } from '../../_models/webtoon-image';
import { ManagaReaderService } from '../../_service/managa-reader.service'; import { ManagaReaderService } from '../../_service/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {MangaReaderComponent} from "../manga-reader/manga-reader.component";
/** /**
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load * How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
@ -62,6 +63,12 @@ const enum DEBUG_MODES {
}) })
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
private readonly mangaReaderService = inject(ManagaReaderService);
private readonly readerService = inject(ReaderService);
private readonly renderer = inject(Renderer2);
private readonly scrollService = inject(ScrollService);
private readonly cdRef = inject(ChangeDetectorRef);
/** /**
* Current page number aka what's recorded on screen * Current page number aka what's recorded on screen
*/ */
@ -150,7 +157,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output * Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output
*/ */
debugMode: DEBUG_MODES = DEBUG_MODES.None; debugMode: DEBUG_MODES = DEBUG_MODES.Outline;
/** /**
* Debug mode. Will filter out any messages in here so they don't hit the log * Debug mode. Will filter out any messages in here so they don't hit the log
*/ */
@ -169,9 +176,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth); return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
} }
constructor(private readerService: ReaderService, private renderer: Renderer2, constructor(@Inject(DOCUMENT) private readonly document: Document) {
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
// This will always exist at this point in time since this is used within manga reader // This will always exist at this point in time since this is used within manga reader
const reader = document.querySelector('.reading-area'); const reader = document.querySelector('.reading-area');
if (reader !== null) { if (reader !== null) {
@ -200,6 +205,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll') fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef)) .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEvent(event)); .subscribe((event) => this.handleScrollEvent(event));
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEndEvent(event));
} }
ngOnInit(): void { ngOnInit(): void {
@ -307,6 +316,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.checkIfShouldTriggerContinuousReader(); this.checkIfShouldTriggerContinuousReader();
} }
handleScrollEndEvent(event?: any) {
if (!this.isScrolling) {
const closestImages = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
const img = this.findClosestVisibleImage(closestImages);
if (img != null) {
this.setPageNum(parseInt(img.getAttribute('page') || this.pageNum + '', 10));
}
}
}
getTotalHeight() { getTotalHeight() {
let totalHeight = 0; let totalHeight = 0;
document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height); document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height);
@ -423,10 +444,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (elem === null || elem === undefined) { return false; } if (elem === null || elem === undefined) { return false; }
const rect = elem.getBoundingClientRect(); const rect = elem.getBoundingClientRect();
const [innerHeight, innerWidth] = this.getInnerDimensions(); const [innerHeight, innerWidth] = this.getInnerDimensions();
if (rect.bottom >= 0 && if (rect.bottom >= 0 &&
rect.right >= 0 && rect.right >= 0 &&
rect.top <= (innerHeight || document.body.clientHeight) && rect.top <= (innerHeight || document.body.clientHeight) &&
@ -438,6 +457,32 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return false; return false;
} }
/**
* Find the closest visible image within the viewport.
* @param images An array of HTML Image Elements
* @returns Closest visible image or null if none are visible
*/
findClosestVisibleImage(images: HTMLImageElement[]): HTMLImageElement | null {
let closestImage: HTMLImageElement | null = null;
let closestDistanceToTop = Number.MAX_VALUE; // Initialize to a high value.
for (const image of images) {
// Get the bounding rectangle of the image.
const rect = image.getBoundingClientRect();
// Calculate the distance of the current image to the top of the viewport.
const distanceToTop = Math.abs(rect.top);
// Check if the image is visible within the viewport.
if (distanceToTop < closestDistanceToTop) {
closestDistanceToTop = distanceToTop;
closestImage = image;
}
}
return closestImage;
}
initWebtoonReader() { initWebtoonReader() {
this.initFinished = false; this.initFinished = false;
@ -449,6 +494,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies(); const [startingIndex, endingIndex] = this.calculatePrefetchIndecies();
this.debugLog('[INIT] Prefetching pages ' + startingIndex + ' to ' + endingIndex + '. Current page: ', this.pageNum); this.debugLog('[INIT] Prefetching pages ' + startingIndex + ' to ' + endingIndex + '. Current page: ', this.pageNum);
for(let i = startingIndex; i <= endingIndex; i++) { for(let i = startingIndex; i <= endingIndex; i++) {
this.loadWebtoonImage(i); this.loadWebtoonImage(i);

View File

@ -10,7 +10,7 @@
<div> <div>
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-alt')">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-title')}}</span>)</span></div> <div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-alt')">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-title')}}</span>)</span></div>
<div class="subtitle"> <div class="subtitle">
{{subtitle}} {{subtitle}} <span *ngIf="totalSeriesPages > 0">{{t('series-progress', {percentage: (((totalSeriesPagesRead + pageNum) / totalSeriesPages) | percent)}) }}</span>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase } from '@angular/common'; import {DOCUMENT, NgStyle, NgIf, NgFor, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { import {
BehaviorSubject, BehaviorSubject,
@ -32,7 +32,7 @@ import {
import { ChangeContext, LabelType, Options, NgxSliderModule } from 'ngx-slider-v2'; import { ChangeContext, LabelType, Options, NgxSliderModule } from 'ngx-slider-v2';
import {animate, state, style, transition, trigger} from '@angular/animations'; import {animate, state, style, transition, trigger} from '@angular/animations';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {NgbModal, NgbProgressbar} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component'; import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
import {Stack} from 'src/app/shared/data-structures/stack'; import {Stack} from 'src/app/shared/data-structures/stack';
@ -124,7 +124,7 @@ enum KeyDirection {
imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent, imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent, DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe, NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective] FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe]
}) })
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -170,6 +170,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Total pages in the given Chapter * Total pages in the given Chapter
*/ */
maxPages = 1; maxPages = 1;
totalSeriesPages = 0;
totalSeriesPagesRead = 0;
user!: User; user!: User;
generalSettingsForm!: FormGroup; generalSettingsForm!: FormGroup;
@ -840,7 +842,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.getBookmarkInfo(this.seriesId).subscribe(bookmarkInfo => { this.readerService.getBookmarkInfo(this.seriesId).subscribe(bookmarkInfo => {
this.setPageNum(0); this.setPageNum(0);
this.title = bookmarkInfo.seriesName; this.title = bookmarkInfo.seriesName;
this.subtitle = 'Bookmarks'; this.subtitle = translate('manga-reader.bookmarks-title');
this.libraryType = bookmarkInfo.libraryType; this.libraryType = bookmarkInfo.libraryType;
this.maxPages = bookmarkInfo.pages; this.maxPages = bookmarkInfo.pages;
this.mangaReaderService.load(bookmarkInfo); this.mangaReaderService.load(bookmarkInfo);
@ -884,6 +886,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
page = this.adjustPagesForDoubleRenderer(page); page = this.adjustPagesForDoubleRenderer(page);
this.totalSeriesPages = results.chapterInfo.seriesTotalPages;
this.totalSeriesPagesRead = results.chapterInfo.seriesTotalPagesRead - page;
this.setPageNum(page); // first call this.setPageNum(page); // first call
this.goToPageEvent = new BehaviorSubject<number>(this.pageNum); this.goToPageEvent = new BehaviorSubject<number>(this.pageNum);

View File

@ -25,4 +25,6 @@ export interface ChapterInfo {
* This will not always be present. Depends on if asked from backend. * This will not always be present. Depends on if asked from backend.
*/ */
doublePairs?: {[key: number]: number}; doublePairs?: {[key: number]: number};
seriesTotalPagesRead: number;
seriesTotalPages: number;
} }

View File

@ -4,7 +4,7 @@
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-alt')}}</a> <a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-alt')}}</a>
<a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a> <a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
<a class="navbar-brand dark-exempt" routerLink="/home" routerLinkActive="active"> <a class="navbar-brand dark-exempt" routerLink="/home" routerLinkActive="active">
<img width="28" height="28" class="logo" ngSrc="assets/images/logo-32.png" alt="kavita icon" aria-hidden="true"/> <app-image width="28px" height="28px" imageUrl="assets/images/logo-32.png" classes="logo" />
<span class="d-none d-md-inline logo"> Kavita</span> <span class="d-none d-md-inline logo"> Kavita</span>
</a> </a>
<ul class="navbar-nav col me-auto"> <ul class="navbar-nav col me-auto">

View File

@ -2,7 +2,8 @@
<div class="col-auto custom-col clickable" [ngbPopover]="popContent" <div class="col-auto custom-col clickable" [ngbPopover]="popContent"
popoverTitle="Your Rating + Overall" [popoverClass]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 'md-popover' : 'lg-popover'"> popoverTitle="Your Rating + Overall" [popoverClass]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 'md-popover' : 'lg-popover'">
<span class="badge rounded-pill ps-0 me-1"> <span class="badge rounded-pill ps-0 me-1">
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt=""> <!-- <img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">-->
<app-image classes="me-1" imageUrl="assets/images/logo-32.png" width="24px" height="24px" />
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container> <ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
<ng-template #notYetRated>N/A</ng-template> <ng-template #notYetRated>N/A</ng-template>
<ng-container *ngIf="overallRating > 0"> + {{overallRating}}</ng-container> <ng-container *ngIf="overallRating > 0"> + {{overallRating}}</ng-container>
@ -13,7 +14,7 @@
<div class="col-auto custom-col clickable" *ngFor="let rating of ratings" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}" <div class="col-auto custom-col clickable" *ngFor="let rating of ratings" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover"> [popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
<span class="badge rounded-pill me-1"> <span class="badge rounded-pill me-1">
<img class="me-1" [ngSrc]="rating.provider | providerImage" width="24" height="24" alt=""> <img class="me-1" [ngSrc]="rating.provider | providerImage" width="24" height="24" alt="" aria-hidden="true">
{{rating.averageScore}}% {{rating.averageScore}}%
</span> </span>
</div> </div>

View File

@ -19,11 +19,12 @@ import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
import {NgxStarsModule} from "ngx-stars"; import {NgxStarsModule} from "ngx-stars";
import {ThemeService} from "../../../_services/theme.service"; import {ThemeService} from "../../../_services/theme.service";
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
import {ImageComponent} from "../../../shared/image/image.component";
@Component({ @Component({
selector: 'app-external-rating', selector: 'app-external-rating',
standalone: true, standalone: true,
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule], imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule, ImageComponent],
templateUrl: './external-rating.component.html', templateUrl: './external-rating.component.html',
styleUrls: ['./external-rating.component.scss'], styleUrls: ['./external-rating.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -288,7 +288,7 @@
<ng-container *ngIf="nextExpectedChapter"> <ng-container *ngIf="nextExpectedChapter">
<ng-container [ngSwitch]="tabId"> <ng-container [ngSwitch]="tabId">
<ng-container *ngSwitchCase="TabID.Volumes"> <ng-container *ngSwitchCase="TabID.Volumes">
<app-card-item *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0" class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item> <app-card-item *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0" class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"></app-card-item>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="TabID.Chapters"> <ng-container *ngSwitchCase="TabID.Chapters">
<app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item> <app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>

View File

@ -15,11 +15,9 @@
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')"> <app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item"> <a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
<img width="24" height="24" class="lazyload img-placeholder favicon" <app-image classes="favicon" width="24px" height="24px"
[src]="imageService.errorWebLinkImage" [imageUrl]="imageService.getWebLinkImage(item)"
[attr.data-src]="imageService.getWebLinkImage(item)" [errorImage]="imageService.errorWebLinkImage"/>
(error)="imageService.updateErroredWebLinkImage($event)"
aria-hidden="true" alt="">
</a> </a>
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>

View File

@ -5,7 +5,8 @@ import {
inject, inject,
Input, Input,
OnChanges, OnChanges,
SimpleChanges SimpleChanges,
ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ReaderService} from 'src/app/_services/reader.service'; import {ReaderService} from 'src/app/_services/reader.service';
@ -31,6 +32,7 @@ import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.compon
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
import {ImageComponent} from "../../../shared/image/image.component";
@Component({ @Component({
@ -38,10 +40,11 @@ import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
standalone: true, standalone: true,
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent, imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent, ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
MetadataDetailComponent, TranslocoDirective], MetadataDetailComponent, TranslocoDirective, ImageComponent],
templateUrl: './series-metadata-detail.component.html', templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss'], styleUrls: ['./series-metadata-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
}) })
export class SeriesMetadataDetailComponent implements OnChanges { export class SeriesMetadataDetailComponent implements OnChanges {

View File

@ -1,11 +1,6 @@
<!--<img #img class="lazyload img-placeholder {{classes}} fade-in" src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="--> <img class="img-placeholder fade-in"
<!-- [attr.data-src]="imageUrl"-->
<!-- (error)="imageService.updateErroredImage($event)"-->
<!-- aria-hidden="true"-->
<!-- alt=""-->
<!-- >-->
<img class="img-placeholder {{classes}} fade-in"
#img #img
alt="" alt=""
[lazyLoad]="imageUrl" (onStateChange)="myCallbackFunction($event)"> aria-hidden="true"
[lazyLoad]="imageUrl"
(onStateChange)="myCallbackFunction($event)">

View File

@ -3,11 +3,6 @@ img {
transition: opacity 1s; transition: opacity 1s;
} }
.img-placeholder {
background: rgb(44,47,51);
background: radial-gradient(circle, rgba(44,47,51,1) 0%, rgba(34,42,24,1) 37%, rgba(2,0,36,1) 100%);
}
.fade-in { .fade-in {
opacity: 0; opacity: 0;
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;

View File

@ -30,6 +30,12 @@ import {LazyLoadImageModule, StateChange} from "ng-lazyload-image";
}) })
export class ImageComponent implements OnChanges { export class ImageComponent implements OnChanges {
private readonly destroyRef = inject(DestroyRef);
protected readonly imageService = inject(ImageService);
private readonly renderer = inject(Renderer2);
private readonly hubService = inject(MessageHubService);
private readonly cdRef = inject(ChangeDetectorRef);
/** /**
* Source url to load image * Source url to load image
*/ */
@ -66,14 +72,18 @@ export class ImageComponent implements OnChanges {
* If the image component should respond to cover updates * If the image component should respond to cover updates
*/ */
@Input() processEvents: boolean = true; @Input() processEvents: boolean = true;
/**
* Note: Parent component must use ViewEncapsulation.None
*/
@Input() classes: string = ''; @Input() classes: string = '';
/**
* A collection of styles to apply. This is useful if the parent component doesn't want to use no view encapsulation
*/
@Input() styles: string = '';
@Input() errorImage: string = this.imageService.errorImage;
@ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>; @ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>;
private readonly destroyRef = inject(DestroyRef);
protected readonly imageService = inject(ImageService);
private readonly renderer = inject(Renderer2);
private readonly hubService = inject(MessageHubService);
private readonly cdRef = inject(ChangeDetectorRef);
constructor() { constructor() {
this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => { this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
@ -127,6 +137,14 @@ export class ImageComponent implements OnChanges {
if (this.background != '') { if (this.background != '') {
this.renderer.setStyle(this.imgElem.nativeElement, 'background', this.background); this.renderer.setStyle(this.imgElem.nativeElement, 'background', this.background);
} }
if (this.styles != '') {
this.renderer.setStyle(this.imgElem.nativeElement, 'styles', this.styles);
}
if (this.classes != '') {
this.renderer.addClass(this.imgElem.nativeElement, this.classes);
}
} }
@ -154,7 +172,7 @@ export class ImageComponent implements OnChanges {
case 'loading-failed': case 'loading-failed':
// The image could not be loaded for some reason. // The image could not be loaded for some reason.
// `event.data` is the error in this case // `event.data` is the error in this case
image.src = this.imageService.errorImage; image.src = this.errorImage;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
break; break;
case 'finally': case 'finally':

View File

@ -31,7 +31,7 @@
<span class="phone-hidden" title="{{title}}"> <span class="phone-hidden" title="{{title}}">
<div> <div>
<ng-container *ngIf="imageUrl !== null && imageUrl !== ''; else iconImg"> <ng-container *ngIf="imageUrl !== null && imageUrl !== ''; else iconImg">
<img class="lazyload img-placeholder" [ngSrc]="imageUrl" width="20" height="20" alt="icon"> <app-image [imageUrl]="imageUrl" width="20px" height="20px"></app-image>
</ng-container> </ng-container>
<ng-template #iconImg><i class="fa {{icon}}" aria-hidden="true"></i></ng-template> <ng-template #iconImg><i class="fa {{icon}}" aria-hidden="true"></i></ng-template>
</div> </div>

View File

@ -12,12 +12,13 @@ import { filter, map } from 'rxjs';
import { NavService } from 'src/app/_services/nav.service'; import { NavService } from 'src/app/_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {CommonModule, NgOptimizedImage} from "@angular/common"; import {CommonModule, NgOptimizedImage} from "@angular/common";
import {ImageComponent} from "../../../shared/image/image.component";
@Component({ @Component({
selector: 'app-side-nav-item', selector: 'app-side-nav-item',
standalone: true, standalone: true,
imports: [CommonModule, RouterLink, NgOptimizedImage], imports: [CommonModule, RouterLink, NgOptimizedImage, ImageComponent],
templateUrl: './side-nav-item.component.html', templateUrl: './side-nav-item.component.html',
styleUrls: ['./side-nav-item.component.scss'], styleUrls: ['./side-nav-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@ -914,6 +914,8 @@
}, },
"cover-image-chooser": { "cover-image-chooser": {
"enter-an-url-pre-title": "Enter an {{url}}",
"url": "Url",
"drag-n-drop": "Drag and drop", "drag-n-drop": "Drag and drop",
"upload": "Upload", "upload": "Upload",
"upload-continued": "an image", "upload-continued": "an image",
@ -1505,13 +1507,15 @@
"brightness-label": "Brightness", "brightness-label": "Brightness",
"bookmark-page-tooltip": "Bookmark Page", "bookmark-page-tooltip": "Bookmark Page",
"unbookmark-page-tooltip": "Unbookmark Page", "unbookmark-page-tooltip": "Unbookmark Page",
"bookmarks-title": "Bookmarks",
"first-time-reading-manga": "Tap the image at any time to open the menu. You can configure different settings or go to page by clicking progress bar. Tap sides of image move to next/prev page.", "first-time-reading-manga": "Tap the image at any time to open the menu. You can configure different settings or go to page by clicking progress bar. Tap sides of image move to next/prev page.",
"layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout", "layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout",
"no-next-chapter": "No Next Chapter", "no-next-chapter": "No Next Chapter",
"no-prev-chapter": "No Previous Chapter", "no-prev-chapter": "No Previous Chapter",
"user-preferences-updated": "User preferences updated", "user-preferences-updated": "User preferences updated",
"emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}" "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}",
"series-progress": "Series Progress: {{percentage}}"
}, },
"metadata-filter": { "metadata-filter": {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.10.4" "version": "0.7.10.5"
}, },
"servers": [ "servers": [
{ {
@ -2904,10 +2904,12 @@
"tags": [ "tags": [
"Library" "Library"
], ],
"summary": "Returns the type of the underlying library",
"parameters": [ "parameters": [
{ {
"name": "libraryId", "name": "libraryId",
"in": "query", "in": "query",
"description": "",
"schema": { "schema": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
@ -13998,6 +14000,16 @@
"description": "Series Title", "description": "Series Title",
"nullable": true "nullable": true
}, },
"seriesTotalPages": {
"type": "integer",
"description": "Total pages for the series",
"format": "int32"
},
"seriesTotalPagesRead": {
"type": "integer",
"description": "Total pages read for the series",
"format": "int32"
},
"pageDimensions": { "pageDimensions": {
"type": "array", "type": "array",
"items": { "items": {