diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index c691817bd..576011b91 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -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, diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index a0158d3ab..26f6871d1 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -59,7 +59,7 @@ public class CollectionController : BaseApiController /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("search")] - public async Task>> SearchTags(string queryString) + public async Task>> SearchTags(string? queryString) { queryString ??= string.Empty; queryString = queryString.Replace(@"%", string.Empty); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index c0706561b..e31e85fa8 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -134,7 +134,7 @@ public class LibraryController : BaseApiController /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("list")] - public ActionResult> GetDirectories(string path) + public ActionResult> 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 } - + /// + /// Returns the type of the underlying library + /// + /// + /// [HttpGet("type")] public async Task> GetLibraryType(int libraryId) { diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index 89a006f8c..ff33cf8e1 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -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!, diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index a5a2b6a7b..733f084b9 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -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) + diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 893a6a9d8..36b4ff3d2 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -143,7 +143,7 @@ public class SeriesController : BaseApiController public async Task 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(); diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 36ddd554e..bb3a8d3ee 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -65,6 +65,14 @@ public class ChapterInfoDto : IChapterInfoDto /// /// Usually just series name, but can include chapter title public string Title { get; set; } = default!; + /// + /// Total pages for the series + /// + public int SeriesTotalPages { get; set; } + /// + /// Total pages read for the series + /// + public int SeriesTotalPagesRead { get; set; } /// /// List of all files with their inner archive structure maintained in filename and dimensions @@ -76,5 +84,4 @@ public class ChapterInfoDto : IChapterInfoDto /// /// This is optionally returned by includeDimensions public IDictionary? DoublePairs { get; set; } - } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 78102e126..8a6bd34aa 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -369,6 +369,7 @@ public class DirectoryService : IDirectoryService /// public void ClearDirectory(string directoryPath) { + directoryPath = directoryPath.Replace(Environment.NewLine, string.Empty); var di = FileSystem.DirectoryInfo.New(directoryPath); if (!di.Exists) return; try diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 062b88935..3e9b02118 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -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; diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index a73a53b5b..333c5ef18 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -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) { diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index bbcd87280..8def30e8b 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -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) diff --git a/UI/Web/src/app/cards/card-item/card-item.component.html b/UI/Web/src/app/cards/card-item/card-item.component.html index 665c1e75a..c9b53787c 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.html +++ b/UI/Web/src/app/cards/card-item/card-item.component.html @@ -9,7 +9,7 @@
-

+

diff --git a/UI/Web/src/app/cards/card-item/card-item.component.scss b/UI/Web/src/app/cards/card-item/card-item.component.scss index e44086e22..794e93233 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.scss +++ b/UI/Web/src/app/cards/card-item/card-item.component.scss @@ -38,6 +38,7 @@ $image-width: 160px; display: block; margin-top: 2px; margin-bottom: 0px; + text-align: center; } .selected-highlight { diff --git a/UI/Web/src/app/cards/card-item/card-item.component.ts b/UI/Web/src/app/cards/card-item/card-item.component.ts index c17e10657..e809f384c 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.ts +++ b/UI/Web/src/app/cards/card-item/card-item.component.ts @@ -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(); diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html index e239591e9..b4ea2f4bb 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html @@ -13,11 +13,13 @@
diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index 1a0774b20..75b531b07 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -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(); - 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. diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts index f56bfee4b..900a7704e 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts @@ -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), diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html index fbe0414b9..ceba55c95 100644 --- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html @@ -26,12 +26,14 @@ {{t('continuous-reading-prev-chapter-alt')}} + image +