mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* Added continous reading to the book reader. Clicking on the max pages to right of progress bar will now go to last page. * Forgot a file for continous book reading * Fixed up some code regarding transitioning between chapters. Arrows now show to represent a chapter transition. * Laid the foundation for reading lists * All foundation is laid out. Actions are wired in the UI. Backend repository is setup. Redid the migration to have ReadingList track modification so we can order them for the user. * Updated add modal to have basic skeleton * Hooked up ability to fetch reading lists from backend * Made a huge performance improvement to GetChapterIdsForSeriesAsync() by reducing a JOIN and an iteration loop. Improvement went from 2 seconds -> 200 ms. * Implemented the ability to add all chapters in a series to a reading list. * Fixed issue with adding new items to reading list not being in a logical order. Lots of work on getting all the information around the reading list view. Added some foreign keys back to chapter so delete should clean up after itself. * Added ability to open directly the series * Reading List Items now have progress attached * Hooked up list deletion and added a case where if doesn't exist on load, then redirect to library. * Lots of changes. Introduced a dashboard component for the main app. This will sit on libraries route for now and will have 3 tabs to show different sections. Moved libraries reel down to bottom as people are more likely to access recently added or in progress than explore their whole library. Note: Bundles are messed up, they need to be reoptimized and routes need to be updated. * Added pagination to the reading lists api and implemented a page to show all lists * Cleaned up old code from all-collections component so now it only handles all collections and doesn't have the old code for an individual collection * Hooked in actions and navigation on reading lists * When the user re-arranges items, they are now persisted * Implemented remove read, but performance is pretty poor. Needs to be optimized. * Lots of API fixes for adding items to a series, returning items, etc. Committing before fixing incorrect fetches of items for a readingListId. * Rewrote the joins for GetReadingListItemDtosByIdAsync() to not return extra records. * Remove bug marker now that it is fixed * Refactor update-by-series to move more of the code to a re-usable function for update-by-volume/chapter APIs * Implemented the ability to add via series, volume or chapter. * Added OPDS support for reading lists. This included adding VolumeId to the ReadingListDto. * Fixed a bug with deleting items * After we create a library inform user that a scan has started * Added some extra help information for users on directory picker, since linux users were getting confused. * Setup for the reading functionality * Fixed an issue where opening the edit series modal and pressing save without doing anything would empty collection tags. Would happen often when editing cover images. * Fixed get-next-chapter for reading list. Refactored all methods to use the new GetUserIdByUsernameAsync(), which is much faster and uses less memory. * Hooked in prev chapter for continuous reading with reading list * Hooked up the read code for manga reader and book reader to have list id passed * Manga reader now functions completely with reading lists * Implemented reading list and incognito mode into book reader * Refactored some common reading code into reader service * Added support for "Series - - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz" format that can occur with FMD2. * Implemented continuous reading with a reading list between different readers. This incurs a 3x performance hit on the book info api. * style changes. Don't emit an event if position of draggable item hasn't changed * Styling and added the edit reading list flow. * Cleaned up some extra spaces when actionables isn't shown. Lots of cleanup for promoted lists. * Refactored some filter code to a common service * Added an RBS check in getting Items for a given user. * Code smells * More smells
405 lines
16 KiB
C#
405 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Comparators;
|
|
using API.DTOs.ReadingLists;
|
|
using API.Entities;
|
|
using API.Extensions;
|
|
using API.Helpers;
|
|
using API.Interfaces;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Controllers
|
|
{
|
|
public class ReadingListController : BaseApiController
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
|
|
|
public ReadingListController(IUnitOfWork unitOfWork)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
|
|
{
|
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns reading lists (paginated) for a given user.
|
|
/// </summary>
|
|
/// <param name="includePromoted">Defaults to true</param>
|
|
/// <returns></returns>
|
|
[HttpPost("lists")]
|
|
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
|
|
{
|
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
|
userParams);
|
|
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
|
|
|
|
return Ok(items);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
|
|
/// </summary>
|
|
/// <remarks>This call is expensive</remarks>
|
|
/// <param name="readingListId"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("items")]
|
|
public async Task<ActionResult<IEnumerable<ReadingListItemDto>>> GetListForUser(int readingListId)
|
|
{
|
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
|
|
|
return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an items position
|
|
/// </summary>
|
|
/// <param name="dto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("update-position")]
|
|
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
|
|
{
|
|
// Make sure UI buffers events
|
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
|
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
|
items.Remove(item);
|
|
items.Insert(dto.ToPosition, item);
|
|
|
|
for (var i = 0; i < items.Count; i++)
|
|
{
|
|
items[i].Order = i;
|
|
}
|
|
|
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok("Updated");
|
|
}
|
|
|
|
return BadRequest("Couldn't update position");
|
|
}
|
|
|
|
[HttpPost("delete-item")]
|
|
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
|
|
{
|
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
|
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
|
items.Remove(item);
|
|
|
|
for (var i = 0; i < items.Count; i++)
|
|
{
|
|
items[i].Order = i;
|
|
}
|
|
|
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok("Updated");
|
|
}
|
|
|
|
return BadRequest("Couldn't delete item");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all entries that are fully read from the reading list
|
|
/// </summary>
|
|
/// <param name="readingListId"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("remove-read")]
|
|
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
|
|
{
|
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
|
items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList());
|
|
|
|
// Collect all Ids to remove
|
|
var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
|
|
|
|
try
|
|
{
|
|
var listItems =
|
|
(await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r =>
|
|
itemIdsToRemove.Contains(r.Id));
|
|
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
|
|
|
|
if (_unitOfWork.HasChanges())
|
|
{
|
|
await _unitOfWork.CommitAsync();
|
|
return Ok("Updated");
|
|
}
|
|
else
|
|
{
|
|
return Ok("Nothing to remove");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
await _unitOfWork.RollbackAsync();
|
|
}
|
|
|
|
return BadRequest("Could not remove read items");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a reading list
|
|
/// </summary>
|
|
/// <param name="readingListId"></param>
|
|
/// <returns></returns>
|
|
[HttpDelete]
|
|
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
|
var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId);
|
|
if (readingList == null)
|
|
{
|
|
return BadRequest("User is not associated with this reading list");
|
|
}
|
|
|
|
user.ReadingLists.Remove(readingList);
|
|
|
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok("Deleted");
|
|
}
|
|
|
|
return BadRequest("There was an issue deleting reading list");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new List with a unique title. Returns the new ReadingList back
|
|
/// </summary>
|
|
/// <param name="dto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("create")]
|
|
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
|
|
|
// When creating, we need to make sure Title is unique
|
|
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
|
|
if (hasExisting)
|
|
{
|
|
return BadRequest("A list of this name already exists");
|
|
}
|
|
user.ReadingLists.Add(new ReadingList()
|
|
{
|
|
Promoted = false,
|
|
Title = dto.Title,
|
|
Summary = string.Empty
|
|
});
|
|
|
|
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
|
|
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title));
|
|
}
|
|
|
|
[HttpPost("update")]
|
|
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
|
|
{
|
|
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
|
if (readingList == null) return BadRequest("List does not exist");
|
|
|
|
if (!string.IsNullOrEmpty(dto.Title))
|
|
{
|
|
readingList.Title = dto.Title; // Should I check if this is unique?
|
|
}
|
|
if (!string.IsNullOrEmpty(dto.Title))
|
|
{
|
|
readingList.Summary = dto.Summary;
|
|
}
|
|
|
|
readingList.Promoted = dto.Promoted;
|
|
|
|
_unitOfWork.ReadingListRepository.Update(readingList);
|
|
|
|
if (await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok("Updated");
|
|
}
|
|
return BadRequest("Could not update reading list");
|
|
}
|
|
|
|
[HttpPost("update-by-series")]
|
|
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto 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 chapterIdsForSeries =
|
|
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
|
|
|
// If there are adds, tell tracking this has been modified
|
|
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, 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")]
|
|
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto 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 chapterIdsForVolume =
|
|
(await _unitOfWork.VolumeRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
|
|
|
|
// If there are adds, tell tracking this has been modified
|
|
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, 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-chapter")]
|
|
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto 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");
|
|
|
|
// If there are adds, tell tracking this has been modified
|
|
if (await AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, 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 a list of Chapters as reading list items to the passed reading list.
|
|
/// </summary>
|
|
/// <param name="seriesId"></param>
|
|
/// <param name="chapterIds"></param>
|
|
/// <param name="readingList"></param>
|
|
/// <returns>True if new chapters were added</returns>
|
|
private async Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
|
|
ReadingList readingList)
|
|
{
|
|
readingList.Items ??= new List<ReadingListItem>();
|
|
var lastOrder = 0;
|
|
if (readingList.Items.Any())
|
|
{
|
|
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
|
|
}
|
|
|
|
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
|
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
|
.OrderBy(c => int.Parse(c.Volume.Name))
|
|
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
|
|
|
var index = lastOrder + 1;
|
|
foreach (var chapter in chaptersForSeries)
|
|
{
|
|
if (existingChapterExists.Contains(chapter.Id)) continue;
|
|
|
|
readingList.Items.Add(new ReadingListItem()
|
|
{
|
|
Order = index,
|
|
ChapterId = chapter.Id,
|
|
SeriesId = seriesId,
|
|
VolumeId = chapter.VolumeId
|
|
});
|
|
index += 1;
|
|
}
|
|
|
|
return index > lastOrder + 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the next chapter within the reading list
|
|
/// </summary>
|
|
/// <param name="currentChapterId"></param>
|
|
/// <param name="readingListId"></param>
|
|
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
|
[HttpGet("next-chapter")]
|
|
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
|
|
{
|
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
|
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
|
if (readingListItem == null) return BadRequest("Id does not exist");
|
|
var index = items.IndexOf(readingListItem) + 1;
|
|
if (items.Count > index)
|
|
{
|
|
return items[index].ChapterId;
|
|
}
|
|
|
|
return Ok(-1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the prev chapter within the reading list
|
|
/// </summary>
|
|
/// <param name="currentChapterId"></param>
|
|
/// <param name="readingListId"></param>
|
|
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
|
[HttpGet("prev-chapter")]
|
|
public async Task<ActionResult<int>> GetPrevChapter(int currentChapterId, int readingListId)
|
|
{
|
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
|
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
|
if (readingListItem == null) return BadRequest("Id does not exist");
|
|
var index = items.IndexOf(readingListItem) - 1;
|
|
if (0 <= index)
|
|
{
|
|
return items[index].ChapterId;
|
|
}
|
|
|
|
return Ok(-1);
|
|
}
|
|
}
|
|
}
|