mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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
This commit is contained in:
parent
5e629913b7
commit
553f9b0d98
@ -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<ReadingList>()
|
||||
{
|
||||
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<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
|
||||
await cleanupService.DeleteReadingListCoverImages();
|
||||
|
||||
Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CleanupCacheDirectory
|
||||
|
||||
[Fact]
|
||||
|
@ -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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
@ -240,7 +241,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
@ -310,7 +311,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
@ -1321,7 +1322,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// 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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
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<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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<ReaderController> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<SeriesDto>()
|
||||
// {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<int>() {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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -5,4 +5,5 @@ public enum SortField
|
||||
SortName = 1,
|
||||
CreatedDate = 2,
|
||||
LastModifiedDate = 3,
|
||||
LastChapterAdded = 4
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ namespace API.DTOs
|
||||
/// </summary>
|
||||
public DateTime LatestReadDate { get; set; }
|
||||
/// <summary>
|
||||
/// DateTime representing last time a chapter was added to the Series
|
||||
/// </summary>
|
||||
public DateTime LastChapterAdded { get; set; }
|
||||
/// <summary>
|
||||
/// Rating from logged in user. Calculated at API-time.
|
||||
/// </summary>
|
||||
public int UserRating { get; set; }
|
||||
|
@ -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<ReadingListItem>()
|
||||
};
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -27,6 +27,7 @@ public interface IReadingListRepository
|
||||
void Update(ReadingList list);
|
||||
Task<int> Count();
|
||||
Task<string> GetCoverImageAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
@ -59,6 +60,15 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> 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);
|
||||
|
@ -95,7 +95,7 @@ public interface ISeriesRepository
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
@ -231,7 +231,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// <summary>
|
||||
/// Gets all series
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="libraryId">Restricts to just one library</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filter"></param>
|
||||
@ -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<SeriesDto>(_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
|
||||
/// <summary>
|
||||
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
|
||||
/// </summary>
|
||||
/// <remarks>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. </remarks>
|
||||
/// <param name="userId">Used to ensure user has access to libraries</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
|
||||
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
|
||||
{
|
||||
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
|
||||
|
||||
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
|
||||
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
|
||||
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<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
|
||||
private async Task<IEnumerable<RecentlyAddedSeries>> 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();
|
||||
|
@ -26,8 +26,9 @@ public class ImageService : IImageService
|
||||
private readonly ILogger<ImageService> _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+";
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -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);
|
||||
|
@ -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<ReaderService> _logger;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger)
|
||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all reading list images that are not in the database. They must follow <see cref="ImageService.ReadingListCoverImageRegex"/> filename pattern.
|
||||
/// </summary>
|
||||
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))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all files and directories in the cache directory
|
||||
/// </summary>
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
@ -78,6 +86,11 @@ namespace API.SignalR
|
||||
/// When a library is created/deleted in the Server
|
||||
/// </summary>
|
||||
public const string LibraryModified = "LibraryModified";
|
||||
/// <summary>
|
||||
/// A user's progress was modified
|
||||
/// </summary>
|
||||
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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
10
UI/Web/src/app/_models/events/user-progress-update-event.ts
Normal file
10
UI/Web/src/app/_models/events/user-progress-update-event.ts
Normal file
@ -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;
|
||||
}
|
@ -40,7 +40,8 @@ export interface SortOptions {
|
||||
export enum SortField {
|
||||
SortName = 1,
|
||||
Created = 2,
|
||||
LastModified = 3
|
||||
LastModified = 3,
|
||||
LastChapterAdded = 4
|
||||
}
|
||||
|
||||
export interface ReadStatus {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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<T> {
|
||||
@ -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,
|
||||
|
@ -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]));
|
||||
|
@ -42,8 +42,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
|
||||
this.hasMessageChanged(prev, curr)))
|
||||
.subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => {
|
||||
console.log('scan event: ', event);
|
||||
|
||||
let libId = 0;
|
||||
if (event.event === EVENTS.ScanSeries) {
|
||||
libId = (event.payload as ScanSeriesEvent).libraryId;
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
All Series
|
||||
@ -16,6 +16,8 @@
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
@ -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<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
filterOpen: EventEmitter<boolean> = 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
|
||||
|
@ -336,6 +336,11 @@
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
||||
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">Last Read: {{series.latestReadDate | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">Last Added To: {{series.lastChapterAdded | date:'shortDate'}}</div>
|
||||
</div>
|
||||
<h4>Volumes</h4>
|
||||
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<span class="card-title library" [ngbTooltip]="subtitle" placement="top" *ngIf="subtitle.length > 0">{{subtitle}}</span>
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!supressLibraryLink && libraryName">{{libraryName | sentenceCase}}</a>
|
||||
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!suppressLibraryLink && libraryName">{{libraryName | sentenceCase}}</a>
|
||||
</div>
|
||||
</div>
|
@ -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() {
|
||||
|
@ -8,14 +8,14 @@
|
||||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mx-auto" style="width: 350px;">
|
||||
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="col ps-1 pe-1">•</span>
|
||||
<span class="col" style="padding-right:0px" href="javascript:void(0)">Drag and drop</span>
|
||||
<span class="col ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">Upload<span class="phone-hidden"> an image</span></a>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span style="padding-right:0px" href="javascript:void(0)">Drag and drop</span>
|
||||
<span class="ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">Upload<span class="phone-hidden"> an image</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="data !== undefined">
|
||||
<app-card-item [title]="data.name" [actions]="actions" [supressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl"
|
||||
<app-card-item [title]="data.name" [actions]="actions" [suppressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl"
|
||||
[entity]="data" [total]="data.pages" [read]="data.pagesRead" (clicked)="handleClick()"
|
||||
[allowSelection]="allowSelection" (selection)="selection.emit(selected)" [selected]="selected"
|
||||
></app-card-item>
|
||||
|
@ -1,9 +1,11 @@
|
||||
<div class="carousel-container" *ngIf="items.length > 0">
|
||||
<div class="carousel-container" *ngIf="items.length > 0 ">
|
||||
<div>
|
||||
<h3 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a></h3>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||
<h3 style="display: inline-block;">
|
||||
<a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a>
|
||||
</h3>
|
||||
<div class="float-end" *ngIf="swiper">
|
||||
<button class="btn btn-icon" [disabled]="swiper.isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||
<button class="btn btn-icon" [disabled]="swiper.isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { SwiperComponent } from 'swiper/angular';
|
||||
import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
|
||||
import { Swiper, SwiperEvents } from 'swiper/types';
|
||||
|
||||
@Component({
|
||||
@ -7,7 +6,7 @@ import { Swiper, SwiperEvents } from 'swiper/types';
|
||||
templateUrl: './carousel-reel.component.html',
|
||||
styleUrls: ['./carousel-reel.component.scss']
|
||||
})
|
||||
export class CarouselReelComponent implements OnInit {
|
||||
export class CarouselReelComponent {
|
||||
|
||||
@ContentChild('carouselItem') carouselItemTemplate!: TemplateRef<any>;
|
||||
@Input() items: any[] = [];
|
||||
@ -19,30 +18,20 @@ export class CarouselReelComponent implements OnInit {
|
||||
|
||||
trackByIdentity: (index: number, item: any) => string;
|
||||
|
||||
get isEnd() {
|
||||
return this.swiper?.isEnd;
|
||||
}
|
||||
|
||||
get isBeginning() {
|
||||
return this.swiper?.isBeginning;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.title}_${item.id}_${item?.name}_${item?.pagesRead}_${index}`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
nextPage() {
|
||||
if (this.isEnd) return;
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isEnd) return;
|
||||
this.swiper.setProgress(this.swiper.progress + 0.25, 600);
|
||||
}
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
if (this.isBeginning) return;
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isBeginning) return;
|
||||
this.swiper.setProgress(this.swiper.progress - 0.25, 600);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<ng-container title>
|
||||
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
|
@ -13,4 +13,4 @@
|
||||
.read-btn--text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,10 +4,11 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
|
||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event';
|
||||
@ -32,17 +33,15 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
collectionTag!: CollectionTag;
|
||||
tagImage: string = '';
|
||||
isLoading: boolean = true;
|
||||
collections: CollectionTag[] = [];
|
||||
collectionTagName: string = '';
|
||||
series: Array<Series> = [];
|
||||
seriesPagination!: Pagination;
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
summary: string = '';
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
filterActive: boolean = false;
|
||||
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
@ -87,26 +86,20 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||
private modalService: NgbModal, private titleService: Title, private accountService: AccountService,
|
||||
private modalService: NgbModal, private titleService: Title,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
private utilityService: UtilityService) {
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
if (routeId === null) {
|
||||
this.router.navigate(['collections']);
|
||||
return;
|
||||
}
|
||||
const tagId = parseInt(routeId, 10);
|
||||
this.seriesPagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||
this.seriesPagination = this.filterUtilityService.pagination();
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
|
||||
this.filterSettings.presets.collectionTags = [tagId];
|
||||
|
||||
this.updateTag(tagId);
|
||||
@ -148,13 +141,13 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
updateTag(tagId: number) {
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collections = tags;
|
||||
const matchingTags = this.collections.filter(t => t.id === tagId);
|
||||
const matchingTags = tags.filter(t => t.id === tagId);
|
||||
if (matchingTags.length === 0) {
|
||||
this.toastr.error('You don\'t have access to any libraries this tag belongs to or this tag is invalid');
|
||||
|
||||
this.router.navigateByUrl('/');
|
||||
return;
|
||||
}
|
||||
|
||||
this.collectionTag = matchingTags[0];
|
||||
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>');
|
||||
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
||||
@ -163,46 +156,25 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
this.router.navigate(['collections', this.collectionTag.id], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.seriesPagination.currentPage} });
|
||||
this.filterUtilityService.updateUrlFromPagination(this.seriesPagination);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.seriesPagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
|
||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter.collectionTags.push(this.collectionTag.id);
|
||||
}
|
||||
|
||||
// TODO: Add ability to filter series for a collection
|
||||
// Reload page after a series is updated or first load
|
||||
this.seriesService.getSeriesForTag(this.collectionTag.id, this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage).subscribe(tags => {
|
||||
this.series = tags.result;
|
||||
this.seriesPagination = tags.pagination;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.seriesPagination = series.pagination;
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.seriesPagination.currentPage = 1;
|
||||
this.onPageChange(this.seriesPagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
}
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
|
@ -1,24 +1,38 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
{{libraryName}}
|
||||
</h2>
|
||||
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
|
||||
<h6 subtitle style="margin-left:40px;" *ngIf="active.fragment === ''">{{pagination?.totalItems}} Series</h6>
|
||||
<div main>
|
||||
<!-- TODO: Implement Tabs here for Recommended and Library view -->
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-pills" style="flex-wrap: nowrap;">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink>{{tab.title | sentenceCase}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.title === 'Recommended'">
|
||||
<app-library [libraryId]="libraryId"></app-library>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.title === 'Library'">
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</app-side-nav-companion-bar>
|
||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { LibraryService } from '../_services/library.service';
|
||||
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-detail',
|
||||
@ -35,6 +36,13 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
filterActive: boolean = false;
|
||||
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'Library', fragment: ''},
|
||||
{title: 'Recommended', fragment: 'recomended'},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
@ -77,13 +85,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, public navService: NavService) {
|
||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService) {
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
if (routeId === null) {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.libraryId = parseInt(routeId, 10);
|
||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
||||
@ -91,9 +100,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
||||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
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();
|
||||
this.filterSettings.presets.libraries = [this.libraryId];
|
||||
}
|
||||
|
||||
@ -142,30 +151,21 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.pagination.currentPage = 1;
|
||||
this.onPageChange(this.pagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.loadingSeries = true;
|
||||
|
||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter.libraries.push(this.libraryId);
|
||||
}
|
||||
|
||||
this.loadingSeries = true;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
|
||||
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
@ -175,7 +175,7 @@ export class LibraryDetailComponent 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();
|
||||
}
|
||||
|
||||
@ -184,10 +184,4 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,19 +11,19 @@
|
||||
|
||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" [clickableTitle]="false">
|
||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
@ -1,13 +1,14 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { debounceTime, filter, take, takeUntil } from 'rxjs/operators';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||
import { Library } from '../_models/library';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { Series } from '../_models/series';
|
||||
import { SortField } from '../_models/series-filter';
|
||||
import { SeriesGroup } from '../_models/series-group';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
@ -23,6 +24,11 @@ import { SeriesService } from '../_services/series.service';
|
||||
})
|
||||
export class LibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* By default, 0, but if non-zero, will limit all API calls to library id
|
||||
*/
|
||||
@Input() libraryId: number = 0;
|
||||
|
||||
user: User | undefined;
|
||||
libraries: Library[] = [];
|
||||
isLoading = false;
|
||||
@ -107,21 +113,36 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadOnDeck() {
|
||||
this.seriesService.getOnDeck().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
let api = this.seriesService.getOnDeck();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getOnDeck(this.libraryId);
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
});
|
||||
}
|
||||
|
||||
loadRecentlyAddedSeries() {
|
||||
this.seriesService.getRecentlyAdded().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
let api = this.seriesService.getRecentlyAdded();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getRecentlyAdded(this.libraryId);
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.recentlyAddedSeries = updatedSeries.result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
loadRecentlyAdded() {
|
||||
this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries;
|
||||
let api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
|
||||
if (this.libraryId === 0) return true;
|
||||
return group.libraryId === this.libraryId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,17 +151,22 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
handleSectionClick(sectionTitle: string) {
|
||||
if (sectionTitle.toLowerCase() === 'collections') {
|
||||
this.router.navigate(['collections']);
|
||||
} else if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
this.router.navigate(['recently-added']);
|
||||
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
const params: any = {};
|
||||
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
const params: any = {};
|
||||
params['readStatus'] = 'true,false,false';
|
||||
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
const params: any = {};
|
||||
params['sortBy'] = SortField.Created + ',false'; // sort by created, desc
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
||||
this.router.navigate(['all-series']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,13 +21,20 @@ img {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
text-align: center;
|
||||
display: block;
|
||||
height: 100vh;
|
||||
|
||||
// Original
|
||||
//display: block;
|
||||
|
||||
// New (for centering in both axis)
|
||||
//display: flex; // Leave this off as it can cutoff the image
|
||||
align-items: center;
|
||||
|
||||
#image-1 {
|
||||
&.double {
|
||||
margin: 0 0 0 auto;
|
||||
@ -92,7 +99,7 @@ img {
|
||||
.full-height {
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@ -101,16 +108,16 @@ img {
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
|
||||
&.double {
|
||||
width: 50%;
|
||||
&.double {
|
||||
width: 50%;
|
||||
|
||||
&.cover {
|
||||
width: 100%;
|
||||
}
|
||||
&.cover {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-double {
|
||||
@ -119,11 +126,11 @@ img {
|
||||
}
|
||||
|
||||
.fit-to-width-double-offset {
|
||||
width: 100%;
|
||||
max-width: 100%; // max-width fixes center alignment issue
|
||||
}
|
||||
|
||||
.original-double-offset {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.fit-to-height-double-offset {
|
||||
|
@ -23,7 +23,6 @@ import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/rea
|
||||
import { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
||||
import { ReaderMode } from '../_models/preferences/reader-mode';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { LibraryType } from '../_models/library';
|
||||
import { ShorcutsModalComponent } from '../reader-shared/_modals/shorcuts-modal/shorcuts-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
@ -326,8 +325,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public readerService: ReaderService, private location: Location,
|
||||
private formBuilder: FormBuilder, private navService: NavService,
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
private libraryService: LibraryService, public utilityService: UtilityService,
|
||||
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
public utilityService: UtilityService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
}
|
||||
@ -852,11 +851,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
||||
|
||||
// If the prev page before we change current page is a cover image, we actually are skipping a page
|
||||
console.log('Page ', this.PageNumber, ' is cover image: ', this.isCoverImage(this.cachedImages.prev()))
|
||||
console.log('Page ', this.pageNum, ' is cover image: ', this.isCoverImage())
|
||||
//console.log('Page ', this.PageNumber, ' is cover image: ', this.isCoverImage(this.cachedImages.prev()))
|
||||
//console.log('Page ', this.pageNum, ' is cover image: ', this.isCoverImage())
|
||||
const pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage(this.cachedImages.prev())) ? 2: 1;
|
||||
// BUG: isCoverImage works on canvasImage, where we need to know if the previous image is a cover image or not.
|
||||
console.log('pageAmt: ', pageAmount);
|
||||
//console.log('pageAmt: ', pageAmount);
|
||||
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
||||
if (this.isLoading) { return; }
|
||||
|
||||
@ -992,7 +991,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// Reset scroll on non HEIGHT Fits
|
||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
||||
window.scrollTo(0, 0);
|
||||
this.document.body.scroll(0, 0)
|
||||
}
|
||||
|
||||
|
||||
|
@ -327,6 +327,7 @@
|
||||
<option [value]="SortField.SortName">Sort Name</option>
|
||||
<option [value]="SortField.Created">Created</option>
|
||||
<option [value]="SortField.LastModified">Last Modified</option>
|
||||
<option [value]="SortField.LastChapterAdded">Item Added</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -84,7 +84,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
||||
|
||||
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.readProgressGroup = new FormGroup({
|
||||
read: new FormControl(this.filter.readStatus.read, []),
|
||||
@ -134,7 +134,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
|
||||
map(val => (val || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestory)).subscribe(changes => {
|
||||
takeUntil(this.onDestory))
|
||||
.subscribe(changes => {
|
||||
this.filter.seriesNameQuery = changes;
|
||||
});
|
||||
}
|
||||
@ -151,11 +152,29 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets) {
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets?.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets?.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets?.readStatus.inProgress);
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets.readStatus.inProgress);
|
||||
|
||||
if (this.filterSettings.presets.sortOptions) {
|
||||
this.sortGroup.get('sortField')?.setValue(this.filterSettings.presets.sortOptions.sortField);
|
||||
this.isAscendingSort = this.filterSettings.presets.sortOptions.isAscending;
|
||||
if (this.filter.sortOptions) {
|
||||
this.filter.sortOptions.isAscending = this.isAscendingSort;
|
||||
this.filter.sortOptions.sortField = this.filterSettings.presets.sortOptions.sortField;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets.rating > 0) {
|
||||
this.updateRating(this.filterSettings.presets.rating);
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets.seriesNameQuery !== '') {
|
||||
this.seriesNameGroup.get('searchNameQuery')?.setValue(this.filterSettings.presets.seriesNameQuery);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.setupTypeaheads();
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
</span>
|
||||
{{readingList?.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle>{{items.length}} Items</h6>
|
||||
<h6 subtitle style="margin-left: 40px">{{items.length}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||
|
||||
|
@ -15,6 +15,6 @@
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [supressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h2 title>
|
||||
Recently Added
|
||||
</h2>
|
||||
|
@ -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<boolean> = new EventEmitter();
|
||||
filterActive: boolean = false;
|
||||
|
||||
onDestroy: Subject<void> = 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;
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
<!-- NOTE: We can put continue point here as Vol X Ch Y or just Ch Y or Book Z ?-->
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0">
|
||||
|
276
UI/Web/src/app/shared/_services/filter-utilities.service.ts
Normal file
276
UI/Web/src/app/shared/_services/filter-utilities.service.ts
Normal file
@ -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<any>, 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
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
<img #img class="lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)"
|
||||
aria-hidden="true">
|
||||
|
||||
<!-- <img #img [defaultImage]="imageService.placeholderImage" [lazyload]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)"
|
||||
aria-hidden="true"> -->
|
||||
aria-hidden="true">
|
@ -4,13 +4,13 @@
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<ng-content select="[subtitle]"></ng-content>
|
||||
</div>
|
||||
<div class="col mr-auto hide-if-empty">
|
||||
<div class="col mr-auto hide-if-empty d-none d-sm-flex">
|
||||
<ng-content select="[main]"></ng-content>
|
||||
</div>
|
||||
<div class="col" *ngIf="hasFilter">
|
||||
<div class="row justify-content-end">
|
||||
<div class="col-auto align-self-end">
|
||||
<button *ngIf="hasFilter" class="btn btn-secondary btn-small" (click)="toggleFilter()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleFilter()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Sort / Filter</span>
|
||||
</button>
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -16,7 +16,7 @@
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
|
||||
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
|
||||
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections()"></button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@
|
||||
Add {{typeaheadControl.value}}...
|
||||
</li>
|
||||
<li *ngFor="let option of filteredOptions | async; let index = index;" (click)="handleOptionClick(option)"
|
||||
class="list-group-item" role="option"
|
||||
class="list-group-item" role="option" [attr.data-index]="index"
|
||||
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
|
||||
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
|
||||
</li>
|
||||
|
@ -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<any>(true, [this.settings.savedData]);
|
||||
}
|
||||
|
||||
|
||||
//this.typeaheadControl.setValue(this.settings.displayFn(this.settings.savedData))
|
||||
}
|
||||
} else {
|
||||
this.optionSelection = new SelectionModel<any>();
|
||||
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user