Reading List Polish (#1879)
* Use Reading Order to count epub pages rather than raw HTML files. * Send email on background thread for initial invite flow. * Reorder default writing style for new users so Horizontal is default * Changed reading activity to use average hours read rather than events to bring more meaningful data. * added ability to start reading incognito from the top of series detail, needs a bit of styling help though. * Refactored extensions out into their own package, added new fields for reading list to cover total run, cbl import now takes those dates and overrides on import. Replaced many instances of numbers to be comma separated. * Added ability to edit reading list run start and end year/month. Refactored some code for valid month/year into a helper method. * Added a way to see the reading list's release years. * Added some merged image code, but had to remove due to cover dimensions not fixed. * tweaked style for accessibility mode on reading list items * Tweaked css for non virtualized and virtualized containers * Fixed release updates failing * Commented out the merge code. * Typo on words read per year * Fixed unit tests * Fixed virtualized scroll * Cleanup CSS
This commit is contained in:
parent
266f302823
commit
fd6ee42f5f
60 changed files with 2847 additions and 430 deletions
|
@ -16,6 +16,7 @@ using API.Extensions;
|
|||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -605,19 +606,14 @@ public class AccountController : BaseApiController
|
|||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
try
|
||||
// Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails
|
||||
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
await _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName!,
|
||||
ServerConfirmationLink = emailLink
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception */
|
||||
}
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName!,
|
||||
ServerConfirmationLink = emailLink
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
return Ok(new InviteUserResponse
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
|
@ -118,7 +119,10 @@ public class ImageController : BaseApiController
|
|||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
return BadRequest($"No cover image");
|
||||
}
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
|
|
@ -33,28 +33,28 @@ public class CblReadingList
|
|||
/// </summary>
|
||||
/// <remarks>This is not a standard, adding based on discussion with CBL Maintainers</remarks>
|
||||
[XmlElement(ElementName="StartYear")]
|
||||
public int StartYear { get; set; }
|
||||
public int StartYear { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Start Year of the Reading List. Overrides calculation
|
||||
/// </summary>
|
||||
/// <remarks>This is not a standard, adding based on discussion with CBL Maintainers</remarks>
|
||||
[XmlElement(ElementName="StartMonth")]
|
||||
public int StartMonth { get; set; }
|
||||
[XmlElement(ElementName = "StartMonth")]
|
||||
public int StartMonth { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// End Year of the Reading List. Overrides calculation
|
||||
/// </summary>
|
||||
/// <remarks>This is not a standard, adding based on discussion with CBL Maintainers</remarks>
|
||||
[XmlElement(ElementName="EndYear")]
|
||||
public int EndYear { get; set; }
|
||||
public int EndYear { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// End Year of the Reading List. Overrides calculation
|
||||
/// </summary>
|
||||
/// <remarks>This is not a standard, adding based on discussion with CBL Maintainers</remarks>
|
||||
[XmlElement(ElementName="EndMonth")]
|
||||
public int EndMonth { get; set; }
|
||||
public int EndMonth { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Issues of the Reading List
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
namespace API.DTOs.ReadingLists;
|
||||
using System;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
|
||||
public class ReadingListDto
|
||||
{
|
||||
|
@ -14,5 +16,21 @@ public class ReadingListDto
|
|||
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
|
||||
/// </summary>
|
||||
public string CoverImage { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Minimum Year the Reading List starts
|
||||
/// </summary>
|
||||
public int StartingYear { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum Month the Reading List starts
|
||||
/// </summary>
|
||||
public int StartingMonth { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum Year the Reading List starts
|
||||
/// </summary>
|
||||
public int EndingYear { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum Month the Reading List starts
|
||||
/// </summary>
|
||||
public int EndingMonth { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -10,4 +10,9 @@ public class UpdateReadingListDto
|
|||
public string Summary { get; set; } = string.Empty;
|
||||
public bool Promoted { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
public int StartingMonth { get; set; } = 0;
|
||||
public int StartingYear { get; set; } = 0;
|
||||
public int EndingMonth { get; set; } = 0;
|
||||
public int EndingYear { get; set; } = 0;
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Entities.Metadata;
|
||||
|
@ -86,10 +87,12 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BackgroundColor)
|
||||
.HasDefaultValue("#000000");
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.GlobalPageLayoutMode)
|
||||
.HasDefaultValue(PageLayoutMode.Cards);
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BookReaderWritingStyle)
|
||||
.HasDefaultValue(WritingStyle.Horizontal);
|
||||
|
||||
|
||||
builder.Entity<Library>()
|
||||
|
|
1872
API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs
generated
Normal file
1872
API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
80
API/Data/Migrations/20230313125914_ReadingListDateRange.cs
Normal file
80
API/Data/Migrations/20230313125914_ReadingListDateRange.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReadingListDateRange : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EndingMonth",
|
||||
table: "ReadingList",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EndingYear",
|
||||
table: "ReadingList",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "StartingMonth",
|
||||
table: "ReadingList",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "StartingYear",
|
||||
table: "ReadingList",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "BookReaderWritingStyle",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EndingMonth",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EndingYear",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StartingMonth",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StartingYear",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "BookReaderWritingStyle",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldDefaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -225,7 +225,9 @@ namespace API.Data.Migrations
|
|||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderWritingStyle")
|
||||
.HasColumnType("INTEGER");
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("BookThemeName")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
@ -871,6 +873,12 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EndingMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EndingYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -883,6 +891,12 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StartingMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StartingYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.DTOs.Metadata;
|
|||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -6,6 +6,7 @@ using API.Data.Misc;
|
|||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -10,6 +10,7 @@ using API.DTOs.Metadata;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Kavita.Common.Extensions;
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
|
@ -6,6 +7,7 @@ using API.DTOs.ReadingLists;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
|
@ -15,10 +17,18 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
[Flags]
|
||||
public enum ReadingListIncludes
|
||||
{
|
||||
None = 1,
|
||||
Items = 2,
|
||||
ItemChapter = 4,
|
||||
}
|
||||
|
||||
public interface IReadingListRepository
|
||||
{
|
||||
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams);
|
||||
Task<ReadingList?> GetReadingListByIdAsync(int readingListId);
|
||||
Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None);
|
||||
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
|
||||
Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId);
|
||||
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
|
||||
|
@ -34,9 +44,9 @@ public interface IReadingListRepository
|
|||
Task<string?> GetCoverImageAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> ReadingListExists(string name);
|
||||
Task<List<ReadingList>> GetAllReadingListsAsync();
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
||||
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
|
@ -88,15 +98,6 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<List<ReadingList>> GetAllReadingListsAsync()
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Include(r => r.Items.OrderBy(i => i.Order))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.Title)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId)
|
||||
{
|
||||
return _context.ReadingListItem
|
||||
|
@ -114,6 +115,23 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If less than 4 images exist, will return nothing back. Will not be full paths, but just cover image filenames
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public async Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId)
|
||||
{
|
||||
return await _context.ReadingListItem
|
||||
.Where(ri => ri.ReadingListId == readingListId)
|
||||
.Include(ri => ri.Chapter)
|
||||
.Where(ri => ri.Chapter.CoverImage != null)
|
||||
.Select(ri => ri.Chapter.CoverImage)
|
||||
.Take(4)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
@ -151,10 +169,11 @@ public class ReadingListRepository : IReadingListRepository
|
|||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ReadingList?> GetReadingListByIdAsync(int readingListId)
|
||||
public async Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Id == readingListId)
|
||||
.Includes(includes)
|
||||
.Include(r => r.Items.OrderBy(item => item.Order))
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
|
|
|
@ -17,6 +17,7 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
|
|
@ -7,14 +7,14 @@ namespace API.Entities.Enums;
|
|||
/// </summary>
|
||||
public enum WritingStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// Vertical writing style for the book-reader
|
||||
/// </summary>
|
||||
[Description ("Vertical")]
|
||||
Vertical = 0,
|
||||
/// <summary>
|
||||
/// Horizontal writing style for the book-reader
|
||||
/// </summary>
|
||||
[Description ("Horizontal")]
|
||||
Horizontal = 1
|
||||
Horizontal = 0,
|
||||
/// <summary>
|
||||
/// Vertical writing style for the book-reader
|
||||
/// </summary>
|
||||
[Description ("Vertical")]
|
||||
Vertical = 1
|
||||
}
|
||||
|
|
|
@ -39,14 +39,22 @@ public class ReadingList : IEntityDate
|
|||
public DateTime LastModified { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
// /// <summary>
|
||||
// /// Minimum Year and Month the Reading List starts
|
||||
// /// </summary>
|
||||
// public DateOnly StartingYear { get; set; }
|
||||
// /// <summary>
|
||||
// /// Maximum Year and Month the Reading List starts
|
||||
// /// </summary>
|
||||
// public DateOnly EndingYear { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum Year the Reading List starts
|
||||
/// </summary>
|
||||
public int StartingYear { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum Month the Reading List starts
|
||||
/// </summary>
|
||||
public int StartingMonth { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum Year the Reading List starts
|
||||
/// </summary>
|
||||
public int EndingYear { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum Month the Reading List starts
|
||||
/// </summary>
|
||||
public int EndingMonth { get; set; }
|
||||
|
||||
// Relationships
|
||||
public int AppUserId { get; set; }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using API.Parser;
|
||||
|
||||
namespace API.Extensions;
|
||||
|
@ -39,6 +40,6 @@ public static class ChapterListExtensions
|
|||
/// <returns></returns>
|
||||
public static int MinimumReleaseYear(this IList<Chapter> chapters)
|
||||
{
|
||||
return chapters.Select(v => v.ReleaseDate.Year).Where(y => y >= 1000).DefaultIfEmpty().Min();
|
||||
return chapters.Select(v => v.ReleaseDate.Year).Where(y => NumberHelper.IsValidYear(y)).DefaultIfEmpty().Min();
|
||||
}
|
||||
}
|
||||
|
|
148
API/Extensions/QueryExtensions/IncludesExtensions.cs
Normal file
148
API/Extensions/QueryExtensions/IncludesExtensions.cs
Normal file
|
@ -0,0 +1,148 @@
|
|||
using System.Linq;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Extensions.QueryExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern
|
||||
/// </summary>
|
||||
public static class IncludesExtensions
|
||||
{
|
||||
public static IQueryable<CollectionTag> Includes(this IQueryable<CollectionTag> queryable,
|
||||
CollectionTagIncludes includes)
|
||||
{
|
||||
if (includes.HasFlag(CollectionTagIncludes.SeriesMetadata))
|
||||
{
|
||||
queryable = queryable.Include(c => c.SeriesMetadatas);
|
||||
}
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<Chapter> Includes(this IQueryable<Chapter> queryable,
|
||||
ChapterIncludes includes)
|
||||
{
|
||||
if (includes.HasFlag(ChapterIncludes.Volumes))
|
||||
{
|
||||
queryable = queryable.Include(v => v.Volume);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(ChapterIncludes.Files))
|
||||
{
|
||||
queryable = queryable
|
||||
.Include(c => c.Files);
|
||||
}
|
||||
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<Series> Includes(this IQueryable<Series> query,
|
||||
SeriesIncludes includeFlags)
|
||||
{
|
||||
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.Chapters))
|
||||
{
|
||||
query = query
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters);
|
||||
}
|
||||
|
||||
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.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags.OrderBy(g => g.NormalizedTitle));
|
||||
}
|
||||
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<AppUser> Includes(this IQueryable<AppUser> query, AppUserIncludes includeFlags)
|
||||
{
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
|
||||
{
|
||||
query = query.Include(u => u.Bookmarks);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Progress))
|
||||
{
|
||||
query = query.Include(u => u.Progresses);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists)
|
||||
.ThenInclude(r => r.Items);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
|
||||
{
|
||||
query = query.Include(u => u.Ratings);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
|
||||
{
|
||||
query = query.Include(u => u.UserPreferences);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))
|
||||
{
|
||||
query = query.Include(u => u.WantToRead);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Devices))
|
||||
{
|
||||
query = query.Include(u => u.Devices);
|
||||
}
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<ReadingList> Includes(this IQueryable<ReadingList> queryable,
|
||||
ReadingListIncludes includes)
|
||||
{
|
||||
if (includes.HasFlag(ReadingListIncludes.Items))
|
||||
{
|
||||
queryable = queryable.Include(r => r.Items.OrderBy(item => item.Order));
|
||||
}
|
||||
|
||||
if (includes.HasFlag(ReadingListIncludes.ItemChapter))
|
||||
{
|
||||
queryable = queryable
|
||||
.Include(r => r.Items.OrderBy(item => item.Order))
|
||||
.ThenInclude(ri => ri.Chapter);
|
||||
}
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
}
|
92
API/Extensions/QueryExtensions/QueryableExtensions.cs
Normal file
92
API/Extensions/QueryExtensions/QueryableExtensions.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Extensions.QueryExtensions;
|
||||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static Task<AgeRestriction> GetUserAgeRestriction(this DbSet<AppUser> queryable, int userId)
|
||||
{
|
||||
if (userId < 1)
|
||||
{
|
||||
return Task.FromResult(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.NotApplicable,
|
||||
IncludeUnknowns = true
|
||||
});
|
||||
}
|
||||
return queryable
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u =>
|
||||
new AgeRestriction(){
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
})
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Applies restriction based on if the Library has restrictions (like include in search)
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<Library> IsRestricted(this IQueryable<Library> query, QueryContext context)
|
||||
{
|
||||
if (context.HasFlag(QueryContext.None)) return query;
|
||||
|
||||
if (context.HasFlag(QueryContext.Dashboard))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInDashboard);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Recommended))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInRecommended);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Search))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInSearch);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all libraries for a given user
|
||||
/// </summary>
|
||||
/// <param name="library"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="queryContext"></param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<int> GetUserLibraries(this IQueryable<Library> library, int userId, QueryContext queryContext = QueryContext.None)
|
||||
{
|
||||
return library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(lib => lib.AppUsers.Any(user => user.Id == userId))
|
||||
.IsRestricted(queryContext)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Select(lib => lib.Id);
|
||||
}
|
||||
|
||||
public static IEnumerable<DateTime> Range(this DateTime startDate, int numberOfDays) =>
|
||||
Enumerable.Range(0, numberOfDays).Select(e => startDate.AddDays(e));
|
||||
|
||||
public static IQueryable<T> WhereIf<T>(this IQueryable<T> queryable, bool condition,
|
||||
Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return condition ? queryable.Where(predicate) : queryable;
|
||||
}
|
||||
}
|
96
API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs
Normal file
96
API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using System.Linq;
|
||||
using API.Data.Misc;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Extensions.QueryExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for restricting Entities based on an AgeRestriction
|
||||
/// </summary>
|
||||
public static class RestrictByAgeExtensions
|
||||
{
|
||||
public static IQueryable<Series> RestrictAgainstAgeRestriction(this IQueryable<Series> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(s => s.Metadata.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.Metadata.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
//q.WhereIf(!restriction.IncludeUnknowns, s => s.Metadata.AgeRating != AgeRating.Unknown);
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
public static IQueryable<CollectionTag> RestrictAgainstAgeRestriction(this IQueryable<CollectionTag> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Genre> RestrictAgainstAgeRestriction(this IQueryable<Genre> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Tag> RestrictAgainstAgeRestriction(this IQueryable<Tag> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Person> RestrictAgainstAgeRestriction(this IQueryable<Person> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(rl => rl.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(rl => rl.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
|
@ -1,290 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IQueryable<Series> RestrictAgainstAgeRestriction(this IQueryable<Series> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(s => s.Metadata.AgeRating <= restriction.AgeRating);
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.Metadata.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
public static IQueryable<CollectionTag> RestrictAgainstAgeRestriction(this IQueryable<CollectionTag> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Genre> RestrictAgainstAgeRestriction(this IQueryable<Genre> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Tag> RestrictAgainstAgeRestriction(this IQueryable<Tag> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<Person> RestrictAgainstAgeRestriction(this IQueryable<Person> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
|
||||
if (restriction.IncludeUnknowns)
|
||||
{
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating));
|
||||
}
|
||||
|
||||
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
|
||||
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
|
||||
}
|
||||
|
||||
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(rl => rl.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(rl => rl.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
public static Task<AgeRestriction> GetUserAgeRestriction(this DbSet<AppUser> queryable, int userId)
|
||||
{
|
||||
if (userId < 1)
|
||||
{
|
||||
return Task.FromResult(new AgeRestriction()
|
||||
{
|
||||
AgeRating = AgeRating.NotApplicable,
|
||||
IncludeUnknowns = true
|
||||
});
|
||||
}
|
||||
return queryable
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u =>
|
||||
new AgeRestriction(){
|
||||
AgeRating = u.AgeRestriction,
|
||||
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
|
||||
})
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
public static IQueryable<CollectionTag> Includes(this IQueryable<CollectionTag> queryable,
|
||||
CollectionTagIncludes includes)
|
||||
{
|
||||
if (includes.HasFlag(CollectionTagIncludes.SeriesMetadata))
|
||||
{
|
||||
queryable = queryable.Include(c => c.SeriesMetadatas);
|
||||
}
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<Chapter> Includes(this IQueryable<Chapter> queryable,
|
||||
ChapterIncludes includes)
|
||||
{
|
||||
if (includes.HasFlag(ChapterIncludes.Volumes))
|
||||
{
|
||||
queryable = queryable.Include(v => v.Volume);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(ChapterIncludes.Files))
|
||||
{
|
||||
queryable = queryable
|
||||
.Include(c => c.Files);
|
||||
}
|
||||
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<Series> Includes(this IQueryable<Series> query,
|
||||
SeriesIncludes includeFlags)
|
||||
{
|
||||
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.Chapters))
|
||||
{
|
||||
query = query
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters);
|
||||
}
|
||||
|
||||
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.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags.OrderBy(g => g.NormalizedTitle));
|
||||
}
|
||||
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<AppUser> Includes(this IQueryable<AppUser> query, AppUserIncludes includeFlags)
|
||||
{
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
|
||||
{
|
||||
query = query.Include(u => u.Bookmarks);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Progress))
|
||||
{
|
||||
query = query.Include(u => u.Progresses);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists)
|
||||
.ThenInclude(r => r.Items);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
|
||||
{
|
||||
query = query.Include(u => u.Ratings);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
|
||||
{
|
||||
query = query.Include(u => u.UserPreferences);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))
|
||||
{
|
||||
query = query.Include(u => u.WantToRead);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Devices))
|
||||
{
|
||||
query = query.Include(u => u.Devices);
|
||||
}
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies restriction based on if the Library has restrictions (like include in search)
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<Library> IsRestricted(this IQueryable<Library> query, QueryContext context)
|
||||
{
|
||||
if (context.HasFlag(QueryContext.None)) return query;
|
||||
|
||||
if (context.HasFlag(QueryContext.Dashboard))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInDashboard);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Recommended))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInRecommended);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Search))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInSearch);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all libraries for a given user
|
||||
/// </summary>
|
||||
/// <param name="library"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="queryContext"></param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<int> GetUserLibraries(this IQueryable<Library> library, int userId, QueryContext queryContext = QueryContext.None)
|
||||
{
|
||||
return library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(lib => lib.AppUsers.Any(user => user.Id == userId))
|
||||
.IsRestricted(queryContext)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Select(lib => lib.Id);
|
||||
}
|
||||
|
||||
public static IEnumerable<DateTime> Range(this DateTime startDate, int numberOfDays) =>
|
||||
Enumerable.Range(0, numberOfDays).Select(e => startDate.AddDays(e));
|
||||
|
||||
public static IQueryable<T> WhereIf<T>(this IQueryable<T> queryable, bool condition,
|
||||
Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return condition ? queryable.Where(predicate) : queryable;
|
||||
}
|
||||
}
|
7
API/Helpers/NumberHelper.cs
Normal file
7
API/Helpers/NumberHelper.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace API.Helpers;
|
||||
|
||||
public static class NumberHelper
|
||||
{
|
||||
public static bool IsValidMonth(int number) => number is > 0 and <= 12;
|
||||
public static bool IsValidYear(int number) => number is >= 1000;
|
||||
}
|
|
@ -508,7 +508,7 @@ public class BookService : IBookService
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static (int year, int month, int day) GetPublicationDate(string publicationDate)
|
||||
{
|
||||
var dateParsed = DateTime.TryParse(publicationDate, out var date);
|
||||
|
@ -571,7 +571,7 @@ public class BookService : IBookService
|
|||
}
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||
return epubBook.Content.Html.Count;
|
||||
return epubBook.GetReadingOrder().Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -218,4 +219,23 @@ public class ImageService : IImageService
|
|||
}
|
||||
|
||||
|
||||
public static string CreateMergedImage(List<string> coverImages, string dest)
|
||||
{
|
||||
// TODO: Needs testing
|
||||
// Currently this doesn't work due to non-standard cover image sizes and dimensions
|
||||
var image = Image.Black(320*4, 160*4);
|
||||
|
||||
for (var i = 0; i < coverImages.Count; i++)
|
||||
{
|
||||
var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential);
|
||||
|
||||
var x = (i % 2) * (image.Width / 2);
|
||||
var y = (i / 2) * (image.Height / 2);
|
||||
|
||||
image = image.Insert(tile, x, y);
|
||||
}
|
||||
|
||||
image.WriteToFile(dest);
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -10,6 +11,7 @@ using API.DTOs.ReadingLists;
|
|||
using API.DTOs.ReadingLists.CBL;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -31,6 +33,8 @@ public interface IReadingListService
|
|||
|
||||
Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading);
|
||||
Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false);
|
||||
Task CalculateStartAndEndDates(ReadingList readingListWithItems);
|
||||
Task<string> GenerateMergedImage(int readingListId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -142,6 +146,25 @@ public class ReadingListService : IReadingListService
|
|||
readingList.Promoted = dto.Promoted;
|
||||
readingList.CoverImageLocked = dto.CoverImageLocked;
|
||||
|
||||
|
||||
if (NumberHelper.IsValidMonth(dto.StartingMonth))
|
||||
{
|
||||
readingList.StartingMonth = dto.StartingMonth;
|
||||
}
|
||||
if (NumberHelper.IsValidYear(dto.StartingYear))
|
||||
{
|
||||
readingList.StartingYear = dto.StartingYear;
|
||||
}
|
||||
if (NumberHelper.IsValidMonth(dto.EndingMonth))
|
||||
{
|
||||
readingList.EndingMonth = dto.EndingMonth;
|
||||
}
|
||||
if (NumberHelper.IsValidYear(dto.EndingYear))
|
||||
{
|
||||
readingList.EndingYear = dto.EndingYear;
|
||||
}
|
||||
|
||||
|
||||
if (!dto.CoverImageLocked)
|
||||
{
|
||||
readingList.CoverImageLocked = false;
|
||||
|
@ -182,6 +205,7 @@ public class ReadingListService : IReadingListService
|
|||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
|
||||
if (readingList == null) return true;
|
||||
await CalculateReadingListAgeRating(readingList);
|
||||
await CalculateStartAndEndDates(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
|
@ -239,6 +263,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
|
||||
await CalculateReadingListAgeRating(readingList);
|
||||
await CalculateStartAndEndDates(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
|
@ -254,6 +279,52 @@ public class ReadingListService : IReadingListService
|
|||
await CalculateReadingListAgeRating(readingList, readingList.Items.Select(i => i.SeriesId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the Start month/year and Ending month/year
|
||||
/// </summary>
|
||||
/// <param name="readingListWithItems">Reading list should have all items</param>
|
||||
public async Task CalculateStartAndEndDates(ReadingList readingListWithItems)
|
||||
{
|
||||
var items = readingListWithItems.Items;
|
||||
if (readingListWithItems.Items.All(i => i.Chapter == null))
|
||||
{
|
||||
items =
|
||||
(await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListWithItems.Id, ReadingListIncludes.ItemChapter))?.Items;
|
||||
}
|
||||
if (items == null || items.Count == 0) return;
|
||||
|
||||
if (items.First().Chapter == null)
|
||||
{
|
||||
_logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities");
|
||||
return;
|
||||
}
|
||||
var maxReleaseDate = items.Max(item => item.Chapter.ReleaseDate);
|
||||
var minReleaseDate = items.Max(item => item.Chapter.ReleaseDate);
|
||||
if (maxReleaseDate != DateTime.MinValue)
|
||||
{
|
||||
readingListWithItems.EndingMonth = maxReleaseDate.Month;
|
||||
readingListWithItems.EndingYear = maxReleaseDate.Year;
|
||||
}
|
||||
if (minReleaseDate != DateTime.MinValue)
|
||||
{
|
||||
readingListWithItems.StartingMonth = minReleaseDate.Month;
|
||||
readingListWithItems.StartingYear = minReleaseDate.Year;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string?> GenerateMergedImage(int readingListId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// var coverImages = (await _unitOfWork.ReadingListRepository.GetFirstFourCoverImagesByReadingListId(readingListId)).ToList();
|
||||
// if (coverImages.Count < 4) return null;
|
||||
// var fullImages = coverImages
|
||||
// .Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList();
|
||||
//
|
||||
// var combinedFile = ImageService.CreateMergedImage(fullImages, _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, $"{readingListId}.png"));
|
||||
// // webp needs to be handled
|
||||
// return combinedFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the highest Age Rating from each Reading List Item
|
||||
/// </summary>
|
||||
|
@ -522,6 +593,14 @@ public class ReadingListService : IReadingListService
|
|||
if (dryRun) return importSummary;
|
||||
|
||||
await CalculateReadingListAgeRating(readingList);
|
||||
await CalculateStartAndEndDates(readingList);
|
||||
|
||||
// For CBL Import only we override pre-calculated dates
|
||||
if (NumberHelper.IsValidMonth(cblReading.StartMonth)) readingList.StartingMonth = cblReading.StartMonth;
|
||||
if (NumberHelper.IsValidYear(cblReading.StartYear)) readingList.StartingYear = cblReading.StartYear;
|
||||
if (NumberHelper.IsValidMonth(cblReading.EndMonth)) readingList.EndingMonth = cblReading.EndMonth;
|
||||
if (NumberHelper.IsValidYear(cblReading.EndYear)) readingList.EndingYear = cblReading.EndYear;
|
||||
|
||||
if (!string.IsNullOrEmpty(readingList.Summary?.Trim()))
|
||||
{
|
||||
readingList.Summary = readingList.Summary?.Trim();
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SeriesService : ISeriesService
|
|||
series.Metadata.AgeRatingLocked = true;
|
||||
}
|
||||
|
||||
if (updateSeriesMetadataDto.SeriesMetadata.ReleaseYear > 1000 && series.Metadata.ReleaseYear != updateSeriesMetadataDto.SeriesMetadata.ReleaseYear)
|
||||
if (NumberHelper.IsValidYear(updateSeriesMetadataDto.SeriesMetadata.ReleaseYear) && series.Metadata.ReleaseYear != updateSeriesMetadataDto.SeriesMetadata.ReleaseYear)
|
||||
{
|
||||
series.Metadata.ReleaseYear = updateSeriesMetadataDto.SeriesMetadata.ReleaseYear;
|
||||
series.Metadata.ReleaseYearLocked = true;
|
||||
|
|
|
@ -8,6 +8,7 @@ using API.DTOs.Statistics;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -340,29 +341,26 @@ public class StatisticService : IStatisticService
|
|||
.Join(_context.Volume, x => x.chapter.VolumeId, volume => volume.Id,
|
||||
(x, volume) => new {x.appUserProgresses, x.chapter, volume})
|
||||
.Join(_context.Series, x => x.appUserProgresses.SeriesId, series => series.Id,
|
||||
(x, series) => new {x.appUserProgresses, x.chapter, x.volume, series});
|
||||
(x, series) => new {x.appUserProgresses, x.chapter, x.volume, series})
|
||||
.WhereIf(userId > 0, x => x.appUserProgresses.AppUserId == userId)
|
||||
.WhereIf(days > 0, x => x.appUserProgresses.LastModified >= DateTime.Now.AddDays(days * -1));
|
||||
|
||||
if (userId > 0)
|
||||
{
|
||||
query = query.Where(x => x.appUserProgresses.AppUserId == userId);
|
||||
}
|
||||
|
||||
if (days > 0)
|
||||
{
|
||||
var date = DateTime.Now.AddDays(days * -1);
|
||||
query = query.Where(x => x.appUserProgresses.LastModified >= date);
|
||||
}
|
||||
// .Where(p => p.chapter.AvgHoursToRead > 0)
|
||||
// .SumAsync(p =>
|
||||
// p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages))))
|
||||
|
||||
var results = await query.GroupBy(x => new
|
||||
{
|
||||
Day = x.appUserProgresses.LastModified.Date,
|
||||
x.series.Format
|
||||
x.series.Format,
|
||||
})
|
||||
.Select(g => new PagesReadOnADayCount<DateTime>
|
||||
{
|
||||
Value = g.Key.Day,
|
||||
Format = g.Key.Format,
|
||||
Count = g.Count()
|
||||
Count = (long) g.Sum(x =>
|
||||
x.chapter.AvgHoursToRead * (x.appUserProgresses.PagesRead / (1.0f * x.chapter.Pages)))
|
||||
})
|
||||
.OrderBy(d => d.Value)
|
||||
.ToListAsync();
|
||||
|
|
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace API.Services.Tasks;
|
||||
|
||||
internal abstract class GithubReleaseMetadata
|
||||
internal class GithubReleaseMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Tag
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue