mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Polish Round 2 (#2411)
This commit is contained in:
parent
ba3e760b31
commit
a2fd87c454
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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!,
|
||||||
|
@ -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) +
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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">
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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),
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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)">
|
||||||
|
@ -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;
|
||||||
|
@ -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':
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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": {
|
||||||
|
14
openapi.json
14
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user