From 2725e6042bfe221d915a5e813453a24faf446246 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 15 Sep 2021 17:25:18 -0700 Subject: [PATCH] WebP Support (#581) * Added trackby so when series scan event comes through, cards can update too * Added chapter boundary toasts on book reader * Handle closing the reader when in a reading list * Somehow the trackby save didn't happen * Fixed an issue where after opening a chapter info modal, then trying to open another in specials tab it would fail due to a pass by reference issue with our factory. * When a series update occurs, if we loose specials tab, but we were on it, reselect volumes/chapters tab * Fixed an issue where older releases would show as available, even though they were already installed. * Converted tabs within modals to use vertical orientation (except on mobile) * Implemented webp support. Only Safari does not support this format natively. MacOS users can use an alternative browser. * Refactored ScannerService and MetadataService to be fully async --- API.Tests/Parser/ParserTest.cs | 1 + API/Interfaces/Services/IMetadataService.cs | 2 +- API/Parser/Parser.cs | 2 +- API/Services/MetadataService.cs | 8 ++-- API/Services/Tasks/ScannerService.cs | 21 +++++----- .../app/_services/action-factory.service.ts | 39 +++++++++---------- UI/Web/src/app/_services/action.service.ts | 4 +- UI/Web/src/app/_services/reader.service.ts | 1 + .../admin/changelog/changelog.component.html | 6 +-- .../book-reader/book-reader.component.ts | 21 +++++----- .../edit-series-modal.component.html | 12 +++--- .../edit-series-modal.component.ts | 6 ++- .../series-card/series-card.component.ts | 1 - .../manga-reader/manga-reader.component.ts | 6 ++- .../series-detail.component.html | 8 ++-- .../series-detail/series-detail.component.ts | 14 +++++++ .../app/shared/_services/utility.service.ts | 15 +++++++ 17 files changed, 102 insertions(+), 65 deletions(-) diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index 5857a50c9..6830cde0d 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -156,6 +156,7 @@ namespace API.Tests.Parser [InlineData("test.png", true)] [InlineData(".test.jpg", false)] [InlineData("!test.jpg", false)] + [InlineData("test.webp", true)] public void IsImageTest(string filename, bool expected) { Assert.Equal(expected, IsImage(filename)); diff --git a/API/Interfaces/Services/IMetadataService.cs b/API/Interfaces/Services/IMetadataService.cs index f0595cf26..6d4d725cf 100644 --- a/API/Interfaces/Services/IMetadataService.cs +++ b/API/Interfaces/Services/IMetadataService.cs @@ -10,7 +10,7 @@ namespace API.Interfaces.Services /// /// /// - void RefreshMetadata(int libraryId, bool forceUpdate = false); + Task RefreshMetadata(int libraryId, bool forceUpdate = false); public bool UpdateMetadata(Chapter chapter, bool forceUpdate); public bool UpdateMetadata(Volume volume, bool forceUpdate); diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index a6cecc931..4b243aeac 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -13,7 +13,7 @@ namespace API.Parser public const string DefaultVolume = "0"; private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg)"; + public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp)"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string BookFileExtensions = @"\.epub|\.pdf"; public const string MacOsMetadataFileStartsWith = @"._"; diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 68c706bc3..4d6552156 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -187,10 +187,10 @@ namespace API.Services /// This can be heavy on memory first run /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - public void RefreshMetadata(int libraryId, bool forceUpdate = false) + public async Task RefreshMetadata(int libraryId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult(); + var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId); // PERF: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used _logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name); @@ -213,7 +213,7 @@ namespace API.Services } - if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result) + if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) { _logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); } @@ -228,7 +228,7 @@ namespace API.Services public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult(); + var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId); var series = library.Series.SingleOrDefault(s => s.Id == seriesId); if (series == null) diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 58f0c4491..6f494de2b 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -108,7 +108,7 @@ namespace API.Services.Tasks "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); - CleanupDbEntities(); + await CleanupDbEntities(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); // Tell UI that this series is done @@ -128,7 +128,7 @@ namespace API.Services.Tasks [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanLibraries() { - var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList(); + var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); foreach (var lib in libraries) { await ScanLibrary(lib.Id, false); @@ -151,8 +151,7 @@ namespace API.Services.Tasks Library library; try { - library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter() - .GetResult(); + library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId); } catch (Exception ex) { @@ -174,7 +173,7 @@ namespace API.Services.Tasks UpdateLibrary(library, series); _unitOfWork.LibraryRepository.Update(library); - if (Task.Run(() => _unitOfWork.CommitAsync()).Result) + if (await _unitOfWork.CommitAsync()) { _logger.LogInformation( "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", @@ -186,7 +185,7 @@ namespace API.Services.Tasks "There was a critical error that resulted in a failed scan. Please check logs and rescan"); } - CleanupAbandonedChapters(); + await CleanupAbandonedChapters(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate)); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete")); @@ -195,9 +194,9 @@ namespace API.Services.Tasks /// /// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters /// - private void CleanupAbandonedChapters() + private async Task CleanupAbandonedChapters() { - var cleanedUp = Task.Run(() => _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters()).Result; + var cleanedUp = await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); _logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp); } @@ -205,10 +204,10 @@ namespace API.Services.Tasks /// /// Cleans up any abandoned rows due to removals from Scan loop /// - private void CleanupDbEntities() + private async Task CleanupDbEntities() { - CleanupAbandonedChapters(); - var cleanedUp = Task.Run( () => _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries()).Result; + await CleanupAbandonedChapters(); + var cleanedUp = await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); _logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp); } diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 001b17ad3..450b577bc 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -113,17 +113,10 @@ export class ActionFactoryService { this.chapterActions.push({ action: Action.Edit, - title: 'Edit', + title: 'Info', callback: this.dummyCallback, requiresAdmin: false }); - - // this.readingListActions.push({ - // action: Action.Promote, // Should I just use CollectionTag modal-like instead? - // title: 'Delete', - // callback: this.dummyCallback, - // requiresAdmin: true - // }); } if (this.hasDownloadRole || this.isAdmin) { @@ -145,33 +138,39 @@ export class ActionFactoryService { } getLibraryActions(callback: (action: Action, library: Library) => void) { - this.libraryActions.forEach(action => action.callback = callback); - return this.libraryActions; + const actions = this.libraryActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } getSeriesActions(callback: (action: Action, series: Series) => void) { - this.seriesActions.forEach(action => action.callback = callback); - return this.seriesActions; + const actions = this.seriesActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } getVolumeActions(callback: (action: Action, volume: Volume) => void) { - this.volumeActions.forEach(action => action.callback = callback); - return this.volumeActions; + const actions = this.volumeActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } getChapterActions(callback: (action: Action, chapter: Chapter) => void) { - this.chapterActions.forEach(action => action.callback = callback); - return this.chapterActions; + const actions = this.chapterActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } getCollectionTagActions(callback: (action: Action, collectionTag: CollectionTag) => void) { - this.collectionTagActions.forEach(action => action.callback = callback); - return this.collectionTagActions; + const actions = this.collectionTagActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } getReadingListActions(callback: (action: Action, readingList: ReadingList) => void) { - this.readingListActions.forEach(action => action.callback = callback); - return this.readingListActions; + const actions = this.readingListActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; } filterBookmarksForFormat(action: ActionItem, series: Series) { diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index b36ac1aa9..a8af23b31 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -1,8 +1,8 @@ import { Injectable, OnDestroy } from '@angular/core'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; -import { forkJoin, Subject } from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component'; import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index a69cb52e1..dee47f7d0 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -150,6 +150,7 @@ export class ReaderService { return newRoute; } + getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { let params: {[key: string]: any} = {}; if (incognitoMode) { diff --git a/UI/Web/src/app/admin/changelog/changelog.component.html b/UI/Web/src/app/admin/changelog/changelog.component.html index 59d091db3..03ef9fe5d 100644 --- a/UI/Web/src/app/admin/changelog/changelog.component.html +++ b/UI/Web/src/app/admin/changelog/changelog.component.html @@ -2,10 +2,8 @@
{{update.updateTitle}}  - Installed - - Available - + Installed + Available

           Download
diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts
index d40134a3d..8f3984565 100644
--- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts
+++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts
@@ -172,8 +172,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
    */
   lastSeenScrollPartPath: string = '';
 
-
-
   /**
    * Hack: Override background color for reader and restore it onDestroy
    */
@@ -479,10 +477,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
     if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
       this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
         this.nextChapterId = chapterId;
-        this.loadChapter(chapterId, 'next');
+        this.loadChapter(chapterId, 'Next');
       });
     } else {
-      this.loadChapter(this.nextChapterId, 'next');
+      this.loadChapter(this.nextChapterId, 'Next');
     }
   }
 
@@ -502,14 +500,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
     if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId) {
       this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
         this.prevChapterId = chapterId;
-        this.loadChapter(chapterId, 'prev');
+        this.loadChapter(chapterId, 'Prev');
       });
     } else {
-      this.loadChapter(this.prevChapterId, 'prev');
+      this.loadChapter(this.prevChapterId, 'Prev');
     }
   }
 
-  loadChapter(chapterId: number, direction: 'next' | 'prev') {
+  loadChapter(chapterId: number, direction: 'Next' | 'Prev') {
     if (chapterId >= 0) {
       this.chapterId = chapterId;
       this.continuousChaptersStack.push(chapterId); 
@@ -517,11 +515,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
       const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
       window.history.replaceState({}, '', newRoute);
       this.init();
+      this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000});
     } else {
       // This will only happen if no actual chapter can be found
       this.toastr.warning('Could not find ' + direction + ' chapter');
       this.isLoading = false;
-      if (direction === 'prev') {
+      if (direction === 'Prev') {
         this.prevPageDisabled = true;
       } else {
         this.nextPageDisabled = true;
@@ -535,7 +534,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
   }
 
   closeReader() {
-    this.location.back();
+    if (this.readingListMode) {
+      this.router.navigateByUrl('lists/' + this.readingListId);
+    } else {
+      this.location.back();
+    }
   }
 
   resetSettings() {
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 3bb42a9bd..c354ac22d 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
@@ -6,13 +6,13 @@
             
         
     
-