From e3467457eae69ed9b6817bab71db01759bf64e7f Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Wed, 19 Apr 2023 17:41:21 -0500 Subject: [PATCH] Release Testing Day 1 (#1933) * Enhance plugin/authenticate to allow RefreshToken to be returned as well. * When typing a series name, min, or max filter, press enter to apply metadata filter. * Cleaned up the documentation around MaxCount and TotalCount * Fixed a bug where PublicationStatus wasn't being correctly set due to some strange logic I coded. * Fixed bookmark mode not having access to critical page dimensions. Fetching bookmark info api now returns dimensions by default. * Fixed pagination scaling code for different fitting options * Fixed missing code to persist page split in manga reader * Removed unneeded prefetch of blank images in bookmark mode --- API/Controllers/PluginController.cs | 1 + API/Controllers/ReaderController.cs | 32 ++++++++++++------- API/DTOs/Reader/BookmarkInfoDto.cs | 13 +++++++- API/Data/Metadata/ComicInfo.cs | 2 +- API/Entities/Metadata/SeriesMetadata.cs | 2 +- API/Services/CacheService.cs | 27 +++++++++++----- API/Services/Tasks/Scanner/ProcessSeries.cs | 4 ++- Kavita.sln.DotSettings | 3 +- .../app/_models/manga-reader/bookmark-info.ts | 9 ++++++ .../edit-series-modal.component.html | 4 +-- .../manga-reader/manga-reader.component.ts | 23 ++++++++----- .../_series/managa-reader.service.ts | 3 +- .../metadata-filter.component.html | 6 ++-- openapi.json | 30 +++++++++++++++-- 14 files changed, 119 insertions(+), 40 deletions(-) diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index e8659b6dc..a03cb3014 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -43,6 +43,7 @@ public class PluginController : BaseApiController { Username = user.UserName!, Token = await _tokenService.CreateToken(user), + RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, }; } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 4311448b3..e53a5402f 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -186,7 +186,7 @@ public class ReaderController : BaseApiController if (chapterId <= 0) return ArraySegment.Empty; var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return BadRequest("Could not find Chapter"); - return Ok(_cacheService.GetCachedFileDimensions(chapterId)); + return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId))); } /// @@ -228,7 +228,7 @@ public class ReaderController : BaseApiController if (includeDimensions) { - info.PageDimensions = _cacheService.GetCachedFileDimensions(chapterId); + info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId)); info.DoublePairs = _readerService.GetPairs(info.PageDimensions); } @@ -260,21 +260,31 @@ public class ReaderController : BaseApiController /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading. /// /// Series Id for all bookmarks + /// Include file dimensions (extra I/O). Defaults to true. /// [HttpGet("bookmark-info")] - public async Task> GetBookmarkInfo(int seriesId) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"seriesId", "includeDimensions"})] + public async Task> GetBookmarkInfo(int seriesId, bool includeDimensions = true) { var totalPages = await _cacheService.CacheBookmarkForSeries(User.GetUserId(), seriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); - return Ok(new BookmarkInfoDto() + var info = new BookmarkInfoDto() { SeriesName = series!.Name, SeriesFormat = series.Format, SeriesId = series.Id, LibraryId = series.LibraryId, Pages = totalPages, - }); + }; + + if (includeDimensions) + { + info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetBookmarkCachePath(seriesId)); + info.DoublePairs = _readerService.GetPairs(info.PageDimensions); + } + + return Ok(info); } @@ -606,7 +616,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user?.Bookmarks == null) return Ok("Nothing to remove"); + if (user.Bookmarks == null) return Ok("Nothing to remove"); try { @@ -643,7 +653,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user?.Bookmarks == null) return Ok(Array.Empty()); + if (user.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId)); } @@ -657,7 +667,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user?.Bookmarks == null) return Ok(Array.Empty()); + if (user.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId)); } @@ -721,7 +731,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("next-chapter")] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { @@ -740,7 +750,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("prev-chapter")] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { @@ -755,7 +765,7 @@ public class ReaderController : BaseApiController /// /// [HttpGet("time-left")] - [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId"})] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})] public async Task> GetEstimateToCompletion(int seriesId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/API/DTOs/Reader/BookmarkInfoDto.cs index 57bfa989c..7583ee76d 100644 --- a/API/DTOs/Reader/BookmarkInfoDto.cs +++ b/API/DTOs/Reader/BookmarkInfoDto.cs @@ -1,4 +1,5 @@ -using API.Entities.Enums; +using System.Collections.Generic; +using API.Entities.Enums; namespace API.DTOs.Reader; @@ -10,4 +11,14 @@ public class BookmarkInfoDto public int LibraryId { get; set; } public LibraryType LibraryType { get; set; } public int Pages { get; set; } + /// + /// List of all files with their inner archive structure maintained in filename and dimensions + /// + /// This is optionally returned by includeDimensions + public IEnumerable? PageDimensions { get; set; } + /// + /// For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page + /// + /// This is optionally returned by includeDimensions + public IDictionary? DoublePairs { get; set; } } diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index c50e21d9f..9e29ca637 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -154,7 +154,7 @@ public class ComicInfo return Math.Max(Count, (int) Math.Floor(float.Parse(Volume))); } - return Count; + return 0; } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 3d015d76e..90eadba5d 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -39,7 +39,7 @@ public class SeriesMetadata : IHasConcurrencyToken /// public int TotalCount { get; set; } = 0; /// - /// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo) + /// Max number of issues/volumes in the series (Max of Volume/Number field in ComicInfo) /// public int MaxCount { get; set; } = 0; public PublicationStatus PublicationStatus { get; set; } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 128157d31..9998526f9 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -32,8 +32,10 @@ public interface ICacheService void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); string GetCachedPagePath(int chapterId, int page); + string GetCachePath(int chapterId); + string GetBookmarkCachePath(int seriesId); IEnumerable GetCachedPages(int chapterId); - IEnumerable GetCachedFileDimensions(int chapterId); + IEnumerable GetCachedFileDimensions(string cachePath); string GetCachedBookmarkPagePath(int seriesId, int page); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); @@ -66,11 +68,15 @@ public class CacheService : ICacheService .OrderByNatural(Path.GetFileNameWithoutExtension); } - public IEnumerable GetCachedFileDimensions(int chapterId) + /// + /// For a given path, scan all files (in reading order) and generate File Dimensions for it. Path must exist + /// + /// + /// + public IEnumerable GetCachedFileDimensions(string cachePath) { var sw = Stopwatch.StartNew(); - var path = GetCachePath(chapterId); - var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + var files = _directoryService.GetFilesWithExtension(cachePath, Tasks.Scanner.Parser.Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) .ToArray(); @@ -94,13 +100,13 @@ public class CacheService : ICacheService Height = image.Height, Width = image.Width, IsWide = image.Width > image.Height, - FileName = file.Replace(path, string.Empty) + FileName = file.Replace(cachePath, string.Empty) }); } } catch (Exception ex) { - _logger.LogError(ex, "There was an error calculating image dimensions for {ChapterId}", chapterId); + _logger.LogError(ex, "There was an error calculating image dimensions for {CachePath}", cachePath); } finally { @@ -259,12 +265,17 @@ public class CacheService : ICacheService /// /// /// - private string GetCachePath(int chapterId) + public string GetCachePath(int chapterId) { return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{chapterId}/")); } - private string GetBookmarkCachePath(int seriesId) + /// + /// Returns the cache path for a given series' bookmarks. Should be cacheDirectory/{seriesId_bookmarks}/ + /// + /// + /// + public string GetBookmarkCachePath(int seriesId) { return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 0dbd07255..785bc2960 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -276,7 +276,9 @@ public class ProcessSeries : IProcessSeries // Set the AgeRating as highest in all the comicInfos if (!series.Metadata.AgeRatingLocked) series.Metadata.AgeRating = chapters.Max(chapter => chapter.AgeRating); + // Count (aka expected total number of chapters or volumes from metadata) across all chapters series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount); + // The actual number of count's defined across all chapter's metadata series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); // To not have to rely completely on ComicInfo, try to parse out if the series is complete by checking parsed filenames as well. if (series.Metadata.MaxCount != series.Metadata.TotalCount) @@ -294,7 +296,7 @@ public class ProcessSeries : IProcessSeries if (series.Metadata.MaxCount >= series.Metadata.TotalCount && series.Metadata.TotalCount > 0) { series.Metadata.PublicationStatus = PublicationStatus.Completed; - } else if (series.Metadata.TotalCount > 0 && series.Metadata.MaxCount > 0) + } else if (series.Metadata.TotalCount > 0) { series.Metadata.PublicationStatus = PublicationStatus.Ended; } diff --git a/Kavita.sln.DotSettings b/Kavita.sln.DotSettings index 277751040..2ebdeb970 100644 --- a/Kavita.sln.DotSettings +++ b/Kavita.sln.DotSettings @@ -12,4 +12,5 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts index e63c31390..26ee5e0bb 100644 --- a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts +++ b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts @@ -1,3 +1,4 @@ +import { FileDimension } from "src/app/manga-reader/_models/file-dimension"; import { LibraryType } from "../library"; import { MangaFormat } from "../manga-format"; @@ -8,4 +9,12 @@ export interface BookmarkInfo { libraryId: number; libraryType: LibraryType; pages: number; + /** + * This will not always be present. Depends on if asked from backend. + */ + pageDimensions?: Array; + /** + * This will not always be present. Depends on if asked from backend. + */ + doublePairs?: {[key: number]: number}; } \ No newline at end of file diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index c8574ed90..a64c2bd67 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -380,11 +380,11 @@
Max Items: {{metadata.maxCount}} - +
Total Items: {{metadata.totalCount}} - +
Publication Status: {{metadata.publicationStatus | publicationStatus}}
Total Pages: {{series.pages}}
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index b6ed5d048..c18bbcd6f 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -373,15 +373,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.FittingOption !== FITTING_OPTION.HEIGHT) { return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px'; } + return this.readingArea?.nativeElement?.clientHeight + 'px'; } // This is for the pagination area get MaxHeight() { - if (this.FittingOption !== FITTING_OPTION.HEIGHT) { - return Math.min(this.readingArea?.nativeElement?.clientHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px'; + if (this.FittingOption === FITTING_OPTION.HEIGHT) { + return 'calc(var(--vh) * 100)'; } - return 'calc(var(--vh) * 100)'; + + const needsScrolling = this.readingArea?.nativeElement?.scrollHeight > this.readingArea?.nativeElement?.clientHeight; + if (this.readingArea?.nativeElement?.clientHeight <= this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) { + if (needsScrolling) { + return Math.min(this.readingArea?.nativeElement?.scrollHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px'; + } + } + return this.readingArea?.nativeElement?.clientHeight + 'px'; } get RightPaginationOffset() { @@ -806,6 +814,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.subtitle = 'Bookmarks'; this.libraryType = bookmarkInfo.libraryType; this.maxPages = bookmarkInfo.pages; + this.mangaReaderService.load(bookmarkInfo); // Due to change detection rules in Angular, we need to re-create the options object to apply the change const newOptions: Options = Object.assign({}, this.pageOptions); @@ -814,10 +823,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.inSetup = false; this.cdRef.markForCheck(); - for (let i = 0; i < PREFETCH_PAGES; i++) { - this.cachedImages.push(new Image()) - } - this.goToPageEvent = new BehaviorSubject(this.pageNum); this.render(); @@ -1625,7 +1630,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { data.readingDirection = this.readingDirection; data.emulateBook = modelSettings.emulateBook; data.swipeToPaginate = modelSettings.swipeToPaginate; - this.accountService.updatePreferences(data).subscribe((updatedPrefs) => { + data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10); + + this.accountService.updatePreferences(data).subscribe(updatedPrefs => { this.toastr.success('User preferences updated'); if (this.user) { this.user.preferences = updatedPrefs; diff --git a/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts b/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts index 49641b80c..9bdf4c03c 100644 --- a/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts +++ b/UI/Web/src/app/manga-reader/_series/managa-reader.service.ts @@ -5,6 +5,7 @@ import { ReaderService } from 'src/app/_services/reader.service'; import { ChapterInfo } from '../_models/chapter-info'; import { DimensionMap } from '../_models/file-dimension'; import { FITTING_OPTION } from '../_models/reader-enums'; +import { BookmarkInfo } from 'src/app/_models/manga-reader/bookmark-info'; @Injectable({ providedIn: 'root' @@ -19,7 +20,7 @@ export class ManagaReaderService { this.renderer = rendererFactory.createRenderer(null, null); } - load(chapterInfo: ChapterInfo) { + load(chapterInfo: ChapterInfo | BookmarkInfo) { chapterInfo.pageDimensions!.forEach(d => { this.pageDimensions[d.pageNumber] = { height: d.height, diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 3dea2ebbb..220ece6af 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -321,7 +321,7 @@ Series name will filter against Name, Sort Name, or Localized Name - +
@@ -329,14 +329,14 @@
- +
- +
diff --git a/openapi.json b/openapi.json index b4b3eeea3..c4c08e989 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.1.37" + "version": "0.7.1.38" }, "servers": [ { @@ -3909,6 +3909,15 @@ "type": "integer", "format": "int32" } + }, + { + "name": "includeDimensions", + "in": "query", + "description": "Include file dimensions (extra I/O). Defaults to true.", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { @@ -10191,6 +10200,23 @@ "pages": { "type": "integer", "format": "int32" + }, + "pageDimensions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDimensionDto" + }, + "description": "List of all files with their inner archive structure maintained in filename and dimensions", + "nullable": true + }, + "doublePairs": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + }, + "description": "For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page", + "nullable": true } }, "additionalProperties": false @@ -13559,7 +13585,7 @@ }, "maxCount": { "type": "integer", - "description": "Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)", + "description": "Max number of issues/volumes in the series (Max of Volume/Number field in ComicInfo)", "format": "int32" }, "publicationStatus": {