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))
{
_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
{
EmailLink = emailLink,

View File

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

View File

@ -134,7 +134,7 @@ public class LibraryController : BaseApiController
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("list")]
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string? path)
{
if (string.IsNullOrEmpty(path))
{
@ -385,7 +385,7 @@ public class LibraryController : BaseApiController
}
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();
return Ok(false);
}
@ -441,7 +441,7 @@ public class LibraryController : BaseApiController
// Override Scrobbling for Comic libraries since there are no providers to scrobble to
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;
}
@ -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")]
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);
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,
UserAgent = userAgent,
@ -55,7 +55,7 @@ public class PluginController : BaseApiController
throw new KavitaUnauthenticatedUserException();
}
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
{
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"));
var mangaFile = chapter.Files.First();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, User.GetUserId());
var info = new ChapterInfoDto()
{
ChapterNumber = dto.ChapterNumber,
@ -242,6 +244,8 @@ public class ReaderController : BaseApiController
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
SeriesTotalPages = series?.Pages ?? 0,
SeriesTotalPagesRead = series?.PagesRead ?? 0,
ChapterTitle = dto.ChapterTitle ?? string.Empty,
Subtitle = string.Empty,
Title = dto.SeriesName,
@ -266,8 +270,7 @@ public class ReaderController : BaseApiController
}
else
{
//info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
info.Subtitle = $"Volume {info.VolumeNumber}";
info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
{
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)
{
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();

View File

@ -65,6 +65,14 @@ public class ChapterInfoDto : IChapterInfoDto
/// </summary>
/// <remarks>Usually just series name, but can include chapter title</remarks>
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>
/// List of all files with their inner archive structure maintained in filename and dimensions
@ -76,5 +84,4 @@ public class ChapterInfoDto : IChapterInfoDto
/// </summary>
/// <remarks>This is optionally returned by includeDimensions</remarks>
public IDictionary<int, int>? DoublePairs { get; set; }
}

View File

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

View File

@ -219,7 +219,7 @@ public class ImageService : IImageService
{
// Parse the URL to get the domain (including subdomain)
var uri = new Uri(url);
var domain = uri.Host;
var domain = uri.Host.Replace(Environment.NewLine, string.Empty);
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
var firstFile = chapter.Files.FirstOrDefault();
if (firstFile == null) return;
if (!_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate,
if (firstFile == null || !_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis,
forceUpdate,
firstFile))
{
volume.WordCount += chapter.WordCount;
series.WordCount += chapter.WordCount;
continue;
}
if (series.Format == MangaFormat.Epub)
{

View File

@ -306,7 +306,7 @@ public class ProcessSeries : IProcessSeries
if (!series.Metadata.PublicationStatusLocked)
{
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;
} else if (series.Metadata.TotalCount > 0)

View File

@ -9,7 +9,7 @@
</ng-container>
<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>
</p>
<span class="download">

View File

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

View File

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

View File

@ -13,11 +13,13 @@
<div class="d-flex justify-content-center">
<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 style="padding-right:0px" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1" style="padding-right:0px"></span>
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</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 {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop';
import { fromEvent, Subject } from 'rxjs';
@ -25,7 +35,14 @@ import {translate, TranslocoModule} from "@ngneat/transloco";
styleUrls: ['./cover-image-chooser.component.scss'],
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.
@ -70,10 +87,8 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(',');
mode: 'file' | 'url' | 'all' = 'all';
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private fb: FormBuilder, private toastr: ToastrService, private uploadService: UploadService,
@Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
constructor(@Inject(DOCUMENT) private document: Document) { }
ngOnInit(): void {
this.form = this.fb.group({
@ -83,10 +98,6 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
/**
* 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 {
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}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false;
@ -59,19 +67,13 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges {
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
isScrobbling: boolean = true;
libraryAllowsScrobbling: boolean = true;
private readonly destroyRef = inject(DestroyRef);
get MangaFormat() {
return MangaFormat;
}
get FilterField() {
return FilterField;
}
protected readonly MangaFormat = MangaFormat;
protected readonly FilterField = FilterField;
constructor(public utilityService: UtilityService, private readerService: ReaderService,
private readonly cdRef: ChangeDetectorRef, private messageHub: MessageHubService,
public accountService: AccountService, private scrobbleService: ScrobblingService) {
constructor() {
// Listen for progress events and re-calculate getTimeLeft
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
map(evt => evt.payload as UserProgressUpdateEvent),

View File

@ -26,12 +26,14 @@
<span class="visually-hidden">{{t('continuous-reading-prev-chapter-alt')}}</span>
</div>
</div>
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block"
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;">
</ng-container>
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
<div>
<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 {takeUntilDestroyed} from "@angular/core/rxjs-interop";
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
@ -62,6 +63,12 @@ const enum DEBUG_MODES {
})
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
*/
@ -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
*/
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
*/
@ -169,9 +176,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
}
constructor(private readerService: ReaderService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
constructor(@Inject(DOCUMENT) private readonly document: Document) {
// This will always exist at this point in time since this is used within manga reader
const reader = document.querySelector('.reading-area');
if (reader !== null) {
@ -200,6 +205,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.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 {
@ -307,6 +316,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
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() {
let totalHeight = 0;
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; }
const rect = elem.getBoundingClientRect();
const [innerHeight, innerWidth] = this.getInnerDimensions();
if (rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (innerHeight || document.body.clientHeight) &&
@ -438,6 +457,32 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
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() {
this.initFinished = false;
@ -449,6 +494,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.cdRef.markForCheck();
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies();
this.debugLog('[INIT] Prefetching pages ' + startingIndex + ' to ' + endingIndex + '. Current page: ', this.pageNum);
for(let i = startingIndex; i <= endingIndex; i++) {
this.loadWebtoonImage(i);

View File

@ -10,7 +10,7 @@
<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">
{{subtitle}}
{{subtitle}} <span *ngIf="totalSeriesPages > 0">{{t('series-progress', {percentage: (((totalSeriesPagesRead + pageNum) / totalSeriesPages) | percent)}) }}</span>
</div>
</div>

View File

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

View File

@ -2,7 +2,8 @@
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
popoverTitle="Your Rating + Overall" [popoverClass]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 'md-popover' : 'lg-popover'">
<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-template #notYetRated>N/A</ng-template>
<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}"
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
<span class="badge rounded-pill me-1">
<img class="me-1" [ngSrc]="rating.provider | providerImage" width="24" height="24" alt="">
<img class="me-1" [ngSrc]="rating.provider | providerImage" width="24" height="24" alt="" aria-hidden="true">
{{rating.averageScore}}%
</span>
</div>

View File

@ -19,11 +19,12 @@ import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
import {NgxStarsModule} from "ngx-stars";
import {ThemeService} from "../../../_services/theme.service";
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
import {ImageComponent} from "../../../shared/image/image.component";
@Component({
selector: 'app-external-rating',
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',
styleUrls: ['./external-rating.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -15,11 +15,9 @@
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-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"
[src]="imageService.errorWebLinkImage"
[attr.data-src]="imageService.getWebLinkImage(item)"
(error)="imageService.updateErroredWebLinkImage($event)"
aria-hidden="true" alt="">
<app-image classes="favicon" width="24px" height="24px"
[imageUrl]="imageService.getWebLinkImage(item)"
[errorImage]="imageService.errorWebLinkImage"/>
</a>
</ng-template>
</app-metadata-detail>

View File

@ -5,7 +5,8 @@ import {
inject,
Input,
OnChanges,
SimpleChanges
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {Router} from '@angular/router';
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 {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
import {ImageComponent} from "../../../shared/image/image.component";
@Component({
@ -38,10 +40,11 @@ import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
standalone: true,
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
MetadataDetailComponent, TranslocoDirective],
MetadataDetailComponent, TranslocoDirective, ImageComponent],
templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
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=="-->
<!-- [attr.data-src]="imageUrl"-->
<!-- (error)="imageService.updateErroredImage($event)"-->
<!-- aria-hidden="true"-->
<!-- alt=""-->
<!-- >-->
<img class="img-placeholder {{classes}} fade-in"
<img class="img-placeholder fade-in"
#img
alt=""
[lazyLoad]="imageUrl" (onStateChange)="myCallbackFunction($event)">
aria-hidden="true"
[lazyLoad]="imageUrl"
(onStateChange)="myCallbackFunction($event)">

View File

@ -3,11 +3,6 @@ img {
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 {
opacity: 0;
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 {
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
*/
@ -66,14 +72,18 @@ export class ImageComponent implements OnChanges {
* If the image component should respond to cover updates
*/
@Input() processEvents: boolean = true;
/**
* Note: Parent component must use ViewEncapsulation.None
*/
@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>;
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() {
this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
@ -127,6 +137,14 @@ export class ImageComponent implements OnChanges {
if (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':
// The image could not be loaded for some reason.
// `event.data` is the error in this case
image.src = this.imageService.errorImage;
image.src = this.errorImage;
this.cdRef.markForCheck();
break;
case 'finally':

View File

@ -31,7 +31,7 @@
<span class="phone-hidden" title="{{title}}">
<div>
<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-template #iconImg><i class="fa {{icon}}" aria-hidden="true"></i></ng-template>
</div>

View File

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

View File

@ -914,6 +914,8 @@
},
"cover-image-chooser": {
"enter-an-url-pre-title": "Enter an {{url}}",
"url": "Url",
"drag-n-drop": "Drag and drop",
"upload": "Upload",
"upload-continued": "an image",
@ -1505,13 +1507,15 @@
"brightness-label": "Brightness",
"bookmark-page-tooltip": "Bookmark 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.",
"layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout",
"no-next-chapter": "No Next Chapter",
"no-prev-chapter": "No Previous Chapter",
"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": {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.10.4"
"version": "0.7.10.5"
},
"servers": [
{
@ -2904,10 +2904,12 @@
"tags": [
"Library"
],
"summary": "Returns the type of the underlying library",
"parameters": [
{
"name": "libraryId",
"in": "query",
"description": "",
"schema": {
"type": "integer",
"format": "int32"
@ -13998,6 +14000,16 @@
"description": "Series Title",
"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": {
"type": "array",
"items": {