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:
Joseph Milazzo 2022-04-14 16:55:06 -05:00 committed by GitHub
parent 5e629913b7
commit 553f9b0d98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 864 additions and 537 deletions

View File

@ -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]

View File

@ -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);

View File

@ -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);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
}

View File

@ -5,4 +5,5 @@ public enum SortField
SortName = 1,
CreatedDate = 2,
LastModifiedDate = 3,
LastChapterAdded = 4
}

View File

@ -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; }

View File

@ -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()

View File

@ -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);

View File

@ -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();

View File

@ -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>

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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()

View File

@ -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);
}

View 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;
}

View File

@ -40,7 +40,8 @@ export interface SortOptions {
export enum SortField {
SortName = 1,
Created = 2,
LastModified = 3
LastModified = 3,
LastChapterAdded = 4
}
export interface ReadStatus {

View File

@ -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;
}

View File

@ -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() {

View File

@ -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,

View File

@ -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]));

View File

@ -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;

View File

@ -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>

View File

@ -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');
}
}

View File

@ -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];

View File

@ -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">

View File

@ -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) => {

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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() {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -13,4 +13,4 @@
.read-btn--text {
display: none;
}
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -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');
}
}

View File

@ -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>

View File

@ -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']);
}
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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>

View File

@ -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();
}

View File

@ -5,7 +5,7 @@
</span>
{{readingList?.title}}&nbsp;<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">

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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');
});
}

View File

@ -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">

View 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
}
}

View File

@ -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';
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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.
*/

View File

@ -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;
});

View File

@ -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 {

View File

@ -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>

View File

@ -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) {