WebP Covers + Series Detail Enhancements (#1652)
* Implemented save covers as webp. Reworked screen to provide more information up front about webp and what browsers can support it. * cleaned up pages to use compact numbering and made compact numbering expand into one decimal place (20.5K) * Fixed an issue with adding new device * If a book has an invalid language set, drop the language altogether rather than reading in a corrupted entry. * Ensure genres and tags render alphabetically. Improved support for partial volumes in Comic parser. * Ensure all people, tags, collections, and genres are in alphabetical order. * Moved some code to Extensions to clean up code. * More unit tests * Cleaned up release year filter css * Tweaked some code in all series to make bulk deletes cleaner on the UI. * Trying out want to read and unread count on series detail page * Added Want to Read button for series page to make it easy to see when something is in want to read list and toggle it. Added tooltips instead of title to buttons, but they don't style correctly. Added a continue point under cover image. * Code smells
This commit is contained in:
parent
f907486c74
commit
e75b208d59
43 changed files with 481 additions and 175 deletions
|
@ -1,5 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
|
@ -12,6 +12,13 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
[Flags]
|
||||
public enum CollectionTagIncludes
|
||||
{
|
||||
None = 1,
|
||||
SeriesMetadata = 2,
|
||||
}
|
||||
|
||||
public interface ICollectionTagRepository
|
||||
{
|
||||
void Add(CollectionTag tag);
|
||||
|
@ -21,7 +28,7 @@ public interface ICollectionTagRepository
|
|||
Task<string> GetCoverImageAsync(int collectionTagId);
|
||||
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId);
|
||||
Task<CollectionTag> GetTagAsync(int tagId);
|
||||
Task<CollectionTag> GetFullTagAsync(int tagId);
|
||||
Task<CollectionTag> GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata);
|
||||
void Update(CollectionTag tag);
|
||||
Task<int> RemoveTagsWithoutSeries();
|
||||
Task<IEnumerable<CollectionTag>> GetAllTagsAsync();
|
||||
|
@ -76,6 +83,15 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageAsync(int collectionTagId)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Id == collectionTagId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
|
@ -114,11 +130,11 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<CollectionTag> GetFullTagAsync(int tagId)
|
||||
public async Task<CollectionTag> GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Id == tagId)
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
@ -143,19 +159,9 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.Title)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageAsync(int collectionTagId)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Id == collectionTagId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ public class GenreRepository : IGenreRepository
|
|||
.SelectMany(s => s.Metadata.Genres)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Title)
|
||||
.OrderBy(p => p.NormalizedTitle)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ public class GenreRepository : IGenreRepository
|
|||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.Genre
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.OrderBy(g => g.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
|
|
@ -101,6 +101,7 @@ public interface ISeriesRepository
|
|||
Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId);
|
||||
Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId);
|
||||
Task<PagedList<SeriesDto>> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter);
|
||||
Task<bool> IsSeriesInWantToRead(int userId, int seriesId);
|
||||
Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
|
@ -161,12 +162,10 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None)
|
||||
{
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId);
|
||||
|
||||
query = AddIncludesToQuery(query, includes);
|
||||
|
||||
return await query.OrderBy(s => s.SortName).ToListAsync();
|
||||
return await _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Includes(includes)
|
||||
.OrderBy(s => s.SortName).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -427,13 +426,10 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// <returns></returns>
|
||||
public async Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
|
||||
{
|
||||
var query = _context.Series
|
||||
return await _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.AsSplitQuery();
|
||||
|
||||
query = AddIncludesToQuery(query, includes);
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
.Includes(includes)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -833,8 +829,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
var metadataDto = await _context.SeriesMetadata
|
||||
.Where(metadata => metadata.SeriesId == seriesId)
|
||||
.Include(m => m.Genres)
|
||||
.Include(m => m.Tags)
|
||||
.Include(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.Tags.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.People)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesMetadataDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -848,6 +844,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId))
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Title)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -1147,11 +1144,10 @@ public class SeriesRepository : ISeriesRepository
|
|||
public async Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None)
|
||||
{
|
||||
var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder);
|
||||
var query = _context.Series.Where(s => s.FolderPath.Equals(normalized));
|
||||
|
||||
query = AddIncludesToQuery(query, includes);
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
return await _context.Series
|
||||
.Where(s => s.FolderPath.Equals(normalized))
|
||||
.Includes(includes)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1479,6 +1475,17 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await PagedList<SeriesDto>.CreateAsync(filteredQuery.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider), userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<bool> IsSeriesInWantToRead(int userId, int seriesId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
return await _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.AnyAsync(s => libraryIds.Contains(s.LibraryId) && s.Id == seriesId);
|
||||
}
|
||||
|
||||
public async Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId)
|
||||
{
|
||||
var info = await _context.Series
|
||||
|
@ -1528,40 +1535,4 @@ public class SeriesRepository : ISeriesRepository
|
|||
.OrderBy(s => s)
|
||||
.LastOrDefaultAsync();
|
||||
}
|
||||
|
||||
private static IQueryable<Series> AddIncludesToQuery(IQueryable<Series> query, SeriesIncludes includeFlags)
|
||||
{
|
||||
// TODO: Move this to an Extension Method
|
||||
if (includeFlags.HasFlag(SeriesIncludes.Library))
|
||||
{
|
||||
query = query.Include(u => u.Library);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(SeriesIncludes.Volumes))
|
||||
{
|
||||
query = query.Include(s => s.Volumes);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(SeriesIncludes.Related))
|
||||
{
|
||||
query = query.Include(s => s.Relations)
|
||||
.ThenInclude(r => r.TargetSeries)
|
||||
.Include(s => s.RelationOf);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(SeriesIncludes.Metadata))
|
||||
{
|
||||
query = query.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.CollectionTags)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags);
|
||||
}
|
||||
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ public class TagRepository : ITagRepository
|
|||
.SelectMany(s => s.Metadata.Tags)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(t => t.Title)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
@ -81,7 +81,7 @@ public class TagRepository : ITagRepository
|
|||
return await _context.Tag
|
||||
.AsNoTracking()
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(t => t.Title)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ public static class Seed
|
|||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue