From e489d2404a9b80164e65fc6222fec94c42e92c5a Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Thu, 30 Nov 2023 08:40:02 -0600 Subject: [PATCH] Reader Polish (#2465) Co-authored-by: Andre Smith --- API/Controllers/OPDSController.cs | 16 +++--- API/Controllers/ReaderController.cs | 15 ++++-- API/Data/Repositories/LibraryRepository.cs | 2 +- API/Services/DirectoryService.cs | 1 + API/Services/ReaderService.cs | 2 +- API/Services/SeriesService.cs | 2 +- API/Services/StreamService.cs | 4 +- API/Services/TachiyomiService.cs | 2 +- API/Services/Tasks/Scanner/LibraryWatcher.cs | 2 +- .../Tasks/Scanner/ParseScannedFiles.cs | 1 - API/Services/Tasks/Scanner/ProcessSeries.cs | 2 +- API/Startup.cs | 1 + .../_models/series-detail/relation-kind.ts | 4 +- .../book-line-overlay.component.ts | 5 ++ .../book-reader/book-reader.component.html | 27 +++++----- .../book-reader/book-reader.component.ts | 30 ++++++++--- .../edit-series-modal.component.html | 52 +++++++++---------- .../edit-series-relation.component.ts | 19 ++++--- .../double-renderer.component.ts | 5 +- .../manga-reader/manga-reader.component.html | 4 +- .../manga-reader/manga-reader.component.ts | 44 ++++++++++------ .../single-renderer.component.html | 24 ++++----- .../single-renderer.component.ts | 4 +- .../library-settings-modal.component.ts | 8 ++- 24 files changed, 156 insertions(+), 120 deletions(-) diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 1eb0dbb745..38922aad92 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -308,7 +308,7 @@ public async Task GetSmartFilters(string apiKey) var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (baseUrl, prefix) = await GetPrefix(); + var (_, prefix) = await GetPrefix(); var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId); var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{prefix}{apiKey}/smart-filters", apiKey, prefix); @@ -337,7 +337,7 @@ public async Task GetExternalSources(string apiKey) var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (baseUrl, prefix) = await GetPrefix(); + var (_, prefix) = await GetPrefix(); var externalSources = await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId); var feed = CreateFeed(await _localizationService.Translate(userId, "external-sources"), $"{prefix}{apiKey}/external-sources", apiKey, prefix); @@ -370,15 +370,13 @@ public async Task GetLibraries(string apiKey) if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); var (baseUrl, prefix) = await GetPrefix(); - var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId); var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix); SetFeedId(feed, "libraries"); // Ensure libraries follow SideNav order var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId, false); - foreach (var sideNavStream in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library)) + foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library).Select(sideNavStream => sideNavStream.Library)) { - var library = sideNavStream.Library; feed.Entries.Add(new FeedEntry() { Id = library!.Id.ToString(), @@ -779,13 +777,13 @@ public async Task GetSeries(string apiKey, int seriesId) var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), _chapterSortComparer); - foreach (var chapter in chapters) + foreach (var chapterId in chapters.Select(c => c.Id)) { - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); - var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); foreach (var mangaFile in files) { - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapterId, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); } } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 932fba045b..dcee9b62dc 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -105,18 +105,23 @@ public async Task GetPdf(int chapterId, string apiKey) /// Should Kavita extract pdf into images. Defaults to false. /// [HttpGet("image")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId","page", "extractPdf", "apiKey"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "page", "extractPdf", "apiKey"})] [AllowAnonymous] public async Task GetImage(int chapterId, int page, string apiKey, bool extractPdf = false) { if (page < 0) page = 0; var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); - var chapter = await _cacheService.Ensure(chapterId, extractPdf); - if (chapter == null) return NoContent(); try { + if (new Random().Next(1, 10) > 5) + { + await Task.Delay(1000); + } + var chapter = await _cacheService.Ensure(chapterId, extractPdf); + if (chapter == null) return NoContent(); + _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); @@ -245,8 +250,8 @@ public async Task> GetChapterInfo(int chapterId, bo LibraryId = dto.LibraryId, IsSpecial = dto.IsSpecial, Pages = dto.Pages, - SeriesTotalPages = series?.Pages ?? 0, - SeriesTotalPagesRead = series?.PagesRead ?? 0, + SeriesTotalPages = series.Pages, + SeriesTotalPagesRead = series.PagesRead, ChapterTitle = dto.ChapterTitle ?? string.Empty, Subtitle = string.Empty, Title = dto.SeriesName, diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 6cae8e2d3b..7c61bc8906 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -280,7 +280,7 @@ private static LanguageDto GetCulture(string s) { Title = s, IsoCode = s - };; + }; } public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index ae83ac7892..67464bad3f 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -644,6 +644,7 @@ public string GetParentDirectoryName(string fileOrFolder) /// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns /// /// + /// /// /// public IList ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null) diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index a569810699..052bf87f2d 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -398,7 +398,7 @@ public async Task GetNextChapterIdAsync(int seriesId, int volumeId, int cur // Handle Chapters within next Volume // ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+ var chapters = volume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer).ToList(); - if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters.Last().Number.Equals(Parser.DefaultChapter)) + if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters[^1].Number.Equals(Parser.DefaultChapter)) { // We need to handle an extra check if the current chapter is the last special, as we should return -1 if (currentChapter.IsSpecial) return -1; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index a234f64a22..b5d41ac52f 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -813,7 +813,7 @@ public async Task GetEstimatedChapterCreationDate(int se private static double ExponentialSmoothing(IList data, double alpha) { - var forecast = data.First(); + var forecast = data[0]; foreach (var value in data) { diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs index fc10378232..f12f10a8ac 100644 --- a/API/Services/StreamService.cs +++ b/API/Services/StreamService.cs @@ -71,7 +71,7 @@ public async Task CreateDashboardStreamFromSmartFilter(int u var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); var maxOrder = user!.DashboardStreams.Max(d => d.Order); @@ -159,7 +159,7 @@ public async Task CreateSideNavStreamFromSmartFilter(int userI var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - var stream = user?.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); var maxOrder = user!.SideNavStreams.Max(d => d.Order); diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 3494f0bf6e..e4e9aab219 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -72,7 +72,7 @@ public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger(volumes - .Last().Chapters + [^1].Chapters .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) .Last()); if (volumeChapter.Number == "0") diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 2cbb24fb40..00dbe135c3 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -179,7 +179,7 @@ private void OnDeleted(object sender, FileSystemEventArgs e) { /// private void OnError(object sender, ErrorEventArgs e) { - _logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers"); + _logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers {Current}/{Total}", _bufferFullCounter, 3); bool condition; lock (Lock) { diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index a7e64bd6a5..585a60073c 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -312,7 +312,6 @@ public async Task ScanLibrariesForSeries(Library library, } await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", library.Name, ProgressEventType.Ended)); - return; async Task ProcessFolder(IList files, string folder) { diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index d73f3e5450..cb9ae2916d 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -295,7 +295,7 @@ public void UpdateSeriesMetadata(Series series, Library library) if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) { series.Metadata.MaxCount = 1; - } else if (series.Metadata.TotalCount == 1 && chapters.Count == 1 && chapters.First().IsSpecial) + } else if (series.Metadata.TotalCount == 1 && chapters.Count == 1 && chapters[0].IsSpecial) { // If a series has a TotalCount of 1 and there is only a Special, mark it as Complete series.Metadata.MaxCount = series.Metadata.TotalCount; diff --git a/API/Startup.cs b/API/Startup.cs index c44f6a5d65..8cf120789c 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -347,6 +347,7 @@ public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJo => { opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest; + opts.IncludeQueryInRequestPath = true; }); app.Use(async (context, next) => diff --git a/UI/Web/src/app/_models/series-detail/relation-kind.ts b/UI/Web/src/app/_models/series-detail/relation-kind.ts index 77470041ca..2de8e701fd 100644 --- a/UI/Web/src/app/_models/series-detail/relation-kind.ts +++ b/UI/Web/src/app/_models/series-detail/relation-kind.ts @@ -17,7 +17,7 @@ export enum RelationKind { Edition = 13 } -export const RelationKinds = [ +const RelationKindsUnsorted = [ {text: 'Prequel', value: RelationKind.Prequel}, {text: 'Sequel', value: RelationKind.Sequel}, {text: 'Spin Off', value: RelationKind.SpinOff}, @@ -31,3 +31,5 @@ export const RelationKinds = [ {text: 'Doujinshi', value: RelationKind.Doujinshi}, {text: 'Other', value: RelationKind.Other}, ]; + +export const RelationKinds = RelationKindsUnsorted.slice().sort((a, b) => a.text.localeCompare(b.text)); diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts index d5cc474994..772258cce3 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts @@ -38,6 +38,7 @@ export class BookLineOverlayComponent implements OnInit { @Input({required: true}) pageNumber: number = 0; @Input({required: true}) parent: ElementRef | undefined; @Output() refreshToC: EventEmitter = new EventEmitter(); + @Output() isOpen: EventEmitter = new EventEmitter(false); xPath: string = ''; selectedText: string = ''; @@ -84,6 +85,8 @@ export class BookLineOverlayComponent implements OnInit { if (!event.target) return; if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) { + event.preventDefault(); + event.stopPropagation(); this.reset(); return; } @@ -96,6 +99,7 @@ export class BookLineOverlayComponent implements OnInit { this.xPath = '//' + this.xPath; } + this.isOpen.emit(true); event.preventDefault(); event.stopPropagation(); } @@ -137,6 +141,7 @@ export class BookLineOverlayComponent implements OnInit { if (selection) { selection.removeAllRanges(); } + this.isOpen.emit(false); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index ccdc2aa2d2..38b5e9bb45 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -3,13 +3,14 @@
{{t('skip-header')}} - + @@ -18,11 +19,6 @@
{{t('title')}}
{{t('close-reader')}}
- - - - -
@@ -129,13 +125,14 @@
{{t('title')}}
- +
- +
-
- + @if(isLoading) {
{{t('loading-book')}}
-
- - + } @else { + ({{t('incognito-mode-label')}}) - {{bookTitle}} - + {{bookTitle}} + }
diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index c4cf2cdecf..b18569e869 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -185,6 +185,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { * Belongs to drawer component */ drawerOpen = false; + /** + * If the word/line overlay is open + */ + isLineOverlayOpen = false; /** * If the action bar is visible */ @@ -1630,20 +1634,21 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.cdRef.markForCheck(); } - // Responsible for handling pagination only handleContainerClick(event: MouseEvent) { - if (this.drawerOpen || ['action-bar', 'offcanvas-backdrop'].some(className => (event.target as Element).classList.contains(className))) { + if (this.drawerOpen || this.isLineOverlayOpen || ['action-bar', 'offcanvas-backdrop'].some(className => (event.target as Element).classList.contains(className))) { return; } - if (this.isCursorOverLeftPaginationArea(event)) { - this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD); - } else if (this.isCursorOverRightPaginationArea(event)) { - this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS) - } else { - this.toggleMenu(event); + if (this.clickToPaginate) { + if (this.isCursorOverLeftPaginationArea(event)) { + this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD); + } else if (this.isCursorOverRightPaginationArea(event)) { + this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS) + } } + + this.toggleMenu(event); } handleReaderClick(event: MouseEvent) { @@ -1706,4 +1711,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { refreshPersonalToC() { this.refreshPToC.emit(); } + + updateLineOverlayOpen(isOpen: boolean) { + // HACK: This hack allows the boolean to be changed to false so that the pagination doesn't trigger and move us to the next page when + // the book overlay is just closing + setTimeout(() => { + this.isLineOverlayOpen = isOpen; + this.cdRef.markForCheck(); + }, 10); + } } 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 0a66a93beb..3a3b33907b 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 @@ -106,9 +106,9 @@