Progress Overhaul + Profile Page and a LOT more! (#4262)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo
2025-12-09 10:00:11 -07:00
committed by GitHub
parent 4ac13f1f25
commit 9f29fa593d
645 changed files with 25585 additions and 4805 deletions
@@ -0,0 +1,13 @@
using System;
using System.Linq;
using API.Entities.User;
namespace API.Extensions.QueryExtensions;
public static class AuthKeyQueryExtensions
{
public static IQueryable<AppUserAuthKey> HasNotExpired(this IQueryable<AppUserAuthKey> queryable)
{
return queryable.Where(k => k.ExpiresAtUtc == null || k.ExpiresAtUtc > DateTime.UtcNow);
}
}
@@ -0,0 +1,68 @@
using System.Linq;
using API.DTOs.Statistics;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
using API.Entities.Progress;
using API.Entities.User;
namespace API.Extensions.QueryExtensions.Filtering;
public static class ActivityFilter
{
/// <summary>
/// Filter AppUserReadingSessionActivityData for the given filter, viewer, and owner
/// </summary>
/// <param name="queryable">source</param>
/// <param name="filter">stats filter from the UI</param>
/// <param name="userId">user id of the user <b>owing</b> the data</param>
/// <param name="socialPreferences">social preferences of the user <b>owing</b> the data</param>
/// <param name="requestingUser">the user <b>requesting</b> the data</param>
/// <param name="onlyCompleted">return only data for fully read chapters</param>
/// <param name="isAggregate">If this is aggregate data (counts, etc), the filter will opt-out of restricting based on Social Libraries/Age Rating</param>
/// <returns></returns>
public static IQueryable<AppUserReadingSessionActivityData> ApplyStatsFilter(
this IQueryable<AppUserReadingSessionActivityData> queryable,
StatsFilterDto filter,
int userId,
AppUserSocialPreferences socialPreferences,
AppUser requestingUser,
bool onlyCompleted = true,
bool isAggregate = false
)
{
var startTime = filter.StartDate?.ToUniversalTime();
var endTime = filter.EndDate?.ToUniversalTime();
var isOwnRequest = userId == requestingUser.Id;
var shouldLimitOnSocialLibraries = !isOwnRequest && socialPreferences.SocialLibraries.Count > 0;
var shouldLimitOnSocialAgeRating =
!isOwnRequest && socialPreferences.SocialMaxAgeRating != AgeRating.NotApplicable;
var shouldLimitOnAgeRating = !isOwnRequest && requestingUser.AgeRestriction != AgeRating.NotApplicable;
queryable = queryable
.Where(d => filter.Libraries.Contains(d.LibraryId) && d.ReadingSession.AppUserId == userId)
.WhereIf(onlyCompleted, d => d.EndPage >= d.Chapter.Pages)
.WhereIf(startTime != null, d => d.StartTime >= startTime)
.WhereIf(endTime != null, d => d.EndTime <= endTime);
if (isAggregate)
{
return queryable;
}
return queryable
.WhereIf(shouldLimitOnSocialLibraries, d => socialPreferences.SocialLibraries.Contains(d.LibraryId))
.WhereIf(shouldLimitOnSocialAgeRating, d =>
(socialPreferences.SocialMaxAgeRating >= d.Chapter.Volume.Series.Metadata.AgeRating && d.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown)
|| (socialPreferences.SocialIncludeUnknowns && d.Chapter.Volume.Series.Metadata.AgeRating == AgeRating.Unknown )
)
.WhereIf(shouldLimitOnAgeRating, d =>
(requestingUser.AgeRestriction >= d.Chapter.Volume.Series.Metadata.AgeRating && d.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown)
|| (requestingUser.AgeRestrictionIncludeUnknowns && d.Chapter.Volume.Series.Metadata.AgeRating == AgeRating.Unknown )
)
;
}
}
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Filtering.v2;
using API.DTOs.Reader;
using API.Entities;
using Kavita.Common;
using Microsoft.EntityFrameworkCore;
@@ -129,4 +127,61 @@ public static class AnnotationFilter
};
}
public static IQueryable<AppUserAnnotation> HasLikes(this IQueryable<AppUserAnnotation> queryable, bool condition,
FilterComparison comparison, int value)
{
if (!condition) return queryable;
return comparison switch
{
FilterComparison.Equal => queryable.Where(a => a.Likes.Count == value),
FilterComparison.NotEqual => queryable.Where(a => a.Likes.Count != value),
FilterComparison.GreaterThan => queryable.Where(a => a.Likes.Count > value),
FilterComparison.GreaterThanEqual => queryable.Where(a => a.Likes.Count >= value),
FilterComparison.LessThan => queryable.Where(a => a.Likes.Count < value),
FilterComparison.LessThanEqual => queryable.Where(a => a.Likes.Count <= value),
FilterComparison.BeginsWith or
FilterComparison.EndsWith or
FilterComparison.Matches or
FilterComparison.Contains or
FilterComparison.MustContains or
FilterComparison.NotContains or
FilterComparison.IsBefore or
FilterComparison.IsAfter or
FilterComparison.IsInLast or
FilterComparison.IsNotInLast or
FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"),
_ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null),
};
}
public static IQueryable<AppUserAnnotation> IsLikedBy(this IQueryable<AppUserAnnotation> queryable, bool condition,
FilterComparison comparison, IList<int> value)
{
if (value.Count == 0 || !condition) return queryable;
return comparison switch
{
FilterComparison.Equal => queryable.Where(a => a.Likes.Contains(value[0])),
FilterComparison.NotEqual => queryable.Where(a => a!.Likes.Contains(value[0])),
FilterComparison.Contains => queryable.Where(a => a.Likes.Any(value.Contains)),
FilterComparison.NotContains => queryable.Where(a => !a.Likes.Any(value.Contains)),
FilterComparison.GreaterThan or
FilterComparison.GreaterThanEqual or
FilterComparison.LessThan or
FilterComparison.LessThanEqual or
FilterComparison.BeginsWith or
FilterComparison.EndsWith or
FilterComparison.Matches or
FilterComparison.MustContains or
FilterComparison.IsBefore or
FilterComparison.IsAfter or
FilterComparison.IsInLast or
FilterComparison.IsNotInLast or
FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"),
_ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null),
};
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using API.Data.Misc;
using API.Data.Repositories;
@@ -271,6 +271,11 @@ public static class IncludesExtensions
query = query.Include(u => u.ChapterRatings);
}
if (includeFlags.HasFlag(AppUserIncludes.AuthKeys))
{
query = query.Include(u => u.AuthKeys);
}
return query.AsSplitQuery();
}
@@ -5,16 +5,13 @@ using System.Linq.Expressions;
using System.Threading.Tasks;
using API.Data.Misc;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Annotations;
using API.DTOs.Filtering;
using API.DTOs.KavitaPlus.Manage;
using API.DTOs.Metadata.Browse;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Person;
using API.Entities.Scrobble;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions.QueryExtensions;
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using API.Data.Misc;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Entities.Person;
using API.Entities.User;
namespace API.Extensions.QueryExtensions;
#nullable enable
@@ -153,7 +152,7 @@ public static class RestrictByAgeExtensions
return q;
}
private static IQueryable<AppUserRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserRating> queryable, AgeRestriction restriction, int userId)
public static IQueryable<AppUserRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserRating> queryable, AgeRestriction restriction, int userId)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId);
@@ -166,7 +165,7 @@ public static class RestrictByAgeExtensions
return q;
}
private static IQueryable<AppUserChapterRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserChapterRating> queryable, AgeRestriction restriction, int userId)
public static IQueryable<AppUserChapterRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserChapterRating> queryable, AgeRestriction restriction, int userId)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId);