Feature/bookmark feedback (#508)

* ImageService had a stream reset before writting out to array. Added logging statment for updating series metadata. Removed ConcurencyCheck due to bad update issue for CollectionTag.

* Added a new screen which lets you quickly see all your bookmarks for a given user.

* Built user bookmark page in user settings. Moved user settings to it's own lazy loaded module. Removed unneded debouncing from downloader and just used throttleTime instead.

* Removed a not-yet implemented tab from series modal

* Fixed a bug in clear bookmarks and adjusted icons within anchors to have proper styling
This commit is contained in:
Joseph Milazzo 2021-08-18 17:16:05 -07:00 committed by GitHub
parent 623e555633
commit 68bb5ed5a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 554 additions and 270 deletions

View file

@ -11,6 +11,7 @@ using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
{
@ -22,16 +23,18 @@ namespace API.Controllers
private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReaderController> _logger;
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
/// <inheritdoc />
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork, ILogger<ReaderController> logger)
{
_directoryService = directoryService;
_cacheService = cacheService;
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <summary>
@ -350,19 +353,31 @@ namespace API.Controllers
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
}
/// <summary>
/// Returns a list of all bookmarked pages for a User
/// </summary>
/// <returns></returns>
[HttpGet("get-all-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
}
/// <summary>
/// Removes all bookmarks for all chapters linked to a Series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("remove-bookmarks")]
public async Task<ActionResult> RemoveBookmarks(int seriesId)
public async Task<ActionResult> RemoveBookmarks(RemoveBookmarkForSeriesDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user.Bookmarks == null) return Ok("Nothing to remove");
try
{
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId == seriesId).ToList();
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList();
_unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.CommitAsync())
@ -370,8 +385,9 @@ namespace API.Controllers
return Ok();
}
}
catch (Exception)
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when trying to clear bookmarks");
await _unitOfWork.RollbackAsync();
}

View file

@ -12,6 +12,7 @@ using API.Interfaces;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Controllers
@ -154,8 +155,8 @@ namespace API.Controllers
}
series.Name = updateSeries.Name.Trim();
series.LocalizedName = updateSeries.LocalizedName.Trim();
series.SortName = updateSeries.SortName.Trim();
series.Summary = updateSeries.Summary.Trim();
series.SortName = updateSeries.SortName?.Trim();
series.Summary = updateSeries.Summary?.Trim(); // BUG: There was an exceptionSystem.NullReferenceException: Object reference not set to an instance of an object.
var needsRefreshMetadata = false;
if (!updateSeries.CoverImageLocked)
@ -296,8 +297,9 @@ namespace API.Controllers
return Ok("Successfully updated");
}
}
catch (Exception)
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when updating metadata");
await _unitOfWork.RollbackAsync();
}
@ -327,6 +329,19 @@ namespace API.Controllers
return Ok(series);
}
/// <summary>
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
/// the user does not have access to.
/// </summary>
/// <returns></returns>
[HttpPost("series-by-ids")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, user.Id));
}
}
}

View file

@ -0,0 +1,7 @@
namespace API.DTOs
{
public class RemoveBookmarkForSeriesDto
{
public int SeriesId { get; init; }
}
}

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs
{
public class SeriesByIdsDto
{
public int[] SeriesIds { get; init; }
}
}

View file

@ -52,7 +52,7 @@ namespace API.Data
IsSpecial = specialTreatment,
};
}
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
{
return new SeriesMetadata()
@ -66,11 +66,11 @@ namespace API.Data
return new CollectionTag()
{
Id = id,
NormalizedTitle = API.Parser.Parser.Normalize(title).ToUpper(),
Title = title,
Summary = summary,
NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted
};
}
}
}
}

View file

@ -443,5 +443,21 @@ namespace API.Data
.AsNoTracking()
.ToListAsync();
}
public async Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId)
{
var allowedLibraries = _context.Library
.Include(l => l.AppUsers)
.Where(library => library.AppUsers.Any(x => x.Id == userId))
.Select(l => l.Id);
return await _context.Series
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
.OrderBy(s => s.SortName)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery()
.ToListAsync();
}
}
}

View file

@ -106,6 +106,17 @@ namespace API.Data
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{
return await _context.Users

View file

@ -9,7 +9,7 @@ namespace API.Entities
/// Represents a user entered field that is used as a tagging and grouping mechanism
/// </summary>
[Index(nameof(Id), nameof(Promoted), IsUnique = true)]
public class CollectionTag : IHasConcurrencyToken
public class CollectionTag
{
public int Id { get; set; }
/// <summary>
@ -42,12 +42,14 @@ namespace API.Entities
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
/// <summary>
/// Not Used due to not using concurrency update
/// </summary>
public uint RowVersion { get; private set; }
/// <inheritdoc />
/// <summary>
/// Not Used due to not using concurrency update
/// </summary>
public void OnSavingChanges()
{
RowVersion++;

View file

@ -63,5 +63,6 @@ namespace API.Interfaces
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId);
}
}

View file

@ -19,5 +19,6 @@ namespace API.Interfaces
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId);
}
}

View file

@ -57,6 +57,7 @@ namespace API.Services
using var img = Image.NewFromFile(path);
using var stream = new MemoryStream();
img.JpegsaveStream(stream);
stream.Position = 0;
return stream.ToArray();
}
catch (Exception ex)