From 553f9b0d98ba85a6add00a9f62c61639f507e843 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 14 Apr 2022 16:55:06 -0500 Subject: [PATCH] Fixes, Tweaks, and Series Filtering (#1217) * From previous fix, added the other locking conditions on the update series metadata. * Fixed a bug where custom series, collection tag, and reading list covers weren't being removed on cleanup. * Ensure reading list detail has a margin to align to the standard * Refactored some event stuff to use dedicated consts. Introduced a new event when users read something, which can update progress bars on cards. * Added recomended and library tags to the library detail page. This will eventually offer more custom analytics * Cleanup some code onc arousel * Adjusted scale to height/width css to better fit * Small css tweaks to better center images in the manga reader in both axis. This takes care of double page rendering as well. * When a special has a Title set in the metadata, on series detail page, show that on the card rather than filename. * Fixed a bug where when paging in manga reader, the scroll to top wasn't working due to changing where scrolling is done * More css goodness for rendering images in manga reader * Fixed a bug where clearing a typeahead externally wouldn't clear the x button * Fixed a bug where filering then using keyboard would select wrong option * Added a new sorting field for Last Chapter Added (new field) to get a similar on deck feel. * Tweaked recently updated to hit the NFR of 500ms (300ms fresh start) and still give a much better experience. * Refactored On deck to now go to all series and also sort by last updated. Recently Added Series now loads all series with sort by created. * Some tweaks on css for cover image chooser * Fixed a bug in pagination control where multiple pagination events could trigger on load and thus multiple requests for data on parent controller. * Updated edit series modal to show when the last chapter was added and when user last read it. * Implemented a highlight on the fitler button when a filter is active. * Refactored metadata filter screens to perserve the filters in the url and thus when navigating back and forth, it will retain. users should click side nav to reset the state. * Hide middle section on companion bar on phones * Cleaned up some prefilters and console.logs * Don't open drawer by default when a filter is active --- API.Tests/Services/CleanupServiceTests.cs | 80 +++-- API.Tests/Services/ReaderServiceTests.cs | 81 ++--- API/Controllers/CollectionController.cs | 2 +- API/Controllers/ReaderController.cs | 43 +-- API/Controllers/ReadingListController.cs | 19 +- API/Controllers/UploadController.cs | 4 +- API/DTOs/Filtering/SortField.cs | 1 + API/DTOs/SeriesDto.cs | 4 + API/Data/DbFactory.cs | 23 ++ .../Repositories/ReadingListRepository.cs | 10 + API/Data/Repositories/SeriesRepository.cs | 61 +--- API/Services/ImageService.cs | 5 +- API/Services/MetadataService.cs | 8 +- API/Services/ReaderService.cs | 11 +- API/Services/SeriesService.cs | 42 ++- API/Services/Tasks/CleanupService.cs | 11 + API/SignalR/MessageFactory.cs | 32 ++ .../app/_interceptors/error.interceptor.ts | 4 +- .../events/user-progress-update-event.ts | 10 + UI/Web/src/app/_models/series-filter.ts | 3 +- UI/Web/src/app/_models/series.ts | 4 + UI/Web/src/app/_services/account.service.ts | 4 +- .../src/app/_services/message-hub.service.ts | 13 +- .../admin/edit-user/edit-user.component.ts | 5 +- .../manage-library.component.ts | 2 - .../app/all-series/all-series.component.html | 6 +- .../app/all-series/all-series.component.ts | 28 +- .../card-details-modal.component.ts | 1 - .../edit-series-modal.component.html | 5 + .../edit-series-modal.component.ts | 4 - .../card-detail-layout.component.ts | 1 + .../cards/card-item/card-item.component.html | 2 +- .../cards/card-item/card-item.component.ts | 29 +- .../cover-image-chooser.component.html | 16 +- .../series-card/series-card.component.html | 2 +- .../carousel-reel.component.html | 12 +- .../carousel-reel/carousel-reel.component.ts | 19 +- .../collection-detail.component.html | 2 +- .../collection-detail.component.scss | 2 +- .../collection-detail.component.ts | 68 ++--- .../library-detail.component.html | 46 ++- .../library-detail.component.ts | 44 ++- UI/Web/src/app/library/library.component.html | 8 +- UI/Web/src/app/library/library.component.ts | 50 +++- .../manga-reader/manga-reader.component.scss | 29 +- .../manga-reader/manga-reader.component.ts | 13 +- .../metadata-filter.component.html | 1 + .../metadata-filter.component.ts | 29 +- .../reading-list-detail.component.html | 2 +- .../reading-lists.component.html | 2 +- .../recently-added.component.html | 2 +- .../recently-added.component.ts | 12 +- .../confirm-reset-password.component.ts | 2 +- .../series-detail.component.html | 1 + .../_services/filter-utilities.service.ts | 276 ++++++++++++++++++ .../app/shared/_services/utility.service.ts | 154 ++-------- .../src/app/shared/image/image.component.html | 6 +- .../side-nav-companion-bar.component.html | 4 +- .../side-nav-companion-bar.component.ts | 7 +- .../sidenav/side-nav/side-nav.component.ts | 1 - .../app/theme-test/theme-test.component.ts | 6 +- .../app/typeahead/typeahead.component.html | 4 +- .../src/app/typeahead/typeahead.component.ts | 23 +- 63 files changed, 864 insertions(+), 537 deletions(-) create mode 100644 UI/Web/src/app/_models/events/user-progress-update-event.ts create mode 100644 UI/Web/src/app/shared/_services/filter-utilities.service.ts diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 1dd131112..2cd7da805 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -11,6 +11,7 @@ using API.Entities.Enums; using API.Services; using API.Services.Tasks; using API.SignalR; +using API.Tests.Helpers; using AutoMapper; using Microsoft.AspNetCore.SignalR; using Microsoft.Data.Sqlite; @@ -125,23 +126,23 @@ public class CleanupServiceTests public async Task DeleteSeriesCoverImages_ShouldDeleteAll() { var filesystem = CreateFileSystem(); - filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); var s = DbFactory.Series("Test 1"); - s.CoverImage = "series_01.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); - s.CoverImage = "series_03.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 3"); - s.CoverImage = "series_1000.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); @@ -158,20 +159,20 @@ public class CleanupServiceTests public async Task DeleteSeriesCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); - filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); // Add 2 series with cover images var s = DbFactory.Series("Test 1"); - s.CoverImage = "series_01.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); - s.CoverImage = "series_03.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); @@ -242,9 +243,9 @@ public class CleanupServiceTests public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); - filesystem.AddFile($"{CoverImageDirectory}tag_01.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}tag_02.jpg", new MockFileData("")); - filesystem.AddFile($"{CoverImageDirectory}tag_1000.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); @@ -255,9 +256,9 @@ public class CleanupServiceTests s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something", - CoverImage ="tag_01.jpg" + CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg" }); - s.CoverImage = "series_01.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); @@ -266,9 +267,9 @@ public class CleanupServiceTests s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something 2", - CoverImage ="tag_02.jpg" + CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg" }); - s.CoverImage = "series_03.jpg"; + s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); @@ -285,6 +286,49 @@ public class CleanupServiceTests #endregion + #region DeleteReadingListCoverImages + [Fact] + public async Task DeleteReadingListCoverImages_ShouldNotDeleteLinkedFiles() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(1)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(2)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData("")); + + // Delete all Series to reset state + await ResetDB(); + + _context.Users.Add(new AppUser() + { + UserName = "Joe", + ReadingLists = new List() + { + new ReadingList() + { + Title = "Something", + NormalizedTitle = API.Parser.Parser.Normalize("Something"), + CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg" + }, + new ReadingList() + { + Title = "Something 2", + NormalizedTitle = API.Parser.Parser.Normalize("Something 2"), + CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg" + } + } + }); + + await _context.SaveChangesAsync(); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + ds); + + await cleanupService.DeleteReadingListCoverImages(); + + Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); + } + #endregion + #region CleanupCacheDirectory [Fact] diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 8439c69d3..cfb89935a 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -11,6 +11,7 @@ using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Services; +using API.SignalR; using API.Tests.Helpers; using AutoMapper; using Microsoft.Data.Sqlite; @@ -147,7 +148,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); Assert.Equal(0, await readerService.CapPageToChapter(1, -1)); Assert.Equal(1, await readerService.CapPageToChapter(1, 10)); @@ -191,7 +192,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var successful = await readerService.SaveReadingProgress(new ProgressDto() { @@ -240,7 +241,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var successful = await readerService.SaveReadingProgress(new ProgressDto() { @@ -310,7 +311,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1); readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); @@ -360,7 +361,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); @@ -420,7 +421,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); @@ -466,7 +467,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); @@ -508,7 +509,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1); @@ -551,7 +552,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1); @@ -587,7 +588,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); @@ -628,7 +629,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); @@ -669,7 +670,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); @@ -708,7 +709,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); @@ -751,7 +752,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 3, 1); @@ -793,7 +794,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1); @@ -846,7 +847,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); @@ -892,7 +893,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1); @@ -934,7 +935,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1); @@ -972,7 +973,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); @@ -1007,7 +1008,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); @@ -1047,7 +1048,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); @@ -1095,7 +1096,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,5, 1); var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); @@ -1137,7 +1138,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); @@ -1178,7 +1179,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1); @@ -1221,7 +1222,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); @@ -1276,7 +1277,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetContinuePoint(1, 1); @@ -1321,7 +1322,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume chapters and 1st of second volume await readerService.SaveReadingProgress(new ProgressDto() @@ -1404,7 +1405,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume and 1st chapter of second volume await readerService.SaveReadingProgress(new ProgressDto() @@ -1470,7 +1471,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume chapters and 1st of second volume await readerService.SaveReadingProgress(new ProgressDto() @@ -1538,7 +1539,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var nextChapter = await readerService.GetContinuePoint(1, 1); Assert.Equal("1", nextChapter.Range); @@ -1575,7 +1576,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume chapters and 1st of second volume await readerService.SaveReadingProgress(new ProgressDto() @@ -1640,7 +1641,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume chapters and 1st of second volume var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); @@ -1681,7 +1682,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); // Save progress on first volume chapters and 1st of second volume await readerService.SaveReadingProgress(new ProgressDto() @@ -1753,7 +1754,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await readerService.MarkSeriesAsRead(user, 1); await _context.SaveChangesAsync(); @@ -1801,7 +1802,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await readerService.MarkChaptersUntilAsRead(user, 1, 5); @@ -1844,7 +1845,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f); @@ -1888,7 +1889,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await readerService.MarkChaptersUntilAsRead(user, 1, 2); @@ -1947,7 +1948,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); const int markReadUntilNumber = 47; @@ -2027,7 +2028,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); await _context.SaveChangesAsync(); @@ -2078,7 +2079,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>()); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 3abe97d47..a98e28952 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -157,7 +157,7 @@ namespace API.Controllers tag.CoverImageLocked = false; tag.CoverImage = string.Empty; await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false); + MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false); _unitOfWork.CollectionTagRepository.Update(tag); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index e2d067abd..04d7e59c3 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -11,6 +11,8 @@ using API.Entities; using API.Extensions; using API.Services; using API.Services.Tasks; +using API.SignalR; +using Hangfire; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -25,23 +27,21 @@ namespace API.Controllers private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IReaderService _readerService; - private readonly IDirectoryService _directoryService; - private readonly ICleanupService _cleanupService; private readonly IBookmarkService _bookmarkService; + private readonly IEventHub _eventHub; /// public ReaderController(ICacheService cacheService, IUnitOfWork unitOfWork, ILogger logger, - IReaderService readerService, IDirectoryService directoryService, - ICleanupService cleanupService, IBookmarkService bookmarkService) + IReaderService readerService, IBookmarkService bookmarkService, + IEventHub eventHub) { _cacheService = cacheService; _unitOfWork = unitOfWork; _logger = logger; _readerService = readerService; - _directoryService = directoryService; - _cleanupService = cleanupService; _bookmarkService = bookmarkService; + _eventHub = eventHub; } /// @@ -111,13 +111,14 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); - if (await _unitOfWork.CommitAsync()) - { - return Ok(); - } + if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress"); - - return BadRequest("There was an issue saving progress"); + // var series = new List() + // {await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(markReadDto.SeriesId, user.Id)}; + // await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); + // await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + // MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, series[0], series[0].Pages)); + return Ok(); } @@ -132,13 +133,19 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); - if (await _unitOfWork.CommitAsync()) - { - return Ok(); - } + if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress"); - - return BadRequest("There was an issue saving progress"); + // Should I do this for every chapter? Maybe in a background task? + // foreach (var chapterId in await + // _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new List() {markReadDto.SeriesId})) + // { + // await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + // MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, chapterId, MessageFactoryEntityTypes.Chapter, 0)); + // } + // + // await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + // MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, MessageFactoryEntityTypes.Series, 0)); + return Ok(); } /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 701f415e6..1b72b20d2 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -211,12 +211,8 @@ namespace API.Controllers { return BadRequest("A list of this name already exists"); } - user.ReadingLists.Add(new ReadingList() - { - Promoted = false, - Title = dto.Title, - Summary = string.Empty - }); + + user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false)); if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list"); @@ -257,7 +253,7 @@ namespace API.Controllers readingList.CoverImageLocked = false; readingList.CoverImage = string.Empty; await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false); + MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false); _unitOfWork.ReadingListRepository.Update(readingList); } @@ -474,14 +470,7 @@ namespace API.Controllers foreach (var chapter in chaptersForSeries) { if (existingChapterExists.Contains(chapter.Id)) continue; - - readingList.Items.Add(new ReadingListItem() - { - Order = index, - ChapterId = chapter.Id, - SeriesId = seriesId, - VolumeId = chapter.VolumeId - }); + readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id)); index += 1; } diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 2ecc0dc38..98005e091 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -149,7 +149,7 @@ namespace API.Controllers { await _unitOfWork.CommitAsync(); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false); + MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false); return Ok(); } @@ -196,7 +196,7 @@ namespace API.Controllers { await _unitOfWork.CommitAsync(); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false); + MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false); return Ok(); } diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs index 0e465f6aa..3d78494bd 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/API/DTOs/Filtering/SortField.cs @@ -5,4 +5,5 @@ public enum SortField SortName = 1, CreatedDate = 2, LastModifiedDate = 3, + LastChapterAdded = 4 } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 5f76634ff..a5756ceca 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -22,6 +22,10 @@ namespace API.DTOs /// public DateTime LatestReadDate { get; set; } /// + /// DateTime representing last time a chapter was added to the Series + /// + public DateTime LastChapterAdded { get; set; } + /// /// Rating from logged in user. Calculated at API-time. /// public int UserRating { get; set; } diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs index 46ebfbf10..41857a455 100644 --- a/API/Data/DbFactory.cs +++ b/API/Data/DbFactory.cs @@ -82,6 +82,29 @@ namespace API.Data }; } + public static ReadingList ReadingList(string title, string summary, bool promoted) + { + return new ReadingList() + { + NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(), + Title = title?.Trim(), + Summary = summary?.Trim(), + Promoted = promoted, + Items = new List() + }; + } + + public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId) + { + return new ReadingListItem() + { + Order = index, + ChapterId = chapterId, + SeriesId = seriesId, + VolumeId = volumeId + }; + } + public static Genre Genre(string name, bool external) { return new Genre() diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 007bc3884..0294d6224 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -27,6 +27,7 @@ public interface IReadingListRepository void Update(ReadingList list); Task Count(); Task GetCoverImageAsync(int readingListId); + Task> GetAllCoverImagesAsync(); } public class ReadingListRepository : IReadingListRepository @@ -59,6 +60,15 @@ public class ReadingListRepository : IReadingListRepository .SingleOrDefaultAsync(); } + public async Task> GetAllCoverImagesAsync() + { + return await _context.ReadingList + .Select(t => t.CoverImage) + .Where(t => !string.IsNullOrEmpty(t)) + .AsNoTracking() + .ToListAsync(); + } + public void Remove(ReadingListItem item) { _context.ReadingListItem.Remove(item); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b3e89495d..30163352f 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -95,7 +95,7 @@ public interface ISeriesRepository Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); Task> GetAllLanguagesForLibrariesAsync(List libraryIds); Task> GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); - Task> GetRecentlyUpdatedSeries(int userId); + Task> GetRecentlyUpdatedSeries(int userId, int pageSize = 30); } public class SeriesRepository : ISeriesRepository @@ -231,7 +231,7 @@ public class SeriesRepository : ISeriesRepository /// /// Gets all series /// - /// + /// Restricts to just one library /// /// /// @@ -617,6 +617,7 @@ public class SeriesRepository : ISeriesRepository LastReadingProgress = _context.AppUserProgresses .Where(p => p.Id == progress.Id && p.AppUserId == userId) .Max(p => p.LastModified), + LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified), s.LastChapterAdded }); if (cutoffOnDate) @@ -628,8 +629,8 @@ public class SeriesRepository : ISeriesRepository var retSeries = query.Where(s => s.AppUserId == userId && s.PagesRead > 0 && s.PagesRead < s.Series.Pages) - .OrderByDescending(s => s.LastReadingProgress) - .ThenByDescending(s => s.LastChapterAdded) + .OrderByDescending(s => s.LastChapterAdded) + .ThenByDescending(s => s.LastReadingProgress) .Select(s => s.Series) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -680,6 +681,7 @@ public class SeriesRepository : ISeriesRepository SortField.SortName => query.OrderBy(s => s.SortName), SortField.CreatedDate => query.OrderBy(s => s.Created), SortField.LastModifiedDate => query.OrderBy(s => s.LastModified), + SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded), _ => query }; } @@ -690,6 +692,7 @@ public class SeriesRepository : ISeriesRepository SortField.SortName => query.OrderByDescending(s => s.SortName), SortField.CreatedDate => query.OrderByDescending(s => s.Created), SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified), + SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded), _ => query }; } @@ -900,16 +903,18 @@ public class SeriesRepository : ISeriesRepository /// /// Return recently updated series, regardless of read progress, and group the number of volume or chapters added. /// + /// This provides 2 levels of pagination. Fetching the individual chapters only looks at 3000. Then when performing grouping + /// in memory, we stop after 30 series. /// Used to ensure user has access to libraries /// - public async Task> GetRecentlyUpdatedSeries(int userId) + public async Task> GetRecentlyUpdatedSeries(int userId, int pageSize = 30) { - var ret = await GetRecentlyAddedChaptersQuery(userId, 150); - - var seriesMap = new Dictionary(); + var seriesMap = new Dictionary(); var index = 0; - foreach (var item in ret) + foreach (var item in await GetRecentlyAddedChaptersQuery(userId)) { + if (seriesMap.Keys.Count == pageSize) break; + if (seriesMap.ContainsKey(item.SeriesName)) { seriesMap[item.SeriesName].Count += 1; @@ -932,43 +937,9 @@ public class SeriesRepository : ISeriesRepository } return seriesMap.Values.AsEnumerable(); - - //return seriesMap.Values.ToList(); - - // var libraries = await _context.AppUser - // .Where(u => u.Id == userId) - // .SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type})) - // .ToListAsync(); - // var libraryIds = libraries.Select(l => l.LibraryId).ToList(); - // - // var cuttoffDate = DateTime.Now - TimeSpan.FromDays(12); - // - // var ret2 = _context.Series - // .Where(s => s.LastChapterAdded >= cuttoffDate - // && libraryIds.Contains(s.LibraryId)) - // .Select((s) => new GroupedSeriesDto - // { - // LibraryId = s.LibraryId, - // LibraryType = s.Library.Type, - // SeriesId = s.Id, - // SeriesName = s.Name, - // //Created = s.LastChapterAdded, // Hmm on first migration this wont work - // Created = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created), // Hmm on first migration this wont work - // Count = s.Volumes.SelectMany(v => v.Chapters).Count(c => c.Created >= cuttoffDate), - // //Id = index, - // Format = s.Format - // }) - // .Take(50) - // .OrderByDescending(c => c.Created) - // .AsSplitQuery() - // .AsEnumerable(); - // - // return ret2; - - } - private async Task> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50) + private async Task> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000) { var libraries = await _context.AppUser .Where(u => u.Id == userId) @@ -1000,7 +971,7 @@ public class SeriesRepository : ISeriesRepository VolumeNumber = c.Volume.Number, ChapterTitle = c.Title }) - .Take(maxRecords) + //.Take(maxRecords) .AsSplitQuery() .Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId)) .AsEnumerable(); diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index b80d1e8f4..6578b6f63 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -26,8 +26,9 @@ public class ImageService : IImageService private readonly ILogger _logger; private readonly IDirectoryService _directoryService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; - public const string SeriesCoverImageRegex = @"series_\d+"; - public const string CollectionTagCoverImageRegex = @"tag_\d+"; + public const string SeriesCoverImageRegex = @"series\d+"; + public const string CollectionTagCoverImageRegex = @"tag\d+"; + public const string ReadingListCoverImageRegex = @"readinglist\d+"; /// diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 590582eb5..1eb154214 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -72,7 +72,7 @@ public class MetadataService : IMetadataService _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(chapter.Id, "chapter"), false); + MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false); return true; } @@ -101,7 +101,7 @@ public class MetadataService : IMetadataService if (firstChapter == null) return false; volume.CoverImage = firstChapter.CoverImage; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, "volume"), false); + await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false); return true; } @@ -138,7 +138,7 @@ public class MetadataService : IMetadataService } } series.CoverImage = firstCover?.CoverImage ?? coverImage; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, "series"), false); + await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); } @@ -300,7 +300,7 @@ public class MetadataService : IMetadataService if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) { - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, "series"), false); + await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); } _logger.LogInformation("[MetadataService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index ab586486d..b4402558a 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -9,6 +9,7 @@ using API.Data.Repositories; using API.DTOs; using API.Entities; using API.Extensions; +using API.SignalR; using Kavita.Common; using Microsoft.Extensions.Logging; @@ -33,13 +34,15 @@ public class ReaderService : IReaderService { private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; + private readonly IEventHub _eventHub; private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer(); private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst(); - public ReaderService(IUnitOfWork unitOfWork, ILogger logger) + public ReaderService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub) { _unitOfWork = unitOfWork; _logger = logger; + _eventHub = eventHub; } public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId) @@ -211,9 +214,11 @@ public class ReaderService : IReaderService _unitOfWork.AppUserProgressRepository.Update(userProgress); } - if (!_unitOfWork.HasChanges()) return true; - if (await _unitOfWork.CommitAsync()) + if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + MessageFactory.UserProgressUpdateEvent(userId, user.UserName, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum)); return true; } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index b1d9e1132..4580c8d95 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -151,26 +151,22 @@ public class SeriesService : ISeriesService UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople, HandleAddPerson, () => series.Metadata.CoverArtistLocked = true); - if (!updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked) series.Metadata.AgeRatingLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked) series.Metadata.PublicationStatusLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.LanguageLocked) series.Metadata.LanguageLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.GenresLocked) series.Metadata.GenresLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.TagsLocked) series.Metadata.TagsLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) series.Metadata.CharacterLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) series.Metadata.ColoristLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.EditorLocked) series.Metadata.EditorLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.InkerLocked) series.Metadata.InkerLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.LettererLocked) series.Metadata.LettererLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) series.Metadata.PencillerLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) series.Metadata.PublisherLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) series.Metadata.TranslatorLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) series.Metadata.CoverArtistLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.WriterLocked) series.Metadata.WriterLocked = false; - if (!updateSeriesMetadataDto.SeriesMetadata.SummaryLocked) series.Metadata.SummaryLocked = false; - + series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; + series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked; + series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked; + series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked; + series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked; + series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharacterLocked; + series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristLocked; + series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorLocked; + series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkerLocked; + series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LettererLocked; + series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillerLocked; series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked; - - + series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked; + series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked; + series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WriterLocked; + series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked; if (!_unitOfWork.HasChanges()) { @@ -491,10 +487,10 @@ public class SeriesService : ISeriesService foreach (var chapter in chapters) { chapter.Title = FormatChapterTitle(chapter, libraryType); - if (chapter.IsSpecial) - { - specials.Add(chapter); - } + if (!chapter.IsSpecial) continue; + + if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName; + specials.Add(chapter); } diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index ee0af81cb..bbf81c9eb 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -64,6 +64,7 @@ namespace API.Services.Tasks await DeleteChapterCoverImages(); await SendProgress(0.7F, "Cleaning deleted cover images"); await DeleteTagCoverImages(); + await DeleteReadingListCoverImages(); await SendProgress(0.8F, "Cleaning deleted cover images"); await SendProgress(1F, "Cleanup finished"); _logger.LogInformation("Cleanup finished"); @@ -116,6 +117,16 @@ namespace API.Services.Tasks _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); } + /// + /// Removes all reading list images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteReadingListCoverImages() + { + var images = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); + var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex); + _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); + } + /// /// Removes all files and directories in the cache directory /// diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 53833e3f3..50bfa5039 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -7,6 +7,14 @@ using API.Entities; namespace API.SignalR { + public static class MessageFactoryEntityTypes + { + public const string Series = "series"; + public const string Volume = "volume"; + public const string Chapter = "chapter"; + public const string CollectionTag = "collection"; + public const string ReadingList = "readingList"; + } public static class MessageFactory { /// @@ -78,6 +86,11 @@ namespace API.SignalR /// When a library is created/deleted in the Server /// public const string LibraryModified = "LibraryModified"; + /// + /// A user's progress was modified + /// + public const string UserProgressUpdate = "UserProgressUpdate"; + public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName) @@ -320,6 +333,25 @@ namespace API.SignalR }; } + public static SignalRMessage UserProgressUpdateEvent(int userId, string username, int seriesId, int volumeId, int chapterId, int pagesRead) + { + return new SignalRMessage() + { + Name = UserProgressUpdate, + Title = "Updating User Progress", + Progress = ProgressType.None, + Body = new + { + UserId = userId, + Username = username, + SeriesId = seriesId, + VolumeId = volumeId, + ChapterId = chapterId, + PagesRead = pagesRead, + } + }; + } + public static SignalRMessage SiteThemeProgressEvent(string subtitle, string themeName, string eventType) { return new SignalRMessage() diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index 8c3cdca61..d01eda14a 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -100,12 +100,12 @@ export class ErrorInterceptor implements HttpInterceptor { const err = error.error; if (err.hasOwnProperty('message') && err.message.trim() !== '') { if (err.message != 'User is not authenticated') { - console.log('500 error: ', error); + console.error('500 error: ', error); } this.toastr.error(err.message); } else if (error.hasOwnProperty('message') && error.message.trim() !== '') { if (error.message != 'User is not authenticated') { - console.log('500 error: ', error); + console.error('500 error: ', error); } this.toastr.error(error.message); } diff --git a/UI/Web/src/app/_models/events/user-progress-update-event.ts b/UI/Web/src/app/_models/events/user-progress-update-event.ts new file mode 100644 index 000000000..e36199c60 --- /dev/null +++ b/UI/Web/src/app/_models/events/user-progress-update-event.ts @@ -0,0 +1,10 @@ +export interface UserProgressUpdateEvent { + userId: number; + username: string; + //entityId: number; + //entityType: 'series' | 'collection' | 'chapter' | 'volume' | 'readingList'; + seriesId: number; + volumeId: number; + chapterId: number; + pagesRead: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/series-filter.ts b/UI/Web/src/app/_models/series-filter.ts index aeafb3331..c2b823ce3 100644 --- a/UI/Web/src/app/_models/series-filter.ts +++ b/UI/Web/src/app/_models/series-filter.ts @@ -40,7 +40,8 @@ export interface SortOptions { export enum SortField { SortName = 1, Created = 2, - LastModified = 3 + LastModified = 3, + LastChapterAdded = 4 } export interface ReadStatus { diff --git a/UI/Web/src/app/_models/series.ts b/UI/Web/src/app/_models/series.ts index 0e3123ac3..17b489b6e 100644 --- a/UI/Web/src/app/_models/series.ts +++ b/UI/Web/src/app/_models/series.ts @@ -44,4 +44,8 @@ export interface Series { * DateTime that represents last time the logged in user read this series */ latestReadDate: string; + /** + * DateTime representing last time a chapter was added to the Series + */ + lastChapterAdded: string; } diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 3057704cb..9d9305ced 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -235,9 +235,7 @@ export class AccountService implements OnDestroy { // set a timeout to refresh the token a minute before it expires const expires = new Date(jwtToken.exp * 1000); const timeout = expires.getTime() - Date.now() - (60 * 1000); - this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => { - console.log('Token Refreshed'); - }), timeout); + this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => {}), timeout); } private stopRefreshTokenTimer() { diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index f53b9681f..ac7619611 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -53,7 +53,11 @@ export enum EVENTS { /** * A library is created or removed from the instance */ - LibraryModified = 'LibraryModified' + LibraryModified = 'LibraryModified', + /** + * A user updates an entities read progress + */ + UserProgressUpdate = 'UserProgressUpdate', } export interface Message { @@ -164,6 +168,13 @@ export class MessageHubService { }); }); + this.hubConnection.on(EVENTS.UserProgressUpdate, resp => { + this.messagesSource.next({ + event: EVENTS.UserProgressUpdate, + payload: resp.body + }); + }); + this.hubConnection.on(EVENTS.Error, resp => { this.messagesSource.next({ event: EVENTS.Error, diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index ceec62cae..29b61caa5 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -1,11 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmService } from 'src/app/shared/confirm.service'; import { Library } from 'src/app/_models/library'; import { Member } from 'src/app/_models/member'; import { AccountService } from 'src/app/_services/account.service'; -import { ServerService } from 'src/app/_services/server.service'; // TODO: Rename this to EditUserModal @Component({ @@ -27,8 +25,7 @@ export class EditUserComponent implements OnInit { public get username() { return this.userForm.get('username'); } public get password() { return this.userForm.get('password'); } - constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService, - private confirmService: ConfirmService) { } + constructor(public modal: NgbActiveModal, private accountService: AccountService) { } ngOnInit(): void { this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email])); diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 19a314396..f9791f561 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -42,8 +42,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy { distinctUntilChanged((prev: Message, curr: Message) => this.hasMessageChanged(prev, curr))) .subscribe((event: Message) => { - console.log('scan event: ', event); - let libId = 0; if (event.event === EVENTS.ScanSeries) { libId = (event.payload as ScanSeriesEvent).libraryId; diff --git a/UI/Web/src/app/all-series/all-series.component.html b/UI/Web/src/app/all-series/all-series.component.html index b47e3258b..edb13798b 100644 --- a/UI/Web/src/app/all-series/all-series.component.html +++ b/UI/Web/src/app/all-series/all-series.component.html @@ -1,4 +1,4 @@ - +

All Series @@ -16,6 +16,8 @@ (pageChange)="onPageChange($event)" > - + diff --git a/UI/Web/src/app/all-series/all-series.component.ts b/UI/Web/src/app/all-series/all-series.component.ts index cfaeba6e2..fe81e17da 100644 --- a/UI/Web/src/app/all-series/all-series.component.ts +++ b/UI/Web/src/app/all-series/all-series.component.ts @@ -5,6 +5,7 @@ import { Subject } from 'rxjs'; import { take, debounceTime, takeUntil } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; import { FilterSettings } from '../metadata-filter/filter-settings'; +import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service'; import { KEY_CODES, UtilityService } from '../shared/_services/utility.service'; import { Library } from '../_models/library'; import { Pagination } from '../_models/pagination'; @@ -30,6 +31,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy { onDestroy: Subject = new Subject(); filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); + filterActive: boolean = false; bulkActionCallback = (action: Action, data: any) => { const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); @@ -71,14 +73,14 @@ export class AllSeriesComponent implements OnInit, OnDestroy { constructor(private router: Router, private seriesService: SeriesService, private titleService: Title, private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, - private utilityService: UtilityService, private route: ActivatedRoute) { + private utilityService: UtilityService, private route: ActivatedRoute, + private filterUtilityService: FilterUtilitiesService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.titleService.setTitle('Kavita - All Series'); - this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1}; - [this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter()); + this.pagination = this.filterUtilityService.pagination(); + [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(); } ngOnInit(): void { @@ -109,12 +111,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy { updateFilter(data: FilterEvent) { this.filter = data.filter; - if (this.pagination !== undefined && this.pagination !== null && !data.isFirst) { - this.pagination.currentPage = 1; - this.onPageChange(this.pagination); - } else { - this.loadPage(); - } + + if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter); + this.loadPage(); } loadPage() { @@ -123,6 +122,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy { this.filter = this.seriesService.createSeriesFilter(); } + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; this.pagination = series.pagination; @@ -132,15 +132,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy { } onPageChange(pagination: Pagination) { - window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage); + this.filterUtilityService.updateUrlFromPagination(this.pagination); this.loadPage(); } trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`; - - getPage() { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get('page'); - } - } diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts index d65215ef6..82a0a3f9d 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts @@ -95,7 +95,6 @@ export class CardDetailsModalComponent implements OnInit { ngOnInit(): void { this.isChapter = this.utilityService.isChapter(this.data); - console.log('isChapter: ', this.isChapter); this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0]; 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 a98130689..86a89e1ec 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 @@ -336,6 +336,11 @@
Library: {{libraryName | sentenceCase}}
Format: {{utilityService.mangaFormat(series.format)}}
+
+
+
Created: {{series.created | date:'shortDate'}}
+
Last Read: {{series.latestReadDate | date:'shortDate'}}
+
Last Added To: {{series.lastChapterAdded | date:'shortDate'}}

Volumes

diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index dc06a525f..d057cd02d 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -219,10 +219,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false }; }); this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => { - // console.log('compareFN:') - // console.log('options: ', options); - // console.log('filter: ', filter); - // console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter))); return options.filter(m => this.utilityService.filter(m.title, filter)); } this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => { diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 9165d215d..933f0fa04 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -61,6 +61,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { if (this.filterSettings === undefined) { + console.log('filter settings was empty, creating our own'); this.filterSettings = new FilterSettings(); } } 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 c197783bb..1ea863f41 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 @@ -47,6 +47,6 @@
{{subtitle}} - {{libraryName | sentenceCase}} + {{libraryName | sentenceCase}} \ No newline at end of file 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 fc98739c7..feb4fe9af 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 @@ -1,20 +1,24 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ToastrService } from 'ngx-toastr'; import { Observable, Subject } from 'rxjs'; -import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators'; +import { filter, finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators'; import { Download } from 'src/app/shared/_models/download'; import { DownloadService } from 'src/app/shared/_services/download.service'; import { UtilityService } from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; import { CollectionTag } from 'src/app/_models/collection-tag'; +import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event'; import { MangaFormat } from 'src/app/_models/manga-format'; import { PageBookmark } from 'src/app/_models/page-bookmark'; import { RecentlyAddedItem } from 'src/app/_models/recently-added-item'; import { Series } from 'src/app/_models/series'; +import { User } from 'src/app/_models/user'; import { Volume } from 'src/app/_models/volume'; +import { AccountService } from 'src/app/_services/account.service'; import { Action, ActionItem } from 'src/app/_services/action-factory.service'; import { ImageService } from 'src/app/_services/image.service'; import { LibraryService } from 'src/app/_services/library.service'; +import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { BulkSelectionService } from '../bulk-selection.service'; @Component({ @@ -51,7 +55,7 @@ export class CardItemComponent implements OnInit, OnDestroy { /** * Supress library link */ - @Input() supressLibraryLink = false; + @Input() suppressLibraryLink = false; /** * This is the entity we are representing. It will be returned if an action is executed. */ @@ -97,6 +101,8 @@ export class CardItemComponent implements OnInit, OnDestroy { isShiftDown: boolean = false; + private user: User | undefined; + get tooltipTitle() { if (this.chapterTitle === '' || this.chapterTitle === null) return this.title; return this.chapterTitle; @@ -111,14 +117,15 @@ export class CardItemComponent implements OnInit, OnDestroy { constructor(public imageService: ImageService, private libraryService: LibraryService, public utilityService: UtilityService, private downloadService: DownloadService, - private toastr: ToastrService, public bulkSelectionService: BulkSelectionService) {} + private toastr: ToastrService, public bulkSelectionService: BulkSelectionService, + private messageHub: MessageHubService, private accountService: AccountService) {} ngOnInit(): void { if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) { this.supressArchiveWarning = true; } - if (this.supressLibraryLink === false) { + if (this.suppressLibraryLink === false) { if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) { this.libraryId = (this.entity as Series).libraryId; } @@ -139,6 +146,20 @@ export class CardItemComponent implements OnInit, OnDestroy { this.chapterTitle = vol.chapters[0].titleName; } } + + this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { + this.user = user; + }); + + this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate), + map(evt => evt.payload as UserProgressUpdateEvent), takeUntil(this.onDestroy)).subscribe(updateEvent => { + if (this.user !== undefined && this.user.username !== updateEvent.username) return; + if (this.utilityService.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return; + if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return; + if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return; + + this.read = updateEvent.pagesRead; + }); } ngOnDestroy() { 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 ca87d201d..caca55bd2 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 @@ -8,14 +8,14 @@
- -
-
- Enter a Url - • - Drag and drop - • - Upload an image + +
+
+ Enter a Url + • + Drag and drop + • + Upload an image
diff --git a/UI/Web/src/app/cards/series-card/series-card.component.html b/UI/Web/src/app/cards/series-card/series-card.component.html index 846e0af3f..dc741e946 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.html +++ b/UI/Web/src/app/cards/series-card/series-card.component.html @@ -1,5 +1,5 @@ - diff --git a/UI/Web/src/app/carousel/carousel-reel/carousel-reel.component.html b/UI/Web/src/app/carousel/carousel-reel/carousel-reel.component.html index 0857e8eff..e7c869d4b 100644 --- a/UI/Web/src/app/carousel/carousel-reel/carousel-reel.component.html +++ b/UI/Web/src/app/carousel/carousel-reel/carousel-reel.component.html @@ -1,9 +1,11 @@ -

-
{{items.length}} Items
+
{{items.length}} Items
diff --git a/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.html b/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.html index 126859b3f..822960698 100644 --- a/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.html +++ b/UI/Web/src/app/reading-list/reading-lists/reading-lists.component.html @@ -15,6 +15,6 @@ (pageChange)="onPageChange($event)" > - + diff --git a/UI/Web/src/app/recently-added/recently-added.component.html b/UI/Web/src/app/recently-added/recently-added.component.html index 06b744c22..ab88a2cc0 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.html +++ b/UI/Web/src/app/recently-added/recently-added.component.html @@ -1,4 +1,4 @@ - +

Recently Added

diff --git a/UI/Web/src/app/recently-added/recently-added.component.ts b/UI/Web/src/app/recently-added/recently-added.component.ts index 8138a3868..0cc6561d3 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.ts +++ b/UI/Web/src/app/recently-added/recently-added.component.ts @@ -5,7 +5,7 @@ import { Subject } from 'rxjs'; import { debounceTime, take, takeUntil } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; import { FilterSettings } from '../metadata-filter/filter-settings'; -import { KEY_CODES } from '../shared/_services/utility.service'; +import { KEY_CODES, UtilityService } from '../shared/_services/utility.service'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { Pagination } from '../_models/pagination'; import { Series } from '../_models/series'; @@ -15,6 +15,8 @@ import { ActionService } from '../_services/action.service'; import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service'; import { SeriesService } from '../_services/series.service'; +// TODO: Do I still need this or can All Series handle it with a custom sort + /** * This component is used as a standard layout for any card detail. ie) series, on-deck, collections, etc. */ @@ -23,7 +25,6 @@ import { SeriesService } from '../_services/series.service'; templateUrl: './recently-added.component.html', styleUrls: ['./recently-added.component.scss'] }) - export class RecentlyAddedComponent implements OnInit, OnDestroy { isLoading: boolean = true; @@ -34,15 +35,17 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { filter: SeriesFilter | undefined = undefined; filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); + filterActive: boolean = false; onDestroy: Subject = new Subject(); constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title, - private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService) { + private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, + private utilityService: UtilityService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - Recently Added'); if (this.pagination === undefined || this.pagination === null) { - this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1}; + this.pagination = {currentPage: 1, itemsPerPage: 30, totalItems: 0, totalPages: 1}; } this.filterSettings.sortDisabled = true; @@ -102,6 +105,7 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { if (page != null) { this.pagination.currentPage = parseInt(page, 10); } + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); this.isLoading = true; this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; diff --git a/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts b/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts index 2fdcbc910..6fa36164e 100644 --- a/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts +++ b/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts @@ -42,7 +42,7 @@ export class ConfirmResetPasswordComponent implements OnInit { this.toastr.success("Password reset"); this.router.navigateByUrl('login'); }, err => { - console.log(err); + console.error(err, 'There was an error trying to confirm reset password'); }); } diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index 8fee5c5c8..c77eaaa9c 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -13,6 +13,7 @@
+
diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts new file mode 100644 index 000000000..80656ad4c --- /dev/null +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -0,0 +1,276 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { LibraryType } from 'src/app/_models/library'; +import { Pagination } from 'src/app/_models/pagination'; +import { SeriesFilter, SortField } from 'src/app/_models/series-filter'; +import { SeriesService } from 'src/app/_services/series.service'; + +@Injectable({ + providedIn: 'root' +}) +export class FilterUtilitiesService { + + constructor(private route: ActivatedRoute, private seriesService: SeriesService) { } + + /** + * Updates the window location with a custom url based on filter and pagination objects + * @param pagination + * @param filter + */ + updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter) { + let params = '?page=' + pagination.currentPage; + + const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter); + window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination)); + } + + /** + * Patches the page query param in the window location. + * @param pagination + */ + updateUrlFromPagination(pagination: Pagination) { + window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination)); + } + + private replacePaginationOnUrl(url: string, pagination: Pagination) { + return url.replace(/page=\d+/i, 'page=' + pagination.currentPage); + } + + /** + * Will fetch current page from route if present + * @returns A default pagination object + */ + pagination(): Pagination { + return {currentPage: parseInt(this.route.snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1}; + } + + + /** + * Returns the current url with query params for the filter + * @param currentUrl Full url, with ?page=1 as a minimum + * @param filter Filter to build url off + * @returns current url with query params added + */ + urlFromFilter(currentUrl: string, filter: SeriesFilter | undefined) { + if (filter === undefined) return currentUrl; + let params = ''; + + + + params += this.joinFilter(filter.formats, 'format'); + params += this.joinFilter(filter.genres, 'genres'); + params += this.joinFilter(filter.ageRating, 'ageRating'); + params += this.joinFilter(filter.publicationStatus, 'publicationStatus'); + params += this.joinFilter(filter.tags, 'tags'); + params += this.joinFilter(filter.languages, 'languages'); + params += this.joinFilter(filter.collectionTags, 'collectionTags'); + params += this.joinFilter(filter.libraries, 'libraries'); + + params += this.joinFilter(filter.writers, 'writers'); + params += this.joinFilter(filter.artists, 'artists'); + params += this.joinFilter(filter.character, 'character'); + params += this.joinFilter(filter.colorist, 'colorist'); + params += this.joinFilter(filter.coverArtist, 'coverArtists'); + params += this.joinFilter(filter.editor, 'editor'); + params += this.joinFilter(filter.inker, 'inker'); + params += this.joinFilter(filter.letterer, 'letterer'); + params += this.joinFilter(filter.penciller, 'penciller'); + params += this.joinFilter(filter.publisher, 'publisher'); + params += this.joinFilter(filter.translators, 'translators'); + + // readStatus (we need to do an additonal check as there is a default case) + if (filter.readStatus && filter.readStatus.inProgress !== true && filter.readStatus.notRead !== true && filter.readStatus.read !== true) { + params += '&readStatus=' + `${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`; + } + + // sortBy (additional check to not save to url if default case) + if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) { + params += '&sortBy=' + filter.sortOptions.sortField + ',' + filter.sortOptions.isAscending; + } + + if (filter.rating > 0) { + params += '&rating=' + filter.rating; + } + + if (filter.seriesNameQuery !== '') { + params += '&name=' + encodeURIComponent(filter.seriesNameQuery); + } + + return currentUrl + params; + } + + private joinFilter(filterProp: Array, key: string) { + let params = ''; + if (filterProp.length > 0) { + params += `&${key}=` + filterProp.join(','); + } + return params; + } + + /** + * Returns a new instance of a filterSettings that is populated with filter presets from URL + * @returns The Preset filter and if something was set within + */ + filterPresetsFromUrl(): [SeriesFilter, boolean] { + const snapshot = this.route.snapshot; + const filter = this.seriesService.createSeriesFilter(); + let anyChanged = false; + + const format = snapshot.queryParamMap.get('format'); + if (format !== undefined && format !== null) { + filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const genres = snapshot.queryParamMap.get('genres'); + if (genres !== undefined && genres !== null) { + filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const ageRating = snapshot.queryParamMap.get('ageRating'); + if (ageRating !== undefined && ageRating !== null) { + filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const publicationStatus = snapshot.queryParamMap.get('publicationStatus'); + if (publicationStatus !== undefined && publicationStatus !== null) { + filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const tags = snapshot.queryParamMap.get('tags'); + if (tags !== undefined && tags !== null) { + filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const languages = snapshot.queryParamMap.get('languages'); + if (languages !== undefined && languages !== null) { + filter.languages = [...filter.languages, ...languages.split(',')]; + anyChanged = true; + } + + const writers = snapshot.queryParamMap.get('writers'); + if (writers !== undefined && writers !== null) { + filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const artists = snapshot.queryParamMap.get('artists'); + if (artists !== undefined && artists !== null) { + filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const character = snapshot.queryParamMap.get('character'); + if (character !== undefined && character !== null) { + filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const colorist = snapshot.queryParamMap.get('colorist'); + if (colorist !== undefined && colorist !== null) { + filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const coverArtists = snapshot.queryParamMap.get('coverArtists'); + if (coverArtists !== undefined && coverArtists !== null) { + filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const editor = snapshot.queryParamMap.get('editor'); + if (editor !== undefined && editor !== null) { + filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const inker = snapshot.queryParamMap.get('inker'); + if (inker !== undefined && inker !== null) { + filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const letterer = snapshot.queryParamMap.get('letterer'); + if (letterer !== undefined && letterer !== null) { + filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const penciller = snapshot.queryParamMap.get('penciller'); + if (penciller !== undefined && penciller !== null) { + filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const publisher = snapshot.queryParamMap.get('publisher'); + if (publisher !== undefined && publisher !== null) { + filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const translators = snapshot.queryParamMap.get('translators'); + if (translators !== undefined && translators !== null) { + filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const libraries = snapshot.queryParamMap.get('libraries'); + if (libraries !== undefined && libraries !== null) { + filter.libraries = [...filter.libraries, ...libraries.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + const collectionTags = snapshot.queryParamMap.get('collectionTags'); + if (collectionTags !== undefined && collectionTags !== null) { + filter.collectionTags = [...filter.collectionTags, ...collectionTags.split(',').map(item => parseInt(item, 10))]; + anyChanged = true; + } + + // Rating, seriesName, + const rating = snapshot.queryParamMap.get('rating'); + if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) { + filter.rating = parseInt(rating, 10); + anyChanged = true; + } + + /// Read status is encoded as true,true,true + const readStatus = snapshot.queryParamMap.get('readStatus'); + if (readStatus !== undefined && readStatus !== null) { + const values = readStatus.split(',').map(i => i === 'true'); + if (values.length === 3) { + filter.readStatus.inProgress = values[0]; + filter.readStatus.notRead = values[1]; + filter.readStatus.read = values[2]; + anyChanged = true; + } + } + + const sortBy = snapshot.queryParamMap.get('sortBy'); + if (sortBy !== undefined && sortBy !== null) { + const values = sortBy.split(','); + if (values.length === 1) { + values.push('true'); + } + if (values.length === 2) { + filter.sortOptions = { + isAscending: values[1] === 'true', + sortField: Number(values[0]) + } + anyChanged = true; + } + } + + const searchNameQuery = snapshot.queryParamMap.get('name'); + if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') { + filter.seriesNameQuery = decodeURIComponent(searchNameQuery); + anyChanged = true; + } + + + return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better + } +} diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 5bb8646f7..99110da65 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -4,7 +4,7 @@ import { Chapter } from 'src/app/_models/chapter'; import { LibraryType } from 'src/app/_models/library'; import { MangaFormat } from 'src/app/_models/manga-format'; import { Series } from 'src/app/_models/series'; -import { SeriesFilter } from 'src/app/_models/series-filter'; +import { SeriesFilter, SortField } from 'src/app/_models/series-filter'; import { Volume } from 'src/app/_models/volume'; export enum KEY_CODES { @@ -67,7 +67,7 @@ export class UtilityService { * @param includeSpace Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end. * @returns */ - formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) { + formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) { switch(libraryType) { case LibraryType.Book: return 'Book' + (includeSpace ? ' ' : ''); @@ -96,133 +96,6 @@ export class UtilityService { return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, '')); } - /** - * Returns a new instance of a filterSettings that is populated with filter presets from URL - * @param snapshot - * @param blankFilter Filter to start with - * @returns The Preset filter and if something was set within - */ - filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot, blankFilter: SeriesFilter): [SeriesFilter, boolean] { - const filter = Object.assign({}, blankFilter); - let anyChanged = false; - - const format = snapshot.queryParamMap.get('format'); - if (format !== undefined && format !== null) { - filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const genres = snapshot.queryParamMap.get('genres'); - if (genres !== undefined && genres !== null) { - filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const ageRating = snapshot.queryParamMap.get('ageRating'); - if (ageRating !== undefined && ageRating !== null) { - filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const publicationStatus = snapshot.queryParamMap.get('publicationStatus'); - if (publicationStatus !== undefined && publicationStatus !== null) { - filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const tags = snapshot.queryParamMap.get('tags'); - if (tags !== undefined && tags !== null) { - filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const languages = snapshot.queryParamMap.get('languages'); - if (languages !== undefined && languages !== null) { - filter.languages = [...filter.languages, ...languages.split(',')]; - anyChanged = true; - } - - const writers = snapshot.queryParamMap.get('writers'); - if (writers !== undefined && writers !== null) { - filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const artists = snapshot.queryParamMap.get('artists'); - if (artists !== undefined && artists !== null) { - filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const character = snapshot.queryParamMap.get('character'); - if (character !== undefined && character !== null) { - filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const colorist = snapshot.queryParamMap.get('colorist'); - if (colorist !== undefined && colorist !== null) { - filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const coverArtists = snapshot.queryParamMap.get('coverArtists'); - if (coverArtists !== undefined && coverArtists !== null) { - filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const editor = snapshot.queryParamMap.get('editor'); - if (editor !== undefined && editor !== null) { - filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const inker = snapshot.queryParamMap.get('inker'); - if (inker !== undefined && inker !== null) { - filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const letterer = snapshot.queryParamMap.get('letterer'); - if (letterer !== undefined && letterer !== null) { - filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const penciller = snapshot.queryParamMap.get('penciller'); - if (penciller !== undefined && penciller !== null) { - filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const publisher = snapshot.queryParamMap.get('publisher'); - if (publisher !== undefined && publisher !== null) { - filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - const translators = snapshot.queryParamMap.get('translators'); - if (translators !== undefined && translators !== null) { - filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))]; - anyChanged = true; - } - - /// Read status is encoded as true,true,true - const readStatus = snapshot.queryParamMap.get('readStatus'); - if (readStatus !== undefined && readStatus !== null) { - const values = readStatus.split(',').map(i => i === "true"); - if (values.length === 3) { - filter.readStatus.inProgress = values[0]; - filter.readStatus.notRead = values[1]; - filter.readStatus.read = values[2]; - anyChanged = true; - } - } - - - return [filter, anyChanged]; - } mangaFormat(format: MangaFormat): string { switch (format) { @@ -305,4 +178,27 @@ export class UtilityService { rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } + + deepEqual(object1: any, object2: any) { + const keys1 = Object.keys(object1); + const keys2 = Object.keys(object2); + if (keys1.length !== keys2.length) { + return false; + } + for (const key of keys1) { + const val1 = object1[key]; + const val2 = object2[key]; + const areObjects = this.isObject(val1) && this.isObject(val2); + if ( + areObjects && !this.deepEqual(val1, val2) || + !areObjects && val1 !== val2 + ) { + return false; + } + } + return true; + } + private isObject(object: any) { + return object != null && typeof object === 'object'; + } } diff --git a/UI/Web/src/app/shared/image/image.component.html b/UI/Web/src/app/shared/image/image.component.html index 10248a9fc..5e89b6466 100644 --- a/UI/Web/src/app/shared/image/image.component.html +++ b/UI/Web/src/app/shared/image/image.component.html @@ -1,7 +1,3 @@ - - + aria-hidden="true"> \ No newline at end of file diff --git a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html index b0c968c0a..d41997a73 100644 --- a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html +++ b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.html @@ -4,13 +4,13 @@
-
+
- diff --git a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts index bdd7a411e..d49830a52 100644 --- a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts +++ b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts @@ -1,4 +1,4 @@ -import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; /** * This should go on all pages which have the side nav present and is not Settings related. @@ -20,6 +20,11 @@ export class SideNavCompanionBarComponent implements OnInit { */ @Input() filterOpenByDefault: boolean = false; + /** + * This implies there is a filter in effect on the underlying page. Will show UI styles to imply this to the user. + */ + @Input() filterActive: boolean = false; + /** * Should be passed through from Filter component. */ diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts index f4a2e15a1..1d1e43e0b 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts @@ -50,7 +50,6 @@ export class SideNavComponent implements OnInit, OnDestroy { }); this.messageHub.messages$.pipe(takeUntil(this.onDestory), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => { - console.log('Received library modfied event'); this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => { this.libraries = libraries; }); diff --git a/UI/Web/src/app/theme-test/theme-test.component.ts b/UI/Web/src/app/theme-test/theme-test.component.ts index a4c422389..bcb26dafd 100644 --- a/UI/Web/src/app/theme-test/theme-test.component.ts +++ b/UI/Web/src/app/theme-test/theme-test.component.ts @@ -46,7 +46,8 @@ export class ThemeTestComponent implements OnInit { volumes: [], localizedNameLocked: false, nameLocked: false, - sortNameLocked: false + sortNameLocked: false, + lastChapterAdded: '', } seriesWithProgress: Series = { @@ -67,7 +68,8 @@ export class ThemeTestComponent implements OnInit { volumes: [], localizedNameLocked: false, nameLocked: false, - sortNameLocked: false + sortNameLocked: false, + lastChapterAdded: '', } get TagBadgeCursor(): typeof TagBadgeCursor { diff --git a/UI/Web/src/app/typeahead/typeahead.component.html b/UI/Web/src/app/typeahead/typeahead.component.html index 3a6164e99..635e11170 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.html +++ b/UI/Web/src/app/typeahead/typeahead.component.html @@ -16,7 +16,7 @@ Loading...
- +
@@ -28,7 +28,7 @@ Add {{typeaheadControl.value}}...
  • diff --git a/UI/Web/src/app/typeahead/typeahead.component.ts b/UI/Web/src/app/typeahead/typeahead.component.ts index 43c8ab681..3aee75d7b 100644 --- a/UI/Web/src/app/typeahead/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/typeahead.component.ts @@ -179,6 +179,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy { ngOnInit() { this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => { + this.clearSelections(); this.init(); }); @@ -258,9 +259,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy { } else { this.optionSelection = new SelectionModel(true, [this.settings.savedData]); } - - - //this.typeaheadControl.setValue(this.settings.displayFn(this.settings.savedData)) } } else { this.optionSelection = new SelectionModel(); @@ -308,14 +306,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy { return; } - - const filteredResults = opts.filter(item => this.filterSelected(item)); - - if (filteredResults.length < this.focusedIndex) return; - const option = filteredResults[this.focusedIndex]; - - this.toggleSelection(option); - this.resetField(); + (item as HTMLElement).click(); this.focusedIndex = 0; event.preventDefault(); event.stopPropagation(); @@ -356,10 +347,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy { this.resetField(); } - clearSelections(event: any) { - this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false)); - this.selectedData.emit(this.optionSelection.selected()); - this.resetField(); + clearSelections() { + if (this.optionSelection) { + this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false)); + this.selectedData.emit(this.optionSelection.selected()); + this.resetField(); + } } handleOptionClick(opt: any) {