mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 05:34:21 -04:00
Bulk Operations (#596)
* Implemented the ability to perform multi-selections on cards. Basic selection code is done, CSS needed and exposing actions. * Implemented a bulk selection bar. Added logic to the card on when to force show checkboxes. * Fixed some bad parsing groups and cases for Comic Chapters. * Hooked up some bulk actions on series detail page. Not hooked up to backend yet. * Fixes #593. URI Enocde library names as sometimes they can have & in them. * Implemented the ability to mark volume/chapters as read/unread. * Hooked up mark as unread with specials as well. * Add to reading list hooked up for Series Detail * Implemented ability to add multiple series to a reading list. * Implemented bulk selection for series cards * Added comments to the new code in ReaderService.cs * Implemented proper styling on bulk operation bar and integrated for collections. * Fixed an issue with shift clicking * Cleaned up css of bulk operations bar * Code cleanup
This commit is contained in:
parent
52c4285168
commit
f5229fd0e6
@ -58,17 +58,17 @@ namespace API.Tests.Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "0")]
|
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
|
||||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
||||||
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")]
|
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")]
|
||||||
[InlineData("Batman & Catwoman - Trail of the Gun 01", "0")]
|
[InlineData("Batman & Catwoman - Trail of the Gun 01", "1")]
|
||||||
[InlineData("Batman & Daredevil - King of New York", "0")]
|
[InlineData("Batman & Daredevil - King of New York", "0")]
|
||||||
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")]
|
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")]
|
||||||
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
|
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
|
||||||
[InlineData("Batman & Wildcat (1 of 3)", "1")]
|
[InlineData("Batman & Wildcat (1 of 3)", "1")]
|
||||||
[InlineData("Batman & Wildcat (2 of 3)", "2")]
|
[InlineData("Batman & Wildcat (2 of 3)", "2")]
|
||||||
[InlineData("Batman And Superman World's Finest #01", "0")]
|
[InlineData("Batman And Superman World's Finest #01", "1")]
|
||||||
[InlineData("Babe 01", "0")]
|
[InlineData("Babe 01", "1")]
|
||||||
[InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")]
|
[InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")]
|
||||||
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
|
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
|
||||||
[InlineData("Superman v1 024 (09-10 1943)", "24")]
|
[InlineData("Superman v1 024 (09-10 1943)", "24")]
|
||||||
@ -78,6 +78,8 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")]
|
[InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")]
|
||||||
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")]
|
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")]
|
||||||
[InlineData("Batman Beyond 04 (of 6) (1999)", "4")]
|
[InlineData("Batman Beyond 04 (of 6) (1999)", "4")]
|
||||||
|
[InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")]
|
||||||
|
[InlineData("Y - The Last Man #001", "1")]
|
||||||
public void ParseComicChapterTest(string filename, string expected)
|
public void ParseComicChapterTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
|
||||||
|
BIN
API.Tests/Services/Test Data/ImageService/cover.expected.jpg
Normal file
BIN
API.Tests/Services/Test Data/ImageService/cover.expected.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 462 KiB |
@ -101,27 +101,7 @@ namespace API.Controllers
|
|||||||
user.Progresses ??= new List<AppUserProgress>();
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
foreach (var volume in volumes)
|
foreach (var volume in volumes)
|
||||||
{
|
{
|
||||||
foreach (var chapter in volume.Chapters)
|
_readerService.MarkChaptersAsRead(user, markReadDto.SeriesId, volume.Chapters);
|
||||||
{
|
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
|
||||||
|
|
||||||
if (userProgress == null)
|
|
||||||
{
|
|
||||||
user.Progresses.Add(new AppUserProgress
|
|
||||||
{
|
|
||||||
PagesRead = chapter.Pages,
|
|
||||||
VolumeId = volume.Id,
|
|
||||||
SeriesId = markReadDto.SeriesId,
|
|
||||||
ChapterId = chapter.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userProgress.PagesRead = chapter.Pages;
|
|
||||||
userProgress.SeriesId = markReadDto.SeriesId;
|
|
||||||
userProgress.VolumeId = volume.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
@ -135,30 +115,6 @@ namespace API.Controllers
|
|||||||
return BadRequest("There was an issue saving progress");
|
return BadRequest("There was an issue saving progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
|
||||||
{
|
|
||||||
AppUserProgress userProgress = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
userProgress =
|
|
||||||
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
|
|
||||||
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
|
||||||
if (progresses.Count > 1)
|
|
||||||
{
|
|
||||||
user.Progresses = new List<AppUserProgress>()
|
|
||||||
{
|
|
||||||
user.Progresses.First()
|
|
||||||
};
|
|
||||||
userProgress = user.Progresses.First();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a Series as Unread (progress)
|
/// Marks a Series as Unread (progress)
|
||||||
@ -175,7 +131,7 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
foreach (var chapter in volume.Chapters)
|
foreach (var chapter in volume.Chapters)
|
||||||
{
|
{
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
var userProgress = ReaderService.GetUserProgressForChapter(user, chapter);
|
||||||
|
|
||||||
if (userProgress == null) continue;
|
if (userProgress == null) continue;
|
||||||
userProgress.PagesRead = 0;
|
userProgress.PagesRead = 0;
|
||||||
@ -206,28 +162,7 @@ namespace API.Controllers
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
foreach (var chapter in chapters)
|
_readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
{
|
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
|
||||||
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
|
||||||
|
|
||||||
if (userProgress == null)
|
|
||||||
{
|
|
||||||
user.Progresses.Add(new AppUserProgress
|
|
||||||
{
|
|
||||||
PagesRead = 0,
|
|
||||||
VolumeId = markVolumeReadDto.VolumeId,
|
|
||||||
SeriesId = markVolumeReadDto.SeriesId,
|
|
||||||
ChapterId = chapter.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userProgress.PagesRead = 0;
|
|
||||||
userProgress.SeriesId = markVolumeReadDto.SeriesId;
|
|
||||||
userProgress.VolumeId = markVolumeReadDto.VolumeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
@ -250,27 +185,119 @@ namespace API.Controllers
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
foreach (var chapter in chapters)
|
_readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
{
|
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
|
||||||
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
|
||||||
|
|
||||||
if (userProgress == null)
|
_unitOfWork.UserRepository.Update(user);
|
||||||
{
|
|
||||||
user.Progresses.Add(new AppUserProgress
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
PagesRead = chapter.Pages,
|
return Ok();
|
||||||
VolumeId = markVolumeReadDto.VolumeId,
|
}
|
||||||
SeriesId = markVolumeReadDto.SeriesId,
|
|
||||||
ChapterId = chapter.Id
|
return BadRequest("Could not save progress");
|
||||||
});
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
/// <summary>
|
||||||
userProgress.PagesRead = chapter.Pages;
|
/// Marks all chapters within a list of volumes as Read. All volumes must belong to the same Series.
|
||||||
userProgress.SeriesId = markVolumeReadDto.SeriesId;
|
/// </summary>
|
||||||
userProgress.VolumeId = markVolumeReadDto.VolumeId;
|
/// <param name="dto"></param>
|
||||||
}
|
/// <returns></returns>
|
||||||
|
[HttpPost("mark-multiple-read")]
|
||||||
|
public async Task<ActionResult> MarkMultipleAsRead(MarkVolumesReadDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
|
||||||
|
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||||
|
foreach (var chapterId in dto.ChapterIds)
|
||||||
|
{
|
||||||
|
chapterIds.Add(chapterId);
|
||||||
|
}
|
||||||
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
|
_readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Could not save progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all chapters within a list of volumes as Unread. All volumes must belong to the same Series.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("mark-multiple-unread")]
|
||||||
|
public async Task<ActionResult> MarkMultipleAsUnread(MarkVolumesReadDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
|
||||||
|
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||||
|
foreach (var chapterId in dto.ChapterIds)
|
||||||
|
{
|
||||||
|
chapterIds.Add(chapterId);
|
||||||
|
}
|
||||||
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
|
_readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Could not save progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all chapters within a list of series as Read.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("mark-multiple-series-read")]
|
||||||
|
public async Task<ActionResult> MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
|
||||||
|
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
_readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Could not save progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all chapters within a list of series as Unread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("mark-multiple-series-unread")]
|
||||||
|
public async Task<ActionResult> MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
|
||||||
|
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
_readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
@ -212,7 +212,7 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update a
|
/// Update the properites (title, summary) of a reading list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -242,6 +242,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not update reading list");
|
return BadRequest("Could not update reading list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds all chapters from a Series to a reading list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("update-by-series")]
|
[HttpPost("update-by-series")]
|
||||||
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
|
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
|
||||||
{
|
{
|
||||||
@ -273,6 +278,86 @@ namespace API.Controllers
|
|||||||
return Ok("Nothing to do");
|
return Ok("Nothing to do");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds all chapters from a list of volumes and chapters to a reading list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("update-by-multiple")]
|
||||||
|
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||||
|
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||||
|
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||||
|
|
||||||
|
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||||
|
foreach (var chapterId in dto.ChapterIds)
|
||||||
|
{
|
||||||
|
chapterIds.Add(chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are adds, tell tracking this has been modified
|
||||||
|
if (await AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
|
||||||
|
{
|
||||||
|
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
return Ok("Updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok("Nothing to do");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds all chapters from a list of series to a reading list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("update-by-multiple-series")]
|
||||||
|
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||||
|
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||||
|
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||||
|
|
||||||
|
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
|
||||||
|
|
||||||
|
foreach (var seriesId in ids.Keys)
|
||||||
|
{
|
||||||
|
// If there are adds, tell tracking this has been modified
|
||||||
|
if (await AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
|
||||||
|
{
|
||||||
|
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
return Ok("Updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok("Nothing to do");
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("update-by-volume")]
|
[HttpPost("update-by-volume")]
|
||||||
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
||||||
{
|
{
|
||||||
|
9
API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs
Normal file
9
API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.Reader
|
||||||
|
{
|
||||||
|
public class MarkMultipleSeriesAsReadDto
|
||||||
|
{
|
||||||
|
public IReadOnlyList<int> SeriesIds { get; init; }
|
||||||
|
}
|
||||||
|
}
|
20
API/DTOs/Reader/MarkVolumesReadDto.cs
Normal file
20
API/DTOs/Reader/MarkVolumesReadDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.Reader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is used for bulk updating a set of volume and or chapters in one go
|
||||||
|
/// </summary>
|
||||||
|
public class MarkVolumesReadDto
|
||||||
|
{
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// A list of Volumes to mark read
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<int> VolumeIds { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// A list of additional Chapters to mark as read
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<int> ChapterIds { get; set; }
|
||||||
|
}
|
||||||
|
}
|
12
API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs
Normal file
12
API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.ReadingLists
|
||||||
|
{
|
||||||
|
public class UpdateReadingListByMultipleDto
|
||||||
|
{
|
||||||
|
public int SeriesId { get; init; }
|
||||||
|
public int ReadingListId { get; init; }
|
||||||
|
public IReadOnlyList<int> VolumeIds { get; init; }
|
||||||
|
public IReadOnlyList<int> ChapterIds { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.ReadingLists
|
||||||
|
{
|
||||||
|
public class UpdateReadingListByMultipleSeriesDto
|
||||||
|
{
|
||||||
|
public int ReadingListId { get; init; }
|
||||||
|
public IReadOnlyList<int> SeriesIds { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -188,11 +189,16 @@ namespace API.Data.Repositories
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesIds"></param>
|
/// <param name="seriesIds"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds)
|
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false)
|
||||||
{
|
{
|
||||||
return await _context.Volume
|
var query = _context.Volume
|
||||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
.Where(v => seriesIds.Contains(v.SeriesId));
|
||||||
.ToListAsync();
|
|
||||||
|
if (includeChapters)
|
||||||
|
{
|
||||||
|
query = query.Include(v => v.Chapters);
|
||||||
|
}
|
||||||
|
return await query.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteSeriesAsync(int seriesId)
|
public async Task<bool> DeleteSeriesAsync(int seriesId)
|
||||||
@ -237,6 +243,35 @@ namespace API.Data.Repositories
|
|||||||
return chapterIds.ToArray();
|
return chapterIds.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This returns a list of tuples<chapterId, seriesId> back for each series id passed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesIds"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds)
|
||||||
|
{
|
||||||
|
var volumes = await _context.Volume
|
||||||
|
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||||
|
.Include(v => v.Chapters)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var seriesChapters = new Dictionary<int, IList<int>>();
|
||||||
|
foreach (var v in volumes)
|
||||||
|
{
|
||||||
|
foreach (var c in v.Chapters)
|
||||||
|
{
|
||||||
|
if (!seriesChapters.ContainsKey(v.SeriesId))
|
||||||
|
{
|
||||||
|
var list = new List<int>();
|
||||||
|
seriesChapters.Add(v.SeriesId, list);
|
||||||
|
}
|
||||||
|
seriesChapters[v.SeriesId].Add(c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesChapters;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||||
{
|
{
|
||||||
var userProgress = await _context.AppUserProgresses
|
var userProgress = await _context.AppUserProgresses
|
||||||
|
@ -44,5 +44,13 @@ namespace API.Data.Repositories
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds)
|
||||||
|
{
|
||||||
|
return await _context.Chapter
|
||||||
|
.Where(c => volumeIds.Contains(c.VolumeId))
|
||||||
|
.Select(c => c.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
@ -44,11 +45,12 @@ namespace API.Interfaces.Repositories
|
|||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<VolumeDto> GetVolumeDtoAsync(int volumeId);
|
Task<VolumeDto> GetVolumeDtoAsync(int volumeId);
|
||||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
|
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
||||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||||
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
||||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
Task<Series> GetSeriesByIdAsync(int seriesId);
|
||||||
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
|
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
|
||||||
|
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to add Progress/Rating information to series list.
|
/// Used to add Progress/Rating information to series list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -10,5 +10,6 @@ namespace API.Interfaces.Repositories
|
|||||||
void Update(Volume volume);
|
void Update(Volume volume);
|
||||||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||||
Task<string> GetVolumeCoverImageAsync(int volumeId);
|
Task<string> GetVolumeCoverImageAsync(int volumeId);
|
||||||
|
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
|
||||||
namespace API.Interfaces.Services
|
namespace API.Interfaces.Services
|
||||||
{
|
{
|
||||||
public interface IReaderService
|
public interface IReaderService
|
||||||
{
|
{
|
||||||
|
void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||||
|
void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||||
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
||||||
Task<int> CapPageToChapter(int chapterId, int page);
|
Task<int> CapPageToChapter(int chapterId, int page);
|
||||||
Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);
|
Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);
|
||||||
|
@ -25,6 +25,99 @@ namespace API.Interfaces.Services
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="chapters"></param>
|
||||||
|
public void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||||
|
{
|
||||||
|
foreach (var chapter in chapters)
|
||||||
|
{
|
||||||
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
|
|
||||||
|
if (userProgress == null)
|
||||||
|
{
|
||||||
|
user.Progresses.Add(new AppUserProgress
|
||||||
|
{
|
||||||
|
PagesRead = chapter.Pages,
|
||||||
|
VolumeId = chapter.VolumeId,
|
||||||
|
SeriesId = seriesId,
|
||||||
|
ChapterId = chapter.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userProgress.PagesRead = chapter.Pages;
|
||||||
|
userProgress.SeriesId = seriesId;
|
||||||
|
userProgress.VolumeId = chapter.VolumeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all Chapters as Unread by creating or updating UserProgress rows. Does not commit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="chapters"></param>
|
||||||
|
public void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||||
|
{
|
||||||
|
foreach (var chapter in chapters)
|
||||||
|
{
|
||||||
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
|
|
||||||
|
if (userProgress == null)
|
||||||
|
{
|
||||||
|
user.Progresses.Add(new AppUserProgress
|
||||||
|
{
|
||||||
|
PagesRead = 0,
|
||||||
|
VolumeId = chapter.VolumeId,
|
||||||
|
SeriesId = seriesId,
|
||||||
|
ChapterId = chapter.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userProgress.PagesRead = 0;
|
||||||
|
userProgress.SeriesId = seriesId;
|
||||||
|
userProgress.VolumeId = chapter.VolumeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="chapter"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||||
|
{
|
||||||
|
AppUserProgress userProgress = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userProgress =
|
||||||
|
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
|
||||||
|
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||||
|
if (progresses.Count > 1)
|
||||||
|
{
|
||||||
|
user.Progresses = new List<AppUserProgress>()
|
||||||
|
{
|
||||||
|
user.Progresses.First()
|
||||||
|
};
|
||||||
|
userProgress = user.Progresses.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProgress;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves progress to DB
|
/// Saves progress to DB
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -82,6 +175,12 @@ namespace API.Interfaces.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that the page is within 0 and total pages for a chapter. Makes one DB call.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<int> CapPageToChapter(int chapterId, int page)
|
public async Task<int> CapPageToChapter(int chapterId, int page)
|
||||||
{
|
{
|
||||||
var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId);
|
var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId);
|
||||||
|
@ -383,22 +383,22 @@ namespace API.Parser
|
|||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?<Series>.*)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)",
|
@"^(?<Series>.+?)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
|
||||||
RegexTimeout),
|
|
||||||
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
|
||||||
new Regex(
|
|
||||||
@"^(?<Series>.*)(?: (?<Volume>\d+))",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
|
||||||
RegexTimeout),
|
|
||||||
// Batman & Robin the Teen Wonder #0
|
|
||||||
new Regex(
|
|
||||||
@"^(?<Series>.*)(?: |_)#(?<Volume>\d+)",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
// Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr
|
// Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?<Series>.*)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-",
|
@"^(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
|
RegexTimeout),
|
||||||
|
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
|
||||||
|
new Regex(
|
||||||
|
@"^(?<Series>.+?)(?: (?<Chapter>\d+))",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
|
RegexTimeout),
|
||||||
|
// Batman & Robin the Teen Wonder #0
|
||||||
|
new Regex(
|
||||||
|
@"^(?<Series>.+?)(?:\s|_)#(?<Chapter>\d+)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
// Saga 001 (2012) (Digital) (Empire-Zone)
|
// Saga 001 (2012) (Digital) (Empire-Zone)
|
||||||
@ -408,12 +408,12 @@ namespace API.Parser
|
|||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
// Amazing Man Comics chapter 25
|
// Amazing Man Comics chapter 25
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?!Vol)(?<Series>.*)( |_)c(hapter)( |_)(?<Chapter>\d*)",
|
@"^(?!Vol)(?<Series>.+?)( |_)c(hapter)( |_)(?<Chapter>\d*)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
// Amazing Man Comics issue #25
|
// Amazing Man Comics issue #25
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?!Vol)(?<Series>.*)( |_)i(ssue)( |_) #(?<Chapter>\d*)",
|
@"^(?!Vol)(?<Series>.+?)( |_)i(ssue)( |_) #(?<Chapter>\d*)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||||
RegexTimeout),
|
RegexTimeout),
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@ export type SeriesActionCallback = (series: Series) => void;
|
|||||||
export type VolumeActionCallback = (volume: Volume) => void;
|
export type VolumeActionCallback = (volume: Volume) => void;
|
||||||
export type ChapterActionCallback = (chapter: Chapter) => void;
|
export type ChapterActionCallback = (chapter: Chapter) => void;
|
||||||
export type ReadingListActionCallback = (readingList: ReadingList) => void;
|
export type ReadingListActionCallback = (readingList: ReadingList) => void;
|
||||||
|
export type VoidActionCallback = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for executing actions
|
* Responsible for executing actions
|
||||||
@ -203,6 +204,85 @@ export class ActionService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all chapters and the volumes as Read. All volumes and chapters must belong to a series
|
||||||
|
* @param seriesId Series Id
|
||||||
|
* @param volumes Volumes, should have id, chapters and pagesRead populated
|
||||||
|
* @param chapters? Chapters, should have id
|
||||||
|
* @param callback Optional callback to perform actions after API completes
|
||||||
|
*/
|
||||||
|
markMultipleAsRead(seriesId: number, volumes: Array<Volume>, chapters?: Array<Chapter>, callback?: VoidActionCallback) {
|
||||||
|
this.readerService.markMultipleRead(seriesId, volumes.map(v => v.id), chapters?.map(c => c.id)).pipe(take(1)).subscribe(() => {
|
||||||
|
volumes.forEach(volume => {
|
||||||
|
volume.pagesRead = volume.pages;
|
||||||
|
volume.chapters?.forEach(c => c.pagesRead = c.pages);
|
||||||
|
});
|
||||||
|
chapters?.forEach(c => c.pagesRead = c.pages);
|
||||||
|
this.toastr.success('Marked as Read');
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all chapters and the volumes as Unread. All volumes must belong to a series
|
||||||
|
* @param seriesId Series Id
|
||||||
|
* @param volumes Volumes, should have id, chapters and pagesRead populated
|
||||||
|
* @param callback Optional callback to perform actions after API completes
|
||||||
|
*/
|
||||||
|
markMultipleAsUnread(seriesId: number, volumes: Array<Volume>, chapters?: Array<Chapter>, callback?: VoidActionCallback) {
|
||||||
|
this.readerService.markMultipleUnread(seriesId, volumes.map(v => v.id), chapters?.map(c => c.id)).pipe(take(1)).subscribe(() => {
|
||||||
|
volumes.forEach(volume => {
|
||||||
|
volume.pagesRead = volume.pages;
|
||||||
|
volume.chapters?.forEach(c => c.pagesRead = c.pages);
|
||||||
|
});
|
||||||
|
chapters?.forEach(c => c.pagesRead = c.pages);
|
||||||
|
this.toastr.success('Marked as Read');
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all series as Read.
|
||||||
|
* @param series Series, should have id, pagesRead populated
|
||||||
|
* @param callback Optional callback to perform actions after API completes
|
||||||
|
*/
|
||||||
|
markMultipleSeriesAsRead(series: Array<Series>, callback?: VoidActionCallback) {
|
||||||
|
this.readerService.markMultipleSeriesRead(series.map(v => v.id)).pipe(take(1)).subscribe(() => {
|
||||||
|
series.forEach(s => {
|
||||||
|
s.pagesRead = s.pages;
|
||||||
|
});
|
||||||
|
this.toastr.success('Marked as Read');
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all series as Unread.
|
||||||
|
* @param series Series, should have id, pagesRead populated
|
||||||
|
* @param callback Optional callback to perform actions after API completes
|
||||||
|
*/
|
||||||
|
markMultipleSeriesAsUnread(series: Array<Series>, callback?: VoidActionCallback) {
|
||||||
|
this.readerService.markMultipleSeriesUnread(series.map(v => v.id)).pipe(take(1)).subscribe(() => {
|
||||||
|
series.forEach(s => {
|
||||||
|
s.pagesRead = s.pages;
|
||||||
|
});
|
||||||
|
this.toastr.success('Marked as Unread');
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
openBookmarkModal(series: Series, callback?: SeriesActionCallback) {
|
openBookmarkModal(series: Series, callback?: SeriesActionCallback) {
|
||||||
if (this.bookmarkModalRef != null) { return; }
|
if (this.bookmarkModalRef != null) { return; }
|
||||||
@ -222,6 +302,52 @@ export class ActionService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMultipleToReadingList(seriesId: number, volumes: Array<Volume>, chapters?: Array<Chapter>, callback?: VoidActionCallback) {
|
||||||
|
if (this.readingListModalRef != null) { return; }
|
||||||
|
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' });
|
||||||
|
this.readingListModalRef.componentInstance.seriesId = seriesId;
|
||||||
|
this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id);
|
||||||
|
this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id);
|
||||||
|
this.readingListModalRef.componentInstance.title = 'Multiple Selections';
|
||||||
|
this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple;
|
||||||
|
|
||||||
|
|
||||||
|
this.readingListModalRef.closed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.readingListModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.readingListModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMultipleSeriesToReadingList(series: Array<Series>, callback?: VoidActionCallback) {
|
||||||
|
if (this.readingListModalRef != null) { return; }
|
||||||
|
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' });
|
||||||
|
this.readingListModalRef.componentInstance.seriesIds = series.map(v => v.id);
|
||||||
|
this.readingListModalRef.componentInstance.title = 'Multiple Selections';
|
||||||
|
this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series;
|
||||||
|
|
||||||
|
|
||||||
|
this.readingListModalRef.closed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.readingListModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.readingListModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) {
|
addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) {
|
||||||
if (this.readingListModalRef != null) { return; }
|
if (this.readingListModalRef != null) { return; }
|
||||||
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' });
|
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' });
|
||||||
|
@ -37,7 +37,7 @@ export class LibraryService {
|
|||||||
listDirectories(rootPath: string) {
|
listDirectories(rootPath: string) {
|
||||||
let query = '';
|
let query = '';
|
||||||
if (rootPath !== undefined && rootPath.length > 0) {
|
if (rootPath !== undefined && rootPath.length > 0) {
|
||||||
query = '?path=' + rootPath;
|
query = '?path=' + encodeURIComponent(rootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient.get<string[]>(this.baseUrl + 'library/list' + query);
|
return this.httpClient.get<string[]>(this.baseUrl + 'library/list' + query);
|
||||||
|
@ -68,9 +68,26 @@ export class ReaderService {
|
|||||||
return this.httpClient.post(this.baseUrl + 'reader/mark-volume-read', {seriesId, volumeId});
|
return this.httpClient.post(this.baseUrl + 'reader/mark-volume-read', {seriesId, volumeId});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markMultipleRead(seriesId: number, volumeIds: Array<number>, chapterIds?: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/mark-multiple-read', {seriesId, volumeIds, chapterIds});
|
||||||
|
}
|
||||||
|
|
||||||
|
markMultipleUnread(seriesId: number, volumeIds: Array<number>, chapterIds?: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/mark-multiple-unread', {seriesId, volumeIds, chapterIds});
|
||||||
|
}
|
||||||
|
|
||||||
|
markMultipleSeriesRead(seriesIds: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/mark-multiple-series-read', {seriesIds});
|
||||||
|
}
|
||||||
|
|
||||||
|
markMultipleSeriesUnread(seriesIds: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/mark-multiple-series-unread', {seriesIds});
|
||||||
|
}
|
||||||
|
|
||||||
markVolumeUnread(seriesId: number, volumeId: number) {
|
markVolumeUnread(seriesId: number, volumeId: number) {
|
||||||
return this.httpClient.post(this.baseUrl + 'reader/mark-volume-unread', {seriesId, volumeId});
|
return this.httpClient.post(this.baseUrl + 'reader/mark-volume-unread', {seriesId, volumeId});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getNextChapter(seriesId: number, volumeId: number, currentChapterId: number, readingListId: number = -1) {
|
getNextChapter(seriesId: number, volumeId: number, currentChapterId: number, readingListId: number = -1) {
|
||||||
if (readingListId > 0) {
|
if (readingListId > 0) {
|
||||||
|
@ -42,6 +42,14 @@ export class ReadingListService {
|
|||||||
return this.httpClient.post(this.baseUrl + 'readinglist/update', model, { responseType: 'text' as 'json' });
|
return this.httpClient.post(this.baseUrl + 'readinglist/update', model, { responseType: 'text' as 'json' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateByMultiple(readingListId: number, seriesId: number, volumeIds: Array<number>, chapterIds?: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple', {readingListId, seriesId, volumeIds, chapterIds});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateByMultipleSeries(readingListId: number, seriesIds: Array<number>) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple-series', {readingListId, seriesIds});
|
||||||
|
}
|
||||||
|
|
||||||
updateBySeries(readingListId: number, seriesId: number) {
|
updateBySeries(readingListId: number, seriesId: number) {
|
||||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-series', {readingListId, seriesId}, { responseType: 'text' as 'json' });
|
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-series', {readingListId, seriesId}, { responseType: 'text' as 'json' });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<div *ngIf="bulkSelectionService.hasSelections()" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||||
|
<div class="d-flex justify-content-around align-items-center">
|
||||||
|
<span class="highlight"><i class="fa fa-check" aria-hidden="true"></i> {{bulkSelectionService.totalSelections()}} selected</span>
|
||||||
|
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||||
|
<span id="bulk-actions-header" class="sr-only">Bulk Actions</span>
|
||||||
|
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
@import "../../../theme/colors";
|
||||||
|
@import "../../../assets/themes/dark.scss";
|
||||||
|
|
||||||
|
.bulk-select {
|
||||||
|
background-color: $dark-form-background-no-opacity;
|
||||||
|
border-bottom: 2px solid $primary-color;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: $primary-color !important;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
|
import { BulkSelectionService } from '../bulk-selection.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bulk-operations',
|
||||||
|
templateUrl: './bulk-operations.component.html',
|
||||||
|
styleUrls: ['./bulk-operations.component.scss']
|
||||||
|
})
|
||||||
|
export class BulkOperationsComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() actionCallback!: (action: Action, data: any) => void;
|
||||||
|
topOffset: number = 0;
|
||||||
|
|
||||||
|
get actions() {
|
||||||
|
return this.bulkSelectionService.getActions(this.actionCallback.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public bulkSelectionService: BulkSelectionService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const navBar = document.querySelector('.navbar');
|
||||||
|
if (navBar) {
|
||||||
|
this.topOffset = Math.ceil(navBar.getBoundingClientRect().height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActionCallback(action: Action, data: any) {
|
||||||
|
this.actionCallback(action, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
performAction(action: ActionItem<any>) {
|
||||||
|
if (typeof action.callback === 'function') {
|
||||||
|
action.callback(action.action, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
147
UI/Web/src/app/cards/bulk-selection.service.ts
Normal file
147
UI/Web/src/app/cards/bulk-selection.service.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { Action, ActionFactoryService } from '../_services/action-factory.service';
|
||||||
|
|
||||||
|
type DataSource = 'volume' | 'chapter' | 'special' | 'series';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops.
|
||||||
|
* This will clear selections between pages.
|
||||||
|
*
|
||||||
|
* Remakrs: Page which renders cards is responsible for listening for shift keydown/keyup and updating our state variable.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BulkSelectionService {
|
||||||
|
|
||||||
|
private debug: boolean = false;
|
||||||
|
private prevIndex: number = 0;
|
||||||
|
private prevDataSource!: DataSource;
|
||||||
|
private selectedCards: { [key: string]: {[key: number]: boolean} } = {};
|
||||||
|
private dataSourceMax: { [key: string]: number} = {};
|
||||||
|
public isShiftDown: boolean = false;
|
||||||
|
|
||||||
|
constructor(private router: Router, private actionFactory: ActionFactoryService) {
|
||||||
|
router.events
|
||||||
|
.pipe(filter(event => event instanceof NavigationStart))
|
||||||
|
.subscribe((event) => {
|
||||||
|
this.deselectAll();
|
||||||
|
this.dataSourceMax = {};
|
||||||
|
this.prevIndex = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCardSelection(dataSource: DataSource, index: number, maxIndex: number, wasSelected: boolean) {
|
||||||
|
if (this.isShiftDown) {
|
||||||
|
|
||||||
|
if (dataSource === this.prevDataSource) {
|
||||||
|
this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index);
|
||||||
|
this.selectCards(dataSource, this.prevIndex, index, !wasSelected);
|
||||||
|
} else {
|
||||||
|
const isForwardSelection = index < this.prevIndex;
|
||||||
|
|
||||||
|
if (isForwardSelection) {
|
||||||
|
this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + this.prevIndex + ' to ' + this.dataSourceMax[this.prevDataSource]);
|
||||||
|
this.selectCards(this.prevDataSource, this.prevIndex, this.dataSourceMax[this.prevDataSource], !wasSelected);
|
||||||
|
this.debugLog('Selecting ' + dataSource + ' cards from ' + 0 + ' to ' + index);
|
||||||
|
this.selectCards(dataSource, 0, index, !wasSelected);
|
||||||
|
} else {
|
||||||
|
this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + 0 + ' to ' + this.prevIndex);
|
||||||
|
this.selectCards(this.prevDataSource, this.prevIndex, 0, !wasSelected);
|
||||||
|
this.debugLog('Selecting ' + dataSource + ' cards from ' + index + ' to ' + maxIndex);
|
||||||
|
this.selectCards(dataSource, index, maxIndex, !wasSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.debugLog('Selecting ' + dataSource + ' cards at ' + index);
|
||||||
|
this.selectCards(dataSource, index, index, !wasSelected);
|
||||||
|
}
|
||||||
|
this.prevIndex = index;
|
||||||
|
this.prevDataSource = dataSource;
|
||||||
|
this.dataSourceMax[dataSource] = maxIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCardSelected(dataSource: DataSource, index: number) {
|
||||||
|
if (this.selectedCards.hasOwnProperty(dataSource) && this.selectedCards[dataSource].hasOwnProperty(index)) {
|
||||||
|
return this.selectedCards[dataSource][index];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCards(dataSource: DataSource, from: number, to: number, value: boolean) {
|
||||||
|
if (!this.selectedCards.hasOwnProperty(dataSource)) {
|
||||||
|
this.selectedCards[dataSource] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from === to) {
|
||||||
|
this.selectedCards[dataSource][to] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from > to) {
|
||||||
|
for (let i = to; i <= from; i++) {
|
||||||
|
this.selectedCards[dataSource][i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = from; i <= to; i++) {
|
||||||
|
this.selectedCards[dataSource][i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectAll() {
|
||||||
|
this.selectedCards = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelections() {
|
||||||
|
const keys = Object.keys(this.selectedCards);
|
||||||
|
return keys.filter(key => {
|
||||||
|
return Object.values(this.selectedCards[key]).filter(item => item).length > 0;
|
||||||
|
}).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSelections() {
|
||||||
|
let sum = 0;
|
||||||
|
const keys = Object.keys(this.selectedCards);
|
||||||
|
keys.forEach(key => {
|
||||||
|
sum += Object.values(this.selectedCards[key]).filter(item => item).length;
|
||||||
|
});
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedCardsForSource(dataSource: DataSource) {
|
||||||
|
if (!this.selectedCards.hasOwnProperty(dataSource)) return [];
|
||||||
|
|
||||||
|
let ret = [];
|
||||||
|
for(let k in this.selectedCards[dataSource]) {
|
||||||
|
if (this.selectedCards[dataSource][k]) {
|
||||||
|
ret.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActions(callback: (action: Action, data: any) => void) {
|
||||||
|
// checks if series is present. If so, returns only series actions
|
||||||
|
// else returns volume/chapter items
|
||||||
|
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread];
|
||||||
|
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
|
||||||
|
return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.actionFactory.getVolumeActions(callback).filter(item => allowedActions.includes(item.action));
|
||||||
|
}
|
||||||
|
|
||||||
|
private debugLog(message: string, extraData?: any) {
|
||||||
|
if (!this.debug) return;
|
||||||
|
|
||||||
|
if (extraData !== undefined) {
|
||||||
|
console.log(message, extraData);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,4 @@ export class CardActionablesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Insert hr to separate admin actions
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="overlay" (click)="handleClick()">
|
<div class="overlay" (click)="handleClick($event)">
|
||||||
<img *ngIf="total > 0 || supressArchiveWarning" class="img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
<img *ngIf="total > 0 || supressArchiveWarning" class="img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||||
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
||||||
<img *ngIf="total === 0 && !supressArchiveWarning" class="img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
<img *ngIf="total === 0 && !supressArchiveWarning" class="img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
||||||
@ -17,20 +17,22 @@
|
|||||||
<div class="error-banner" *ngIf="total === 0 && !supressArchiveWarning">
|
<div class="error-banner" *ngIf="total === 0 && !supressArchiveWarning">
|
||||||
Cannot Read
|
Cannot Read
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="not-read-badge" *ngIf="read === 0 && total > 0"></div>
|
<div class="not-read-badge" *ngIf="read === 0 && total > 0"></div>
|
||||||
|
<div class="bulk-mode {{bulkSelectionService.hasSelections() ? 'always-show' : ''}}" (click)="handleSelection($event)" *ngIf="allowSelection">
|
||||||
|
<input type="checkbox" attr.aria-labelledby="{{title}}_{{entity.id}}" [ngModel]="selected" [ngModelOptions]="{standalone: true}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<span class="card-title" placement="top" ngbTooltip="{{title}}" (click)="handleClick()" tabindex="0">
|
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" ngbTooltip="{{title}}" (click)="handleClick()" tabindex="0">
|
||||||
<span *ngIf="isPromoted()">
|
<span *ngIf="isPromoted()">
|
||||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">(promoted)</span>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="sr-only">{{utilityService.mangaFormat(format)}}</span>
|
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="sr-only">{{utilityService.mangaFormat(format)}}</span>
|
||||||
{{title}}
|
{{title}}
|
||||||
<span class="sr-only">(promoted)</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="card-actions float-right">
|
<span class="card-actions float-right">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||||
|
@ -38,6 +38,8 @@ $image-width: 160px;
|
|||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.img-top {
|
.img-top {
|
||||||
height: $image-height;
|
height: $image-height;
|
||||||
}
|
}
|
||||||
@ -71,12 +73,36 @@ $image-width: 160px;
|
|||||||
border-color: transparent $primary-color transparent transparent;
|
border-color: transparent $primary-color transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.bulk-mode {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&.always-show {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
height: $image-height;
|
height: $image-height;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
|
.bulk-mode {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay-item {
|
.overlay-item {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { Volume } from 'src/app/_models/volume';
|
|||||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { LibraryService } from 'src/app/_services/library.service';
|
import { LibraryService } from 'src/app/_services/library.service';
|
||||||
|
import { BulkSelectionService } from '../bulk-selection.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-item',
|
selector: 'app-card-item',
|
||||||
@ -21,23 +22,71 @@ import { LibraryService } from 'src/app/_services/library.service';
|
|||||||
})
|
})
|
||||||
export class CardItemComponent implements OnInit, OnDestroy {
|
export class CardItemComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card item url. Will internally handle error and missing covers
|
||||||
|
*/
|
||||||
@Input() imageUrl = '';
|
@Input() imageUrl = '';
|
||||||
|
/**
|
||||||
|
* Name of the card
|
||||||
|
*/
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
|
/**
|
||||||
|
* Any actions to perform on the card
|
||||||
|
*/
|
||||||
@Input() actions: ActionItem<any>[] = [];
|
@Input() actions: ActionItem<any>[] = [];
|
||||||
@Input() read = 0; // Pages read
|
/**
|
||||||
@Input() total = 0; // Total Pages
|
* Pages Read
|
||||||
|
*/
|
||||||
|
@Input() read = 0;
|
||||||
|
/**
|
||||||
|
* Total Pages
|
||||||
|
*/
|
||||||
|
@Input() total = 0;
|
||||||
|
/**
|
||||||
|
* Supress library link
|
||||||
|
*/
|
||||||
@Input() supressLibraryLink = false;
|
@Input() supressLibraryLink = false;
|
||||||
@Input() entity!: Series | Volume | Chapter | CollectionTag; // This is the entity we are representing. It will be returned if an action is executed.
|
/**
|
||||||
|
* This is the entity we are representing. It will be returned if an action is executed.
|
||||||
|
*/
|
||||||
|
@Input() entity!: Series | Volume | Chapter | CollectionTag;
|
||||||
|
/**
|
||||||
|
* If the entity is selected or not.
|
||||||
|
*/
|
||||||
|
@Input() selected: boolean = false;
|
||||||
|
/**
|
||||||
|
* If the entity should show selection code
|
||||||
|
*/
|
||||||
|
@Input() allowSelection: boolean = false;
|
||||||
|
/**
|
||||||
|
* Event emitted when item is clicked
|
||||||
|
*/
|
||||||
@Output() clicked = new EventEmitter<string>();
|
@Output() clicked = new EventEmitter<string>();
|
||||||
|
/**
|
||||||
libraryName: string | undefined = undefined; // Library name item belongs to
|
* When the card is selected.
|
||||||
|
*/
|
||||||
|
@Output() selection = new EventEmitter<boolean>();
|
||||||
|
/**
|
||||||
|
* Library name item belongs to
|
||||||
|
*/
|
||||||
|
libraryName: string | undefined = undefined;
|
||||||
libraryId: number | undefined = undefined;
|
libraryId: number | undefined = undefined;
|
||||||
supressArchiveWarning: boolean = false; // This will supress the cannot read archive warning when total pages is 0
|
/**
|
||||||
|
* This will supress the cannot read archive warning when total pages is 0
|
||||||
|
*/
|
||||||
|
supressArchiveWarning: boolean = false;
|
||||||
|
/**
|
||||||
|
* Format of the entity (only applies to Series)
|
||||||
|
*/
|
||||||
format: MangaFormat = MangaFormat.UNKNOWN;
|
format: MangaFormat = MangaFormat.UNKNOWN;
|
||||||
|
|
||||||
|
|
||||||
download$: Observable<Download> | null = null;
|
download$: Observable<Download> | null = null;
|
||||||
downloadInProgress: boolean = false;
|
downloadInProgress: boolean = false;
|
||||||
|
|
||||||
|
isShiftDown: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
get MangaFormat(): typeof MangaFormat {
|
get MangaFormat(): typeof MangaFormat {
|
||||||
return MangaFormat;
|
return MangaFormat;
|
||||||
}
|
}
|
||||||
@ -46,7 +95,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||||
private toastr: ToastrService) {}
|
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||||
@ -69,7 +118,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
this.onDestroy.complete();
|
this.onDestroy.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick() {
|
handleClick(event?: any) {
|
||||||
this.clicked.emit(this.title);
|
this.clicked.emit(this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +195,14 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
isPromoted() {
|
isPromoted() {
|
||||||
const tag = this.entity as CollectionTag;
|
const tag = this.entity as CollectionTag;
|
||||||
// TODO: Validate if this works with reading lists
|
|
||||||
return tag.hasOwnProperty('promoted') && tag.promoted;
|
return tag.hasOwnProperty('promoted') && tag.promoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleSelection(event?: any) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.selection.emit(this.selected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import { TypeaheadModule } from '../typeahead/typeahead.module';
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { CardDetailLayoutComponent } from './card-detail-layout/card-detail-layout.component';
|
import { CardDetailLayoutComponent } from './card-detail-layout/card-detail-layout.component';
|
||||||
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
|
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
|
||||||
|
import { BulkOperationsComponent } from './bulk-operations/bulk-operations.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +35,8 @@ import { CardDetailsModalComponent } from './_modals/card-details-modal/card-det
|
|||||||
BookmarksModalComponent,
|
BookmarksModalComponent,
|
||||||
CardActionablesComponent,
|
CardActionablesComponent,
|
||||||
CardDetailLayoutComponent,
|
CardDetailLayoutComponent,
|
||||||
CardDetailsModalComponent
|
CardDetailsModalComponent,
|
||||||
|
BulkOperationsComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -70,7 +72,8 @@ import { CardDetailsModalComponent } from './_modals/card-details-modal/card-det
|
|||||||
BookmarksModalComponent,
|
BookmarksModalComponent,
|
||||||
CardActionablesComponent,
|
CardActionablesComponent,
|
||||||
CardDetailLayoutComponent,
|
CardDetailLayoutComponent,
|
||||||
CardDetailsModalComponent
|
CardDetailsModalComponent,
|
||||||
|
BulkOperationsComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CardsModule { }
|
export class CardsModule { }
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
<ng-container *ngIf="data !== undefined">
|
<ng-container *ngIf="data !== undefined">
|
||||||
<app-card-item [title]="data.name" [actions]="actions" [supressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl" [entity]="data" [total]="data.pages" [read]="data.pagesRead" (clicked)="handleClick()"></app-card-item>
|
<app-card-item [title]="data.name" [actions]="actions" [supressLibraryLink]="suppressLibraryLink" [imageUrl]="imageUrl"
|
||||||
|
[entity]="data" [total]="data.pages" [read]="data.pagesRead" (clicked)="handleClick()"
|
||||||
|
[allowSelection]="allowSelection" (selection)="selection.emit(selected)" [selected]="selected"
|
||||||
|
></app-card-item>
|
||||||
</ng-container>
|
</ng-container>
|
@ -21,9 +21,22 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
@Input() data!: Series;
|
@Input() data!: Series;
|
||||||
@Input() libraryId = 0;
|
@Input() libraryId = 0;
|
||||||
@Input() suppressLibraryLink = false;
|
@Input() suppressLibraryLink = false;
|
||||||
|
/**
|
||||||
|
* If the entity is selected or not.
|
||||||
|
*/
|
||||||
|
@Input() selected: boolean = false;
|
||||||
|
/**
|
||||||
|
* If the entity should show selection code
|
||||||
|
*/
|
||||||
|
@Input() allowSelection: boolean = false;
|
||||||
|
|
||||||
@Output() clicked = new EventEmitter<Series>();
|
@Output() clicked = new EventEmitter<Series>();
|
||||||
@Output() reload = new EventEmitter<boolean>();
|
@Output() reload = new EventEmitter<boolean>();
|
||||||
@Output() dataChanged = new EventEmitter<Series>();
|
@Output() dataChanged = new EventEmitter<Series>();
|
||||||
|
/**
|
||||||
|
* When the card is selected.
|
||||||
|
*/
|
||||||
|
@Output() selection = new EventEmitter<boolean>();
|
||||||
|
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
actions: ActionItem<Series>[] = [];
|
actions: ActionItem<Series>[] = [];
|
||||||
|
@ -11,14 +11,6 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters mt-2 mb-2">
|
<div class="row no-gutters mt-2 mb-2">
|
||||||
<!-- <div>
|
|
||||||
<button class="btn btn-primary" (click)="read()" [disabled]="isLoading">
|
|
||||||
<span>
|
|
||||||
<i class="fa fa-book-open"></i>
|
|
||||||
</span>
|
|
||||||
<span class="read-btn--text"> Read</span>
|
|
||||||
</button>
|
|
||||||
</div> -->
|
|
||||||
<div class="ml-2" *ngIf="isAdmin">
|
<div class="ml-2" *ngIf="isAdmin">
|
||||||
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
||||||
<span>
|
<span>
|
||||||
@ -33,6 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
|
|
||||||
<app-card-detail-layout
|
<app-card-detail-layout
|
||||||
header="Series"
|
header="Series"
|
||||||
@ -44,7 +37,9 @@
|
|||||||
(applyFilter)="updateFilter($event)"
|
(applyFilter)="updateFilter($event)"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"></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>
|
</ng-template>
|
||||||
</app-card-detail-layout>
|
</app-card-detail-layout>
|
||||||
|
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, HostListener, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
|
||||||
import { UpdateFilterEvent } from 'src/app/cards/card-detail-layout/card-detail-layout.component';
|
import { UpdateFilterEvent } from 'src/app/cards/card-detail-layout/card-detail-layout.component';
|
||||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
||||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
|
||||||
import { Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter';
|
import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
|
import { ActionService } from 'src/app/_services/action.service';
|
||||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
@ -39,9 +40,31 @@ export class CollectionDetailComponent implements OnInit {
|
|||||||
mangaFormat: null
|
mangaFormat: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bulkActionCallback = (action: Action, data: any) => {
|
||||||
|
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||||
|
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case Action.AddToReadingList:
|
||||||
|
this.actionService.addMultipleSeriesToReadingList(selectedSeries);
|
||||||
|
break;
|
||||||
|
case Action.MarkAsRead:
|
||||||
|
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||||
|
this.loadPage();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Action.MarkAsUnread:
|
||||||
|
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||||
|
this.loadPage();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
||||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||||
private modalService: NgbModal, private titleService: Title, private accountService: AccountService, private utilityService: UtilityService) {
|
private modalService: NgbModal, private titleService: Title, private accountService: AccountService,
|
||||||
|
public bulkSelectionService: BulkSelectionService, private actionService: ActionService) {
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
@ -63,6 +86,20 @@ export class CollectionDetailComponent implements OnInit {
|
|||||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
|
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.shift', ['$event'])
|
||||||
|
handleKeypress(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keyup.shift', ['$event'])
|
||||||
|
handleKeyUp(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateTag(tagId: number) {
|
updateTag(tagId: number) {
|
||||||
this.collectionService.allTags().subscribe(tags => {
|
this.collectionService.allTags().subscribe(tags => {
|
||||||
this.collections = tags;
|
this.collections = tags;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
<app-card-detail-layout header="{{libraryName}}"
|
<app-card-detail-layout header="{{libraryName}}"
|
||||||
[isLoading]="loadingSeries"
|
[isLoading]="loadingSeries"
|
||||||
[items]="series"
|
[items]="series"
|
||||||
@ -8,6 +9,6 @@
|
|||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"></app-series-card>
|
<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>
|
</ng-template>
|
||||||
</app-card-detail-layout>
|
</app-card-detail-layout>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, HostListener, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component';
|
import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||||
|
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
import { Pagination } from '../_models/pagination';
|
import { Pagination } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
@ -30,9 +32,39 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
mangaFormat: null
|
mangaFormat: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bulkActionCallback = (action: Action, data: any) => {
|
||||||
|
console.log('handling bulk action callback');
|
||||||
|
// we need to figure out what is actually selected now
|
||||||
|
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||||
|
|
||||||
|
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case Action.AddToReadingList:
|
||||||
|
this.actionService.addMultipleSeriesToReadingList(selectedSeries);
|
||||||
|
break;
|
||||||
|
case Action.MarkAsRead:
|
||||||
|
console.log('marking series as read: ', selectedSeries)
|
||||||
|
|
||||||
|
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||||
|
this.loadPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Action.MarkAsUnread:
|
||||||
|
//console.log('marking volumes as unread: ', selectedVolumeIds)
|
||||||
|
//console.log('marking chapters as unread: ', chapters)
|
||||||
|
|
||||||
|
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||||
|
this.loadPage();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||||
private actionService: ActionService) {
|
private actionService: ActionService, public bulkSelectionService: BulkSelectionService) {
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('id');
|
||||||
if (routeId === null) {
|
if (routeId === null) {
|
||||||
this.router.navigateByUrl('/libraries');
|
this.router.navigateByUrl('/libraries');
|
||||||
@ -53,6 +85,20 @@ export class LibraryDetailComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.shift', ['$event'])
|
||||||
|
handleKeypress(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keyup.shift', ['$event'])
|
||||||
|
handleKeyUp(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleAction(action: Action, library: Library) {
|
handleAction(action: Action, library: Library) {
|
||||||
let lib: Partial<Library> = library;
|
let lib: Partial<Library> = library;
|
||||||
if (library === undefined) {
|
if (library === undefined) {
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
import { noUndefined } from '@angular/compiler/src/util';
|
||||||
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ReadingList } from 'src/app/_models/reading-list';
|
import { ReadingList } from 'src/app/_models/reading-list';
|
||||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||||
|
|
||||||
export enum ADD_FLOW {
|
export enum ADD_FLOW {
|
||||||
Series = 0,
|
Series = 0,
|
||||||
Volume = 1,
|
Volume = 1,
|
||||||
Chapter = 2
|
Chapter = 2,
|
||||||
|
Multiple = 3,
|
||||||
|
Multiple_Series
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -18,9 +22,31 @@ export enum ADD_FLOW {
|
|||||||
export class AddToListModalComponent implements OnInit, AfterViewInit {
|
export class AddToListModalComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
@Input() title!: string;
|
@Input() title!: string;
|
||||||
|
/**
|
||||||
|
* Only used in Series flow
|
||||||
|
*/
|
||||||
@Input() seriesId?: number;
|
@Input() seriesId?: number;
|
||||||
|
/**
|
||||||
|
* Only used in Volume flow
|
||||||
|
*/
|
||||||
@Input() volumeId?: number;
|
@Input() volumeId?: number;
|
||||||
|
/**
|
||||||
|
* Only used in Chapter flow
|
||||||
|
*/
|
||||||
@Input() chapterId?: number;
|
@Input() chapterId?: number;
|
||||||
|
/**
|
||||||
|
* Only used in Multiple flow
|
||||||
|
*/
|
||||||
|
@Input() volumeIds?: Array<number>;
|
||||||
|
/**
|
||||||
|
* Only used in Multiple flow
|
||||||
|
*/
|
||||||
|
@Input() chapterIds?: Array<number>;
|
||||||
|
/**
|
||||||
|
* Only used in Multiple_Series flow
|
||||||
|
*/
|
||||||
|
@Input() seriesIds?: Array<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines which Input is required and which API is used to associate to the Reading List
|
* Determines which Input is required and which API is used to associate to the Reading List
|
||||||
*/
|
*/
|
||||||
@ -36,7 +62,7 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
|
||||||
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService) { }
|
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService, private toastr: ToastrService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
@ -70,18 +96,34 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addToList(readingList: ReadingList) {
|
addToList(readingList: ReadingList) {
|
||||||
|
|
||||||
|
if (this.type === ADD_FLOW.Multiple_Series && this.seriesIds !== undefined) {
|
||||||
|
this.readingListService.updateByMultipleSeries(readingList.id, this.seriesIds).subscribe(() => {
|
||||||
|
this.toastr.success('Series added to reading list');
|
||||||
|
this.modal.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.seriesId === undefined) return;
|
if (this.seriesId === undefined) return;
|
||||||
|
|
||||||
if (this.type === ADD_FLOW.Series) {
|
if (this.type === ADD_FLOW.Series && this.seriesId !== undefined) {
|
||||||
this.readingListService.updateBySeries(readingList.id, this.seriesId).subscribe(() => {
|
this.readingListService.updateBySeries(readingList.id, this.seriesId).subscribe(() => {
|
||||||
|
this.toastr.success('Series added to reading list');
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
});
|
});
|
||||||
} else if (this.type === ADD_FLOW.Volume && this.volumeId !== undefined) {
|
} else if (this.type === ADD_FLOW.Volume && this.volumeId !== undefined) {
|
||||||
this.readingListService.updateByVolume(readingList.id, this.seriesId, this.volumeId).subscribe(() => {
|
this.readingListService.updateByVolume(readingList.id, this.seriesId, this.volumeId).subscribe(() => {
|
||||||
|
this.toastr.success('Volumes added to reading list');
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
});
|
});
|
||||||
} else if (this.type === ADD_FLOW.Chapter && this.chapterId !== undefined) {
|
} else if (this.type === ADD_FLOW.Chapter && this.chapterId !== undefined) {
|
||||||
this.readingListService.updateByChapter(readingList.id, this.seriesId, this.chapterId).subscribe(() => {
|
this.readingListService.updateByChapter(readingList.id, this.seriesId, this.chapterId).subscribe(() => {
|
||||||
|
this.toastr.success('Chapter added to reading list');
|
||||||
|
this.modal.close();
|
||||||
|
});
|
||||||
|
} else if (this.type === ADD_FLOW.Multiple && this.volumeIds !== undefined && this.chapterIds !== undefined) {
|
||||||
|
this.readingListService.updateByMultiple(readingList.id, this.seriesId, this.volumeIds, this.chapterIds).subscribe(() => {
|
||||||
|
this.toastr.success('Chapters and Volumes added to reading list');
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -97,15 +97,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav-tabs nav-pills" [destroyOnHide]="false">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav-tabs nav-pills" [destroyOnHide]="false">
|
||||||
<li [ngbNavItem]="1" *ngIf="hasSpecials">
|
<li [ngbNavItem]="1" *ngIf="hasSpecials">
|
||||||
<a ngbNavLink>Specials</a>
|
<a ngbNavLink>Specials</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div *ngFor="let chapter of specials; trackBy: trackByChapterIdentity">
|
<div *ngFor="let chapter of specials; let idx = index; trackBy: trackByChapterIdentity">
|
||||||
<app-card-item class="col-auto" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
|
<app-card-item class="col-auto" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
[imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||||
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item>
|
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true"></app-card-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -114,16 +115,16 @@
|
|||||||
<a ngbNavLink>Volumes/Chapters</a>
|
<a ngbNavLink>Volumes/Chapters</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div *ngFor="let volume of volumes; trackBy: trackByVolumeIdentity">
|
<div *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
|
||||||
<app-card-item class="col-auto" *ngIf="volume.number != 0" [entity]="volume" [title]="'Volume ' + volume.name" (click)="openVolume(volume)"
|
<app-card-item class="col-auto" *ngIf="volume.number != 0" [entity]="volume" [title]="'Volume ' + volume.name" (click)="openVolume(volume)"
|
||||||
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
|
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
|
||||||
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions"></app-card-item>
|
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let chapter of chapters; trackBy: trackByChapterIdentity">
|
<div *ngFor="let chapter of chapters; let idx = index; trackBy: trackByChapterIdentity">
|
||||||
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="'Chapter ' + chapter.range" (click)="openChapter(chapter)"
|
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="'Chapter ' + chapter.range" (click)="openChapter(chapter)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
|
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
|
||||||
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item>
|
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||||
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
|
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
|
||||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||||
import { ConfirmService } from '../shared/confirm.service';
|
import { ConfirmService } from '../shared/confirm.service';
|
||||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
||||||
import { DownloadService } from '../shared/_services/download.service';
|
import { DownloadService } from '../shared/_services/download.service';
|
||||||
import { UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
|
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
|
||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
||||||
@ -53,6 +54,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
seriesActions: ActionItem<Series>[] = [];
|
seriesActions: ActionItem<Series>[] = [];
|
||||||
volumeActions: ActionItem<Volume>[] = [];
|
volumeActions: ActionItem<Volume>[] = [];
|
||||||
chapterActions: ActionItem<Chapter>[] = [];
|
chapterActions: ActionItem<Chapter>[] = [];
|
||||||
|
bulkActions: ActionItem<any>[] = [];
|
||||||
|
|
||||||
hasSpecials = false;
|
hasSpecials = false;
|
||||||
specials: Array<Chapter> = [];
|
specials: Array<Chapter> = [];
|
||||||
@ -88,6 +90,48 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.pagesRead}`;
|
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.pagesRead}`;
|
||||||
|
|
||||||
|
bulkActionCallback = (action: Action, data: any) => {
|
||||||
|
console.log('handling bulk action callback');
|
||||||
|
if (this.series === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seriesId = this.series.id;
|
||||||
|
// we need to figure out what is actually selected now
|
||||||
|
const selectedVolumeIndexes = this.bulkSelectionService.getSelectedCardsForSource('volume');
|
||||||
|
const selectedChapterIndexes = this.bulkSelectionService.getSelectedCardsForSource('chapter');
|
||||||
|
const selectedSpecialIndexes = this.bulkSelectionService.getSelectedCardsForSource('special');
|
||||||
|
|
||||||
|
const selectedChapterIds = this.chapters.filter((chapter, index: number) => selectedChapterIndexes.includes(index + ''));
|
||||||
|
const selectedVolumeIds = this.volumes.filter((volume, index: number) => selectedVolumeIndexes.includes(index + ''));
|
||||||
|
const selectedSpecials = this.specials.filter((chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
|
||||||
|
const chapters = [...selectedChapterIds, ...selectedSpecials];
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case Action.AddToReadingList:
|
||||||
|
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, () => this.actionInProgress = false);
|
||||||
|
break;
|
||||||
|
case Action.MarkAsRead:
|
||||||
|
console.log('marking volumes as read: ', selectedVolumeIds)
|
||||||
|
console.log('marking chapters as read: ', chapters)
|
||||||
|
|
||||||
|
this.actionService.markMultipleAsRead(seriesId, selectedVolumeIds, chapters, () => {
|
||||||
|
this.setContinuePoint();
|
||||||
|
this.actionInProgress = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Action.MarkAsUnread:
|
||||||
|
console.log('marking volumes as unread: ', selectedVolumeIds)
|
||||||
|
console.log('marking chapters as unread: ', chapters)
|
||||||
|
|
||||||
|
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
|
||||||
|
this.setContinuePoint();
|
||||||
|
this.actionInProgress = false;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onDestroy: Subject<void> = new Subject();
|
private onDestroy: Subject<void> = new Subject();
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +155,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
||||||
private confirmService: ConfirmService, private titleService: Title,
|
private confirmService: ConfirmService, private titleService: Title,
|
||||||
private downloadService: DownloadService, private actionService: ActionService,
|
private downloadService: DownloadService, private actionService: ActionService,
|
||||||
public imageSerivce: ImageService, private messageHub: MessageHubService) {
|
public imageSerivce: ImageService, private messageHub: MessageHubService,
|
||||||
|
public bulkSelectionService: BulkSelectionService) {
|
||||||
ratingConfig.max = 5;
|
ratingConfig.max = 5;
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
@ -151,6 +196,20 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.onDestroy.complete();
|
this.onDestroy.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.shift', ['$event'])
|
||||||
|
handleKeypress(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keyup.shift', ['$event'])
|
||||||
|
handleKeyUp(event: KeyboardEvent) {
|
||||||
|
if (event.key === KEY_CODES.SHIFT) {
|
||||||
|
this.bulkSelectionService.isShiftDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleSeriesActionCallback(action: Action, series: Series) {
|
handleSeriesActionCallback(action: Action, series: Series) {
|
||||||
this.actionInProgress = true;
|
this.actionInProgress = true;
|
||||||
switch(action) {
|
switch(action) {
|
||||||
@ -281,6 +340,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
|
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
|
||||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||||
this.chapters = volumes.filter(v => v.number === 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters);
|
this.chapters = volumes.filter(v => v.number === 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters);
|
||||||
@ -490,7 +550,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadSeries() {
|
downloadSeries() {
|
||||||
|
|
||||||
this.downloadService.downloadSeriesSize(this.series.id).pipe(take(1)).subscribe(async (size) => {
|
this.downloadService.downloadSeriesSize(this.series.id).pipe(take(1)).subscribe(async (size) => {
|
||||||
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
|
@ -15,7 +15,8 @@ export enum KEY_CODES {
|
|||||||
G = 'g',
|
G = 'g',
|
||||||
B = 'b',
|
B = 'b',
|
||||||
BACKSPACE = 'Backspace',
|
BACKSPACE = 'Backspace',
|
||||||
DELETE = 'Delete'
|
DELETE = 'Delete',
|
||||||
|
SHIFT = 'Shift'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Breakpoint {
|
export enum Breakpoint {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user