From 58bbba29ccd21fefaac6f1d1f78f0e5e99de8f51 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Tue, 4 Oct 2022 19:40:34 -0500 Subject: [PATCH] Misc Polish (#1569) * Introduced a lock for DB work during the scan to hopefully reduce the concurrency issues * Don't allow multiple theme scans to occur * Fixed bulk actions not having all actions due to nested actionable menu changes * Refactored the Scan loop to be synchronous to avoid any issues. After first loop, no real performance issues. * Updated the LibraryWatcher when under many internal buffer full issues, to suspend watching for a full hour, to allow whatever downloading to complete. * Removed Semaphore as it's not needed anymore * Updated the output for logger to explicitly say from Kavita (if you're pushing to Seq) * Fixed a broken test * Fixed ReleaseYear not populating due to a change from a contributor around how to populate ReleaseYear. * Ensure when scan folder runs, that we don't double enqueue the same tasks. * Fixed user settings not loading the correct tab * Changed the Release Year -> Release * Added more refresh hooks in reader to hopefully ensure faster refreshes * Reset images between chapter loads to help flush image faster. Don't show broken image icon when an image is still loading. * Fixed the prefetcher not properly loading the correct images and hence, allowing a bit of lag between chapter loads. * Code smells --- API.Tests/Services/BookmarkServiceTests.cs | 71 ++++++++++++++++++- API.Tests/Services/ParseScannedFilesTests.cs | 7 +- API/Controllers/LibraryController.cs | 2 + API/Entities/AppUserBookmark.cs | 2 +- API/Logging/LogLevelOptions.cs | 2 +- API/Services/TaskScheduler.cs | 8 ++- API/Services/Tasks/Scanner/LibraryWatcher.cs | 34 +++++++++ .../Tasks/Scanner/ParseScannedFiles.cs | 6 +- API/Services/Tasks/Scanner/ProcessSeries.cs | 10 +-- API/Services/Tasks/ScannerService.cs | 24 +++++-- .../app/_services/action-factory.service.ts | 18 ++++- .../bulk-operations.component.ts | 17 +++-- .../src/app/cards/bulk-selection.service.ts | 32 ++++++++- .../series-info-cards.component.html | 2 +- .../manga-reader/manga-reader.component.html | 4 +- .../manga-reader/manga-reader.component.ts | 10 +-- .../user-preferences.component.ts | 4 +- 17 files changed, 208 insertions(+), 45 deletions(-) diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 88f0fc587..97c07a281 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -410,7 +410,7 @@ public class BookmarkServiceTests #region Misc [Fact] - public async Task ShouldNotDeleteBookmarkOnChapterDeletion() + public async Task ShouldNotDeleteBookmark_OnChapterDeletion() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); @@ -462,8 +462,6 @@ public class BookmarkServiceTests var ds = new DirectoryService(Substitute.For>(), filesystem); - var bookmarkService = Create(ds); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); var vol = await _unitOfWork.VolumeRepository.GetVolumeAsync(1); vol.Chapters = new List(); @@ -475,5 +473,72 @@ public class BookmarkServiceTests Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1)); } + + [Fact] + public async Task ShouldNotDeleteBookmark_OnVolumeDeletion() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); + filesystem.AddFile($"{BookmarkDirectory}1/1/0001.jpg", new MockFileData("123")); + + // Delete all Series to reset state + await ResetDB(); + var series = new Series() + { + Name = "Test", + Library = new Library() + { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + new Volume() + { + Chapters = new List() + { + new Chapter() + { + + } + } + } + } + }; + + _context.Series.Add(series); + + + _context.AppUser.Add(new AppUser() + { + UserName = "Joe", + Bookmarks = new List() + { + new AppUserBookmark() + { + Page = 1, + ChapterId = 1, + FileName = $"1/1/0001.jpg", + SeriesId = 1, + VolumeId = 1 + } + } + }); + + await _context.SaveChangesAsync(); + + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); + Assert.NotEmpty(user.Bookmarks); + + series.Volumes = new List(); + _unitOfWork.SeriesRepository.Update(series); + await _unitOfWork.CommitAsync(); + + + var ds = new DirectoryService(Substitute.For>(), filesystem); + Assert.Single(ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories)); + Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1)); + } + #endregion } diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index 4139168f1..d5e235a80 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -53,7 +53,7 @@ internal class MockReadingItemService : IReadingItemService public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public ParserInfo Parse(string path, string rootPath, LibraryType type) @@ -245,11 +245,11 @@ public class ParseScannedFilesTests var parsedSeries = new Dictionary>(); - void TrackFiles(Tuple> parsedInfo) + Task TrackFiles(Tuple> parsedInfo) { var skippedScan = parsedInfo.Item1; var parsedFiles = parsedInfo.Item2; - if (parsedFiles.Count == 0) return; + if (parsedFiles.Count == 0) return Task.CompletedTask; var foundParsedSeries = new ParsedSeries() { @@ -259,6 +259,7 @@ public class ParseScannedFilesTests }; parsedSeries.Add(foundParsedSeries, parsedFiles); + return Task.CompletedTask; } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 031ae12d7..56ab5a41e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -213,9 +213,11 @@ public class LibraryController : BaseApiController { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + // Validate user has Admin privileges var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); if (!isAdmin) return BadRequest("API key must belong to an admin"); + if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path"); dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath); diff --git a/API/Entities/AppUserBookmark.cs b/API/Entities/AppUserBookmark.cs index 78d43fc9e..faaf431b3 100644 --- a/API/Entities/AppUserBookmark.cs +++ b/API/Entities/AppUserBookmark.cs @@ -11,8 +11,8 @@ public class AppUserBookmark : IEntityDate { public int Id { get; set; } public int Page { get; set; } - public int VolumeId { get; set; } public int SeriesId { get; set; } + public int VolumeId { get; set; } public int ChapterId { get; set; } /// diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index f5e877b79..34d7d353f 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -40,7 +40,7 @@ public static class LogLevelOptions public static LoggerConfiguration CreateConfig(LoggerConfiguration configuration) { - const string outputTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {ThreadId}] [{Level}] {SourceContext} {Message:lj}{NewLine}{Exception}"; + const string outputTemplate = "[Kavita] [{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {ThreadId}] [{Level}] {SourceContext} {Message:lj}{NewLine}{Exception}"; return configuration .MinimumLevel .ControlledBy(LogLevelSwitch) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 055a73e08..8a7a62471 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -158,7 +158,13 @@ public class TaskScheduler : ITaskScheduler public void ScanSiteThemes() { - _logger.LogInformation("Starting Site Theme scan"); + if (HasAlreadyEnqueuedTask("ThemeService", "Scan", Array.Empty(), ScanQueue)) + { + _logger.LogInformation("A Theme Scan is already running"); + return; + } + + _logger.LogInformation("Enqueueing Site Theme scan"); BackgroundJob.Enqueue(() => _themeService.Scan()); } diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 6788872da..02c6fa8f1 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -51,6 +51,14 @@ public class LibraryWatcher : ILibraryWatcher /// The Job will be enqueued instantly private readonly TimeSpan _queueWaitTime; + /// + /// Counts within a time frame how many times the buffer became full. Is used to reschedule LibraryWatcher to start monitoring much later rather than instantly + /// + private int _bufferFullCounter = 0; + + private DateTime _lastBufferOverflow = DateTime.MinValue; + + public LibraryWatcher(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger, IScannerService scannerService, IHostEnvironment environment) { @@ -118,6 +126,9 @@ public class LibraryWatcher : ILibraryWatcher public async Task RestartWatching() { _logger.LogDebug("[LibraryWatcher] Restarting watcher"); + + UpdateBufferOverflow(); + StopWatching(); await StartWatching(); } @@ -151,6 +162,15 @@ public class LibraryWatcher : ILibraryWatcher 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"); + _bufferFullCounter += 1; + _lastBufferOverflow = DateTime.Now; + + if (_bufferFullCounter >= 3) + { + _logger.LogInformation("[LibraryWatcher] Internal buffer has been overflown multiple times in past 10 minutes. Suspending file watching for an hour"); + BackgroundJob.Schedule(() => RestartWatching(), TimeSpan.FromHours(1)); + return; + } Task.Run(RestartWatching); } @@ -162,8 +182,11 @@ public class LibraryWatcher : ILibraryWatcher /// This is public only because Hangfire will invoke it. Do not call external to this class. /// File or folder that changed /// If the change is on a directory and not a file + // ReSharper disable once MemberCanBePrivate.Global public async Task ProcessChange(string filePath, bool isDirectoryChange = false) { + UpdateBufferOverflow(); + var sw = Stopwatch.StartNew(); _logger.LogDebug("[LibraryWatcher] Processing change of {FilePath}", filePath); try @@ -232,4 +255,15 @@ public class LibraryWatcher : ILibraryWatcher // Select the first folder and join with library folder, this should give us the folder to scan. return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder.First())); } + + private void UpdateBufferOverflow() + { + if (_bufferFullCounter == 0) return; + // If the last buffer overflow is over 5 mins back, we can remove a buffer count + if (_lastBufferOverflow < DateTime.Now.Subtract(TimeSpan.FromMinutes(5))) + { + _bufferFullCounter = Math.Min(0, _bufferFullCounter - 1); + _lastBufferOverflow = DateTime.Now; + } + } } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 03b4ca9d5..0b8641789 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -223,7 +223,7 @@ public class ParseScannedFiles /// public async Task ScanLibrariesForSeries(LibraryType libraryType, IEnumerable folders, string libraryName, bool isLibraryScan, - IDictionary> seriesPaths, Action>> processSeriesInfos, bool forceCheck = false) + IDictionary> seriesPaths, Func>, Task> processSeriesInfos, bool forceCheck = false) { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started)); @@ -242,7 +242,7 @@ public class ParseScannedFiles Series = fp.SeriesName, Format = fp.Format, }).ToList(); - processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); + await processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); _logger.LogDebug("Skipped File Scan for {Folder} as it hasn't changed since last scan", folder); return; } @@ -280,7 +280,7 @@ public class ParseScannedFiles { if (scannedSeries[series].Count > 0 && processSeriesInfos != null) { - processSeriesInfos.Invoke(new Tuple>(false, scannedSeries[series])); + await processSeriesInfos.Invoke(new Tuple>(false, scannedSeries[series])); } } }, forceCheck); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index c13a93758..dbf3bc2bf 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; @@ -48,8 +49,6 @@ public class ProcessSeries : IProcessSeries private volatile IList _people; private volatile IList _tags; - - public ProcessSeries(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService, IFileService fileService, IMetadataService metadataService, IWordCountAnalyzerService wordCountAnalyzerService) @@ -167,7 +166,9 @@ public class ProcessSeries : IProcessSeries catch (Exception ex) { await _unitOfWork.RollbackAsync(); - _logger.LogCritical(ex, "[ScannerService] There was an issue writing to the database for series {@SeriesName}", series.Name); + _logger.LogCritical(ex, + "[ScannerService] There was an issue writing to the database for series {@SeriesName}", + series.Name); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series}", @@ -234,7 +235,7 @@ public class ProcessSeries : IProcessSeries var chapters = series.Volumes.SelectMany(volume => volume.Chapters).ToList(); // Update Metadata based on Chapter metadata - series.Metadata.ReleaseYear = chapters.Min(c => c.ReleaseDate.Year); + series.Metadata.ReleaseYear = chapters.Select(v => v.ReleaseDate.Year).Where(y => y >= 1000).Min(); if (series.Metadata.ReleaseYear < 1000) { @@ -439,6 +440,7 @@ public class ProcessSeries : IProcessSeries _logger.LogDebug("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name); foreach (var volumeNumber in distinctVolumes) { + _logger.LogDebug("[ScannerService] Looking up volume for {volumeNumber}", volumeNumber); var volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber); if (volume == null) { diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index f64e85302..698116463 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -102,6 +102,12 @@ public class ScannerService : IScannerService var seriesId = await _unitOfWork.SeriesRepository.GetSeriesIdByFolder(folder); if (seriesId > 0) { + if (TaskScheduler.HasAlreadyEnqueuedTask(Name, "ScanSeries", + new object[] {seriesId, true})) + { + _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); + return; + } BackgroundJob.Enqueue(() => ScanSeries(seriesId, true)); return; } @@ -119,6 +125,12 @@ public class ScannerService : IScannerService var library = libraries.FirstOrDefault(l => l.Folders.Select(Scanner.Parser.Parser.NormalizePath).Contains(libraryFolder)); if (library != null) { + if (TaskScheduler.HasAlreadyEnqueuedTask(Name, "ScanLibrary", + new object[] {library.Id, false})) + { + _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); + return; + } BackgroundJob.Enqueue(() => ScanLibrary(library.Id, false)); } } @@ -175,13 +187,11 @@ public class ScannerService : IScannerService } var parsedSeries = new Dictionary>(); - var processTasks = new List(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name)); await _processSeries.Prime(); - void TrackFiles(Tuple> parsedInfo) + async Task TrackFiles(Tuple> parsedInfo) { var parsedFiles = parsedInfo.Item2; if (parsedFiles.Count == 0) return; @@ -198,7 +208,7 @@ public class ScannerService : IScannerService return; } - processTasks.Add(_processSeries.ProcessSeriesAsync(parsedFiles, library)); + await _processSeries.ProcessSeriesAsync(parsedFiles, library); parsedSeries.Add(foundParsedSeries, parsedFiles); } @@ -424,7 +434,7 @@ public class ScannerService : IScannerService await _processSeries.Prime(); var processTasks = new List(); - void TrackFiles(Tuple> parsedInfo) + async Task TrackFiles(Tuple> parsedInfo) { var skippedScan = parsedInfo.Item1; var parsedFiles = parsedInfo.Item2; @@ -452,7 +462,7 @@ public class ScannerService : IScannerService seenSeries.Add(foundParsedSeries); - processTasks.Add(_processSeries.ProcessSeriesAsync(parsedFiles, library)); + await _processSeries.ProcessSeriesAsync(parsedFiles, library); } @@ -512,7 +522,7 @@ public class ScannerService : IScannerService } private async Task ScanFiles(Library library, IEnumerable dirs, - bool isLibraryScan, Action>> processSeriesInfos = null, bool forceChecks = false) + bool isLibraryScan, Func>, Task> processSeriesInfos = null, bool forceChecks = false) { var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); var scanWatch = Stopwatch.StartNew(); diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 3ff5ee74c..ffbd54c84 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -543,12 +543,28 @@ export class ActionFactoryService { }); } - private applyCallbackToList(list: Array>, callback: (action: ActionItem, data: any) => void): Array> { + public applyCallbackToList(list: Array>, callback: (action: ActionItem, data: any) => void): Array> { const actions = list.map((a) => { return { ...a }; }); actions.forEach((action) => this.applyCallback(action, callback)); return actions; } + + // Checks the whole tree for the action and returns true if it exists + public hasAction(actions: Array>, action: Action) { + var actionFound = false; + + if (actions.length === 0) return actionFound; + + for (let i = 0; i < actions.length; i++) + { + if (actions[i].action === action) return true; + if (this.hasAction(actions[i].children, action)) return true; + } + + + return actionFound; + } } diff --git a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts index c93a64f5d..788f2b428 100644 --- a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts +++ b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; -import { Action, ActionItem } from 'src/app/_services/action-factory.service'; +import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { BulkSelectionService } from '../bulk-selection.service'; @Component({ @@ -24,14 +24,15 @@ export class BulkOperationsComponent implements OnInit, OnDestroy { return Action; } - constructor(public bulkSelectionService: BulkSelectionService, private readonly cdRef: ChangeDetectorRef) { } + constructor(public bulkSelectionService: BulkSelectionService, private readonly cdRef: ChangeDetectorRef, + private actionFactoryService: ActionFactoryService) { } ngOnInit(): void { this.bulkSelectionService.actions$.pipe(takeUntil(this.onDestory)).subscribe(actions => { - actions.forEach(a => a.callback = this.actionCallback.bind(this)); - this.actions = actions; - this.hasMarkAsRead = this.actions.filter(act => act.action === Action.MarkAsRead).length > 0; - this.hasMarkAsUnread = this.actions.filter(act => act.action === Action.MarkAsUnread).length > 0; + // We need to do a recursive callback apply + this.actions = this.actionFactoryService.applyCallbackToList(actions, this.actionCallback.bind(this)); + this.hasMarkAsRead = this.actionFactoryService.hasAction(this.actions, Action.MarkAsRead); + this.hasMarkAsUnread = this.actionFactoryService.hasAction(this.actions, Action.MarkAsUnread); this.cdRef.markForCheck(); }); } @@ -46,9 +47,7 @@ export class BulkOperationsComponent implements OnInit, OnDestroy { } performAction(action: ActionItem) { - if (typeof action.callback === 'function') { - action.callback(action, null); - } + this.actionCallback(action, null); } executeAction(action: Action) { diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 8592786e5..eb30fc3e6 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -142,16 +142,17 @@ export class BulkSelectionService { getActions(callback: (action: ActionItem, data: any) => void) { // checks if series is present. If so, returns only series actions // else returns volume/chapter items - const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection, Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList]; + const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection, + Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList]; if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) { - return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action)); + return this.applyFilterToList(this.actionFactory.getSeriesActions(callback), allowedActions); } if (Object.keys(this.selectedCards).filter(item => item === 'bookmark').length > 0) { return this.actionFactory.getBookmarkActions(callback); } - return this.actionFactory.getVolumeActions(callback).filter(item => allowedActions.includes(item.action)); + return this.applyFilterToList(this.actionFactory.getVolumeActions(callback), allowedActions); } private debugLog(message: string, extraData?: any) { @@ -163,4 +164,29 @@ export class BulkSelectionService { console.log(message); } } + + private applyFilter(action: ActionItem, allowedActions: Array) { + + var ret = false; + if (action.action === Action.Submenu || allowedActions.includes(action.action)) { + // Do something + ret = true; + } + + if (action.children === null || action.children?.length === 0) return ret; + + action.children = action.children.filter((childAction) => this.applyFilter(childAction, allowedActions)); + + // action.children?.forEach((childAction) => { + // this.applyFilter(childAction, allowedActions); + // }); + return ret; + } + + private applyFilterToList(list: Array>, allowedActions: Array): Array> { + const actions = list.map((a) => { + return { ...a }; + }); + return actions.filter(action => this.applyFilter(action, allowedActions)); + } } diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html index 8c8ecc8c1..309dab25f 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html @@ -1,7 +1,7 @@
- + {{seriesMetadata.releaseYear}}
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index d1d6b221d..21c28f7fd 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -66,11 +66,11 @@ 'fit-to-height-double-offset': FittingOption === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage, 'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}" [style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle" (dblclick)="bookmarkPage($event)"> -  - +
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index ad00b0d35..12d179ef1 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -661,6 +661,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.pageNum = 0; this.pagingDirection = PAGING_DIRECTION.FORWARD; this.inSetup = true; + this.canvasImage.src = ''; + this.canvasImage2.src = ''; this.cdRef.markForCheck(); if (this.goToPageEvent) { @@ -1042,8 +1044,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.isCoverImage() || this.isWideImage(this.canvasImagePrev) ) ? 2 : 1; - } - if (this.layoutMode === LayoutMode.DoubleReversed) { + } else if (this.layoutMode === LayoutMode.DoubleReversed) { pageAmount = !( this.isCoverImage() || this.isCoverImage(this.pageNum - 1) @@ -1300,13 +1301,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * and also maintains page info (wide image, etc) due to onload event. */ prefetch() { - for(let i = 1; i <= PREFETCH_PAGES - 3; i++) { + for(let i = 0; i <= PREFETCH_PAGES - 3; i++) { const numOffset = this.pageNum + i; if (numOffset > this.maxPages - 1) continue; - const index = numOffset % this.cachedImages.length; + const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length; if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) { this.cachedImages[index].src = this.getPageUrl(numOffset); + this.cachedImages[index].onload = () => this.cdRef.markForCheck(); } } diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index ffb822ded..92e25bc4f 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -61,7 +61,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { {title: 'Theme', fragment: FragmentID.Theme}, {title: 'Devices', fragment: FragmentID.Devices}, ]; - active = this.tabs[0]; + active = this.tabs[1]; opdsEnabled: boolean = false; makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)}; @@ -87,7 +87,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { if (tab.length > 0) { this.active = tab[0]; } else { - this.active = this.tabs[0]; // Default to first tab + this.active = this.tabs[1]; // Default to preferences } this.cdRef.markForCheck(); });