Updated search logic. More UI updates.
This commit is contained in:
@@ -50,6 +50,10 @@ public static class LoggingExtensions
|
||||
.AddIfNotEmpty("Title", criteria.Title)
|
||||
.AddIfNotEmpty("Circle", criteria.Circle)
|
||||
.Add("Locale", criteria.Locale)
|
||||
.AddIfNotEmpty("SaleStatus", criteria.SaleStatus.ToString())
|
||||
.AddIfNotEmpty("CircleStatus", criteria.CircleStatus.ToString())
|
||||
.AddIfNotEmpty("TagStatus", criteria.TagStatus.ToString())
|
||||
.AddIfNotEmpty("CreatorStatus", criteria.CreatorStatus.ToString())
|
||||
.AddIfNotEmpty("AgeRatings", criteria.AgeRatings)
|
||||
.AddIfNotEmpty("Languages", criteria.SupportedLanguages)
|
||||
.AddIfNotEmpty("TagIds", criteria.TagIds, preview: 5)
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
public enum VoiceWorkSortField
|
||||
{
|
||||
ExpectedReleaseDate,
|
||||
ScheduledReleaseDate,
|
||||
ReleaseDate,
|
||||
AnyReleaseDate,
|
||||
Downloads,
|
||||
WishlistCount,
|
||||
SalesToWishlistRatio,
|
||||
StarRating,
|
||||
FavoriteCircle
|
||||
FavoriteCircle,
|
||||
ProductId
|
||||
}
|
||||
@@ -8,6 +8,8 @@ public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> :
|
||||
where TCriteria : new()
|
||||
where TSortField : struct, Enum
|
||||
{
|
||||
protected abstract bool UseSelectIdQuery { get; }
|
||||
|
||||
public async Task<SearchResult<TItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IQueryable<TBaseQuery> baseQuery = GetBaseQuery();
|
||||
@@ -16,12 +18,7 @@ public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> :
|
||||
int total = await filteredQuery.CountAsync(cancellationToken);
|
||||
|
||||
IOrderedQueryable<TBaseQuery> orderedQuery = ApplySorting(filteredQuery, options.SortOptions);
|
||||
IQueryable<TItem> selectQuery = GetSelectQuery(orderedQuery);
|
||||
|
||||
TItem[] items = await selectQuery
|
||||
.Skip((options.PageNumber - 1) * options.PageSize)
|
||||
.Take(options.PageSize)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
TItem[] items = await GetItemsAsync(options, orderedQuery, cancellationToken);
|
||||
|
||||
await PostLoadAsync(items, cancellationToken);
|
||||
|
||||
@@ -44,10 +41,10 @@ public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> :
|
||||
var (field, direction) = (sortOptions[i].Field, sortOptions[i].Direction);
|
||||
bool isDescending = direction == SortDirection.Descending;
|
||||
|
||||
IOrderedQueryable<TBaseQuery> applyFirst(Expression<Func<TBaseQuery, object>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
|
||||
IOrderedQueryable<TBaseQuery> applyNext(Expression<Func<TBaseQuery, object>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);
|
||||
IOrderedQueryable<TBaseQuery> applyFirst(Expression<Func<TBaseQuery, object?>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
|
||||
IOrderedQueryable<TBaseQuery> applyNext(Expression<Func<TBaseQuery, object?>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);
|
||||
|
||||
Expression<Func<TBaseQuery, object>> selector = GetSortExpression(field);
|
||||
Expression<Func<TBaseQuery, object?>> selector = GetSortExpression(field);
|
||||
|
||||
ordered = (i == 0) ? applyFirst(selector) : applyNext(selector);
|
||||
}
|
||||
@@ -83,8 +80,34 @@ public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> :
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Expression<Func<TBaseQuery, object>> GetSortExpression(TSortField field);
|
||||
private async Task<TItem[]> GetItemsAsync(SearchOptions<TCriteria, TSortField> options, IOrderedQueryable<TBaseQuery> orderedQuery, CancellationToken cancellationToken)
|
||||
{
|
||||
if (UseSelectIdQuery)
|
||||
{
|
||||
int[] ids = await GetSelectIdQuery(orderedQuery)
|
||||
.Skip((options.PageNumber - 1) * options.PageSize)
|
||||
.Take(options.PageSize)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
Dictionary<int, TItem> items = await GetItems(ids);
|
||||
|
||||
return [.. ids.Select(uniqueId => items[uniqueId])];
|
||||
}
|
||||
else
|
||||
{
|
||||
IQueryable<TItem> selectQuery = GetSelectQuery(orderedQuery);
|
||||
|
||||
return await selectQuery
|
||||
.Skip((options.PageNumber - 1) * options.PageSize)
|
||||
.Take(options.PageSize)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Expression<Func<TBaseQuery, object?>> GetSortExpression(TSortField field);
|
||||
protected abstract IEnumerable<(Expression<Func<TBaseQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain();
|
||||
protected abstract IQueryable<int> GetSelectIdQuery(IOrderedQueryable<TBaseQuery> query);
|
||||
protected abstract IQueryable<TItem> GetSelectQuery(IOrderedQueryable<TBaseQuery> query);
|
||||
protected abstract Task<Dictionary<int, TItem>> GetItems(int[] ids);
|
||||
protected virtual Task PostLoadAsync(IList<TItem> items, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ public class CircleQuery
|
||||
|
||||
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleQuery>, ICircleSearchProvider
|
||||
{
|
||||
protected override bool UseSelectIdQuery => false;
|
||||
|
||||
protected override IQueryable<CircleQuery> GetBaseQuery()
|
||||
{
|
||||
// Project from Circles so we can use correlated subqueries per CircleId.
|
||||
@@ -120,7 +122,7 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleS
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override Expression<Func<CircleQuery, object>> GetSortExpression(CircleSortField field) => field switch
|
||||
protected override Expression<Func<CircleQuery, object?>> GetSortExpression(CircleSortField field) => field switch
|
||||
{
|
||||
CircleSortField.Favorite => x => !x.Circle.Favorite,
|
||||
CircleSortField.Blacklisted => x => !x.Circle.Blacklisted,
|
||||
@@ -134,6 +136,11 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleS
|
||||
yield return (x => x.Circle.MakerId, SortDirection.Ascending);
|
||||
}
|
||||
|
||||
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<CircleQuery> query)
|
||||
{
|
||||
return query.Select(x => x.Circle.CircleId);
|
||||
}
|
||||
|
||||
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleQuery> query)
|
||||
{
|
||||
// Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate
|
||||
@@ -205,4 +212,77 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleS
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
protected override async Task<Dictionary<int, CircleSearchItem>> GetItems(int[] ids)
|
||||
{
|
||||
// Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate
|
||||
var selected =
|
||||
from circle in context.Circles.AsNoTracking()
|
||||
//join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws
|
||||
//from latest in vws.DefaultIfEmpty()
|
||||
where ids.Contains(circle.CircleId)
|
||||
select new CircleSearchItem
|
||||
{
|
||||
CircleId = circle.CircleId,
|
||||
Name = circle.Name,
|
||||
MakerId = circle.MakerId,
|
||||
Favorite = circle.Favorite,
|
||||
Blacklisted = circle.Blacklisted,
|
||||
Spam = circle.Spam,
|
||||
// Aggregates
|
||||
Downloads = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
|
||||
.Sum() ?? 0,
|
||||
|
||||
Releases = context.VoiceWorks
|
||||
.Count(v => v.CircleId == circle.CircleId && v.SalesDate != null),
|
||||
|
||||
Pending = context.VoiceWorks
|
||||
.Count(v => v.CircleId == circle.CircleId && v.ExpectedDate != null),
|
||||
|
||||
FirstReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Min(),
|
||||
|
||||
LatestReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Max(),
|
||||
|
||||
// "Latest" by ProductId length, then value
|
||||
LatestProductId = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.ProductId)
|
||||
.FirstOrDefault(),
|
||||
|
||||
// If you want these two in base query too:
|
||||
LatestVoiceWorkHasImage = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => (bool?)v.HasImage)
|
||||
.FirstOrDefault(),
|
||||
|
||||
LatestVoiceWorkSalesDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.SalesDate)
|
||||
.FirstOrDefault(),
|
||||
//Downloads = item.Downloads,
|
||||
//Releases = item.Releases,
|
||||
//Pending = item.Pending,
|
||||
//FirstReleaseDate = item.FirstReleaseDate,
|
||||
//LatestReleaseDate = item.LatestReleaseDate,
|
||||
//LatestProductId = item.LatestProductId,
|
||||
//LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null,
|
||||
//LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null
|
||||
};
|
||||
|
||||
return await selected.ToDictionaryAsync(x => x.CircleId);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Data.Repositories.Creators;
|
||||
|
||||
public class CreatorSearchProvider(AppDbContext context) : SearchProvider<CreatorSearchItem, CreatorSearchCriteria, CreatorSortField, CreatorSearchItem>, ICreatorSearchProvider
|
||||
{
|
||||
protected override bool UseSelectIdQuery => false;
|
||||
|
||||
protected override IQueryable<CreatorSearchItem> GetBaseQuery()
|
||||
{
|
||||
return
|
||||
@@ -37,9 +39,9 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider<Creato
|
||||
return filteredQuery;
|
||||
}
|
||||
|
||||
protected override Expression<Func<CreatorSearchItem, object>> GetSortExpression(CreatorSortField field)
|
||||
protected override Expression<Func<CreatorSearchItem, object?>> GetSortExpression(CreatorSortField field)
|
||||
{
|
||||
Expression<Func<CreatorSearchItem, object>> selector = field switch
|
||||
Expression<Func<CreatorSearchItem, object?>> selector = field switch
|
||||
{
|
||||
CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||
CreatorSortField.Favorite => x => !x.Favorite,
|
||||
@@ -59,4 +61,14 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider<Creato
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<CreatorSearchItem> query)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<Dictionary<int, CreatorSearchItem>> GetItems(int[] ids)
|
||||
{
|
||||
return Task.FromResult(new Dictionary<int, CreatorSearchItem>());
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Data.Repositories.Tags;
|
||||
|
||||
public class TagSearchProvider(AppDbContext context) : SearchProvider<TagSearchItem, TagSearchCriteria, TagSortField, TagSearchItem>, ITagSearchProvider
|
||||
{
|
||||
protected override bool UseSelectIdQuery => false;
|
||||
|
||||
protected override IQueryable<TagSearchItem> GetBaseQuery()
|
||||
{
|
||||
return
|
||||
@@ -41,9 +43,9 @@ public class TagSearchProvider(AppDbContext context) : SearchProvider<TagSearchI
|
||||
return filteredQuery;
|
||||
}
|
||||
|
||||
protected override Expression<Func<TagSearchItem, object>> GetSortExpression(TagSortField field)
|
||||
protected override Expression<Func<TagSearchItem, object?>> GetSortExpression(TagSortField field)
|
||||
{
|
||||
Expression<Func<TagSearchItem, object>> selector = field switch
|
||||
Expression<Func<TagSearchItem, object?>> selector = field switch
|
||||
{
|
||||
TagSortField.EnglishName => x => x.EnglishName ?? "",
|
||||
TagSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||
@@ -64,4 +66,14 @@ public class TagSearchProvider(AppDbContext context) : SearchProvider<TagSearchI
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<TagSearchItem> query)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<Dictionary<int, TagSearchItem>> GetItems(int[] ids)
|
||||
{
|
||||
return Task.FromResult(new Dictionary<int, TagSearchItem>());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using JSMR.Domain.Enums;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
||||
using CircleStatus = JSMR.Application.VoiceWorks.Queries.Search.CircleStatus;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||
@@ -20,6 +21,8 @@ public class VoiceWorkQuery
|
||||
|
||||
public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSearch fullTextSearch) : SearchProvider<VoiceWorkSearchResult, VoiceWorkSearchCriteria, VoiceWorkSortField, VoiceWorkQuery>, IVoiceWorkSearchProvider
|
||||
{
|
||||
protected override bool UseSelectIdQuery => true;
|
||||
|
||||
protected override IQueryable<VoiceWorkQuery> GetBaseQuery()
|
||||
{
|
||||
return
|
||||
@@ -45,14 +48,6 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
IQueryable<VoiceWorkQuery> filteredQuery = query;
|
||||
|
||||
filteredQuery = ApplyKeywordsFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria);
|
||||
|
||||
if (criteria.Status is not null)
|
||||
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Status == (byte)criteria.Status);
|
||||
|
||||
switch (criteria.SaleStatus)
|
||||
{
|
||||
@@ -64,6 +59,18 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
break;
|
||||
}
|
||||
|
||||
if (criteria.SupportedLanguages.Length > 0)
|
||||
filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));
|
||||
|
||||
filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria);
|
||||
filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria);
|
||||
|
||||
if (criteria.Status is not null)
|
||||
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Status == (byte)criteria.Status);
|
||||
|
||||
if (criteria.ScheduledReleaseDateStart is not null)
|
||||
filteredQuery = filteredQuery.Where(x => x.VoiceWork.PlannedReleaseDate >= criteria.ScheduledReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));
|
||||
|
||||
@@ -79,8 +86,8 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
if (criteria.AgeRatings.Length > 0)
|
||||
filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating));
|
||||
|
||||
if (criteria.SupportedLanguages.Length > 0)
|
||||
filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));
|
||||
//if (criteria.SupportedLanguages.Length > 0)
|
||||
// filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));
|
||||
|
||||
if (criteria.AIGenerationOptions.Length > 0)
|
||||
filteredQuery = filteredQuery.Where(x => criteria.AIGenerationOptions.Contains((AIGeneration)x.VoiceWork.AIGeneration));
|
||||
@@ -133,6 +140,19 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
break;
|
||||
}
|
||||
|
||||
//switch (criteria.CircleStatus)
|
||||
//{
|
||||
// case CircleStatus.NotBlacklisted:
|
||||
// query = query.Where(x => x.Circle.Blacklisted == false);
|
||||
// break;
|
||||
// case CircleStatus.Favorited:
|
||||
// query = query.Where(x => x.Circle.Favorite);
|
||||
// break;
|
||||
// case CircleStatus.Blacklisted:
|
||||
// query = query.Where(x => x.Circle.Blacklisted);
|
||||
// break;
|
||||
//}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -314,13 +334,15 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
|
||||
protected override IEnumerable<(Expression<Func<VoiceWorkQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
|
||||
{
|
||||
yield return (x => x.VoiceWork.ProductId, SortDirection.Ascending);
|
||||
yield return (x => x.VoiceWork.ProductId, SortDirection.Descending);
|
||||
}
|
||||
|
||||
protected override Expression<Func<VoiceWorkQuery, object>> GetSortExpression(VoiceWorkSortField field) => field switch
|
||||
protected override Expression<Func<VoiceWorkQuery, object?>> GetSortExpression(VoiceWorkSortField field) => field switch
|
||||
{
|
||||
VoiceWorkSortField.ScheduledReleaseDate => x => x.VoiceWork.PlannedReleaseDate ?? x.VoiceWork.PlannedReleaseDate ?? DateTime.MinValue,
|
||||
VoiceWorkSortField.ReleaseDate => x => x.VoiceWork.SalesDate ?? x.VoiceWork.ExpectedDate ?? DateTime.MinValue,
|
||||
VoiceWorkSortField.ExpectedReleaseDate => x => x.VoiceWork.ExpectedDate,
|
||||
VoiceWorkSortField.ScheduledReleaseDate => x => x.VoiceWork.PlannedReleaseDate,
|
||||
VoiceWorkSortField.ReleaseDate => x => x.VoiceWork.SalesDate,
|
||||
VoiceWorkSortField.AnyReleaseDate => x => x.VoiceWork.SalesDate ?? x.VoiceWork.PlannedReleaseDate ?? x.VoiceWork.ExpectedDate,
|
||||
VoiceWorkSortField.Downloads => x => x.VoiceWork.Downloads ?? 0,
|
||||
VoiceWorkSortField.WishlistCount => x => x.VoiceWork.WishlistCount ?? 0,
|
||||
VoiceWorkSortField.StarRating => x => x.VoiceWork.StarRating ?? 0,
|
||||
@@ -328,6 +350,11 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
_ => x => x.VoiceWork.ProductId
|
||||
};
|
||||
|
||||
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<VoiceWorkQuery> query)
|
||||
{
|
||||
return query.Select(x => x.VoiceWork.VoiceWorkId);
|
||||
}
|
||||
|
||||
protected override IQueryable<VoiceWorkSearchResult> GetSelectQuery(IOrderedQueryable<VoiceWorkQuery> query)
|
||||
{
|
||||
var result =
|
||||
@@ -363,6 +390,43 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async Task<Dictionary<int, VoiceWorkSearchResult>> GetItems(int[] ids)
|
||||
{
|
||||
var result =
|
||||
from voiceWork in context.VoiceWorks.AsNoTracking()
|
||||
join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps
|
||||
from englishVoiceWork in ps.DefaultIfEmpty()
|
||||
join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs
|
||||
from circle in cs.DefaultIfEmpty()
|
||||
let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce"
|
||||
where ids.Contains(voiceWork.VoiceWorkId)
|
||||
select new VoiceWorkSearchResult()
|
||||
{
|
||||
VoiceWorkId = voiceWork.VoiceWorkId,
|
||||
ProductId = voiceWork.ProductId,
|
||||
OriginalProductId = voiceWork.OriginalProductId,
|
||||
ProductName = englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName,
|
||||
ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html",
|
||||
Description = englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description,
|
||||
Favorite = voiceWork.Favorite,
|
||||
HasImage = voiceWork.HasImage,
|
||||
Maker = circle.Name,
|
||||
MakerId = circle.MakerId,
|
||||
ExpectedDate = voiceWork.ExpectedDate,
|
||||
SalesDate = voiceWork.SalesDate,
|
||||
PlannedReleaseDate = voiceWork.PlannedReleaseDate,
|
||||
Downloads = voiceWork.Downloads,
|
||||
WishlistCount = voiceWork.WishlistCount,
|
||||
Status = voiceWork.Status,
|
||||
SubtitleLanguage = voiceWork.SubtitleLanguage,
|
||||
HasTrial = voiceWork.HasTrial,
|
||||
HasChobit = voiceWork.HasChobit,
|
||||
IsValid = voiceWork.IsValid
|
||||
};
|
||||
|
||||
return await result.ToDictionaryAsync(x => x.VoiceWorkId);
|
||||
}
|
||||
|
||||
protected override async Task PostLoadAsync(IList<VoiceWorkSearchResult> items, CancellationToken cancellationToken)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
|
||||
@@ -430,7 +430,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.ReleaseDate, SortDirection.Ascending)
|
||||
new(VoiceWorkSortField.AnyReleaseDate, SortDirection.Ascending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -448,7 +449,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.ReleaseDate, SortDirection.Descending)
|
||||
new(VoiceWorkSortField.AnyReleaseDate, SortDirection.Descending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -466,7 +468,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.Downloads, SortDirection.Ascending)
|
||||
new(VoiceWorkSortField.Downloads, SortDirection.Ascending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -484,7 +487,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.Downloads, SortDirection.Descending)
|
||||
new(VoiceWorkSortField.Downloads, SortDirection.Descending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -538,7 +542,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.StarRating, SortDirection.Ascending)
|
||||
new(VoiceWorkSortField.StarRating, SortDirection.Ascending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -556,7 +561,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
||||
{
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.StarRating, SortDirection.Descending)
|
||||
new(VoiceWorkSortField.StarRating, SortDirection.Descending),
|
||||
new(VoiceWorkSortField.ProductId, SortDirection.Ascending)
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div>
|
||||
<label>@IndexInfo</label>
|
||||
</div>
|
||||
<MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)TotalItems / (decimal)PageSize))" Selected="@PageNumber" SelectedChanged="OnSelectedChanged" />
|
||||
<div>
|
||||
<MudPagination class="pager" ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)TotalItems / (decimal)PageSize))" Selected="@PageNumber" SelectedChanged="OnSelectedChanged" />
|
||||
<div class="page-sizes">
|
||||
<MudSelect T="int" Value="PageSize" ValueChanged="OnPageSizeChanged" Dense="true" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (int value in PageSizes)
|
||||
{
|
||||
|
||||
@@ -7,18 +7,20 @@
|
||||
<JImage OverlayClass="j-voice-work-image-overlay" ImageClass="j-voice-work-image" Source="@ImageUrlProvider.GetImageUrl(Product, "main")"></JImage>
|
||||
</div>
|
||||
<div class="j-voice-work-content">
|
||||
<div class="j-product-title">@Product.ProductName</div>
|
||||
<div class="j-product-title">
|
||||
<a href="@Product.ProductUrl" target="_blank">@Product.ProductName</a>
|
||||
</div>
|
||||
<div class="j-product-contributors">
|
||||
<span class="j-circle">
|
||||
<MudChip T="string"
|
||||
Href="https://github.com/MudBlazor/MudBlazor"
|
||||
Href=@($"https://www.dlsite.com/maniax/circle/profile/=/maker_id/{Product.MakerId}.html")
|
||||
Target="_blank"
|
||||
Variant="Variant.Filled"
|
||||
Icon="@Icons.Material.Outlined.Circle">@Product.Maker</MudChip>
|
||||
@foreach (var creator in Product.Creators)
|
||||
{
|
||||
<MudChip T="string"
|
||||
Href="https://github.com/MudBlazor/MudBlazor"
|
||||
Href=@($"https://www.dlsite.com/maniax/fsr/=/keyword_creater/{creator.Name}")
|
||||
Target="_blank"
|
||||
Variant="Variant.Filled"
|
||||
Icon="@Icons.Material.Filled.Person">@creator.Name</MudChip>
|
||||
@@ -36,7 +38,7 @@
|
||||
<div class="j-voice-work-info">
|
||||
<div class="j-release-date-container">
|
||||
<span class="j-icon j-icon-calendar"></span>
|
||||
<span>@GetSomething(Product)</span>
|
||||
<span>@GetReleaseDateText(Product)</span>
|
||||
</div>
|
||||
<div class="j-wishlist-container">
|
||||
<span class="j-icon j-icon-star j-icon-color-yellow"></span>
|
||||
@@ -49,6 +51,14 @@
|
||||
<span>@((Product.Downloads ?? 0).ToString("n0"))</span>
|
||||
</div>
|
||||
}
|
||||
@* <div class="j-icon-2 j-icon-2-flag-@GetFlagClassSuffix(Product)"></div> *@
|
||||
<div class="j-spacer"></div>
|
||||
@if (Product.HasTrial || Product.HasChobit)
|
||||
{
|
||||
<div class="j-trial-container">
|
||||
<div class="j-icon j-icon-headphones"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +66,7 @@
|
||||
[Parameter]
|
||||
public required VoiceWorkSearchResult Product { get; set; }
|
||||
|
||||
private string GetSomething(VoiceWorkSearchResult voiceWork)
|
||||
private string GetReleaseDateText(VoiceWorkSearchResult voiceWork)
|
||||
{
|
||||
if (voiceWork.SalesDate.HasValue)
|
||||
{
|
||||
@@ -82,4 +92,19 @@
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private string GetFlagClassSuffix(VoiceWorkSearchResult voiceWork)
|
||||
{
|
||||
switch (voiceWork.SubtitleLanguage)
|
||||
{
|
||||
case 1:
|
||||
return "us";
|
||||
case 2:
|
||||
return "cn";
|
||||
case 3:
|
||||
return "kr";
|
||||
default:
|
||||
return "jp";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,67 +18,6 @@
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
|
||||
<style>
|
||||
.j-product-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.j-voice-work-card {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
background-image: linear-gradient(0deg, rgb(30, 53, 69), rgb(39, 59, 73));
|
||||
border-color: rgb(63, 78, 88);
|
||||
background-image: linear-gradient(0deg, rgb(30, 53, 69), rgb(57, 79, 94));
|
||||
}
|
||||
|
||||
.j-voice-work-image-container {
|
||||
width: 240px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.j-voice-work-card > .j-voice-work-image-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.j-voice-work-image {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.j-voice-work-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.j-voice-work-card > .j-voice-work-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.j-product-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-family: "Poppins", "M+ 1p";
|
||||
color: #d2dce6;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
|
||||
.j-product-description {
|
||||
/* color: #7C8099; */
|
||||
font-size: 1rem;
|
||||
font-family: "Poppins", "M+ 1p";
|
||||
}
|
||||
|
||||
.j-voice-work-info {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.j-voice-work-card > .j-voice-work-info {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
VoiceWorkSearchResult[]? availableVoiceWorks;
|
||||
VoiceWorkSearchResult[]? upcomingVoiceWorks;
|
||||
|
||||
@@ -14,6 +14,39 @@
|
||||
<div class="search-filter-control-span-4">
|
||||
<MudTextField T="string" Value="Keywords" ValueChanged="OnKeywordsChanged" Immediate="true" DebounceInterval="500" Label="Filter" Variant="Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<MudSelect T="string" Value="SelectedSaleStatus" ValueChanged="OnSaleStatusChanged" Label="Sale Status" Variant="Variant.Text">
|
||||
<MudSelectItem Value="@SaleStatus.Available.ToString()">Available</MudSelectItem>
|
||||
<MudSelectItem Value="@SaleStatus.Upcoming.ToString()">Upcoming</MudSelectItem>
|
||||
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<MudSelect T="string" Value="SelectedCircleStatus" ValueChanged="OnCircleStatusChanged" Label="Circle Status" Variant="Variant.Text">
|
||||
<MudSelectItem Value="@CircleStatus.NotBlacklisted.ToString()">Not Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@CircleStatus.Favorited.ToString()">Favorite</MudSelectItem>
|
||||
<MudSelectItem Value="@CircleStatus.Blacklisted.ToString()">Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<MudSelect T="string" Value="SelectedTagStatus" ValueChanged="OnTagStatusChanged" Label="Tag Status" Dense="true" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search">
|
||||
<MudSelectItem Value="@TagStatus.NotBlacklisted.ToString()">Not Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@TagStatus.FavoriteExcludeBlacklist.ToString()">Favorite (Exclude Blacklisted)</MudSelectItem>
|
||||
<MudSelectItem Value="@TagStatus.FavoriteIncludeBlacklist.ToString()">Favorite (Include Blacklisted)</MudSelectItem>
|
||||
<MudSelectItem Value="@TagStatus.Blacklisted.ToString()">Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<MudSelect T="string" Value="SelectedCreatorStatus" ValueChanged="OnCreatorStatusChanged" Label="Creator Status" Dense="true" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search">
|
||||
<MudSelectItem Value="@CreatorStatus.NotBlacklisted.ToString()">Not Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@CreatorStatus.FavoriteExcludeBlacklist.ToString()">Favorite (Exclude Blacklisted)</MudSelectItem>
|
||||
<MudSelectItem Value="@CreatorStatus.FavoriteIncludeBlacklist.ToString()">Favorite (Include Blacklisted)</MudSelectItem>
|
||||
<MudSelectItem Value="@CreatorStatus.Blacklisted.ToString()">Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<JProductCollection Products="searchResults?.Items"></JProductCollection>
|
||||
@@ -25,6 +58,10 @@
|
||||
|
||||
@code {
|
||||
public string? Keywords { get; set; }
|
||||
public string? SelectedSaleStatus { get; set; } = string.Empty;
|
||||
public string? SelectedCircleStatus { get; set; } = string.Empty;
|
||||
public string? SelectedTagStatus { get; set; } = string.Empty;
|
||||
public string? SelectedCreatorStatus { get; set; } = string.Empty;
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 100;
|
||||
|
||||
@@ -48,11 +85,15 @@
|
||||
Criteria = new()
|
||||
{
|
||||
Keywords = Keywords,
|
||||
SupportedLanguages = [Domain.Enums.Language.English]
|
||||
SaleStatus = string.IsNullOrWhiteSpace(SelectedSaleStatus) == false ? Enum.Parse<SaleStatus>(SelectedSaleStatus) : null,
|
||||
CircleStatus = string.IsNullOrWhiteSpace(SelectedCircleStatus) == false ? Enum.Parse<CircleStatus>(SelectedCircleStatus) : null,
|
||||
TagStatus = string.IsNullOrWhiteSpace(SelectedTagStatus) == false ? Enum.Parse<TagStatus>(SelectedTagStatus) : null,
|
||||
CreatorStatus = string.IsNullOrWhiteSpace(SelectedCreatorStatus) == false ? Enum.Parse<CreatorStatus>(SelectedCreatorStatus) : null,
|
||||
//SupportedLanguages = [Domain.Enums.Language.English]
|
||||
},
|
||||
SortOptions =
|
||||
[
|
||||
new(VoiceWorkSortField.ReleaseDate, Application.Common.Search.SortDirection.Descending)
|
||||
new(GetSortField(), Application.Common.Search.SortDirection.Descending)
|
||||
],
|
||||
PageNumber = PageNumber,
|
||||
PageSize = PageSize
|
||||
@@ -70,6 +111,30 @@
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnSaleStatusChanged(string? saleStatus)
|
||||
{
|
||||
SelectedSaleStatus = saleStatus;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnCircleStatusChanged(string? circleStatus)
|
||||
{
|
||||
SelectedCircleStatus = circleStatus;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnTagStatusChanged(string? tagStatus)
|
||||
{
|
||||
SelectedTagStatus = tagStatus;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnCreatorStatusChanged(string? creatorStatus)
|
||||
{
|
||||
SelectedCreatorStatus = creatorStatus;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnPageNumberChanged(int newPageNumber)
|
||||
{
|
||||
PageNumber = newPageNumber;
|
||||
@@ -81,4 +146,19 @@
|
||||
PageSize = newPageSize;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
private VoiceWorkSortField GetSortField()
|
||||
{
|
||||
SaleStatus? saleStatus = string.IsNullOrWhiteSpace(SelectedSaleStatus) == false ? Enum.Parse<SaleStatus>(SelectedSaleStatus) : null;
|
||||
|
||||
switch (saleStatus)
|
||||
{
|
||||
case SaleStatus.Available:
|
||||
return VoiceWorkSortField.ReleaseDate;
|
||||
case SaleStatus.Upcoming:
|
||||
return VoiceWorkSortField.ExpectedReleaseDate;
|
||||
default:
|
||||
return VoiceWorkSortField.AnyReleaseDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,20 @@ code {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pagination .pager {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination > .page-sizes {
|
||||
justify-content: flex-end;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pagination > .page-sizes > * {
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
/* Circle */
|
||||
.j-circle-image-container {
|
||||
height: 300px;
|
||||
@@ -330,8 +344,12 @@ code {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Product */
|
||||
/* Spacer */
|
||||
.j-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Product */
|
||||
.j-product-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -344,6 +362,9 @@ code {
|
||||
background-image: linear-gradient(0deg, rgb(30, 53, 69), rgb(39, 59, 73));
|
||||
border-color: rgb(63, 78, 88);
|
||||
background-image: linear-gradient(0deg, rgb(30, 53, 69), rgb(57, 79, 94));
|
||||
border-top-color: rgb(83, 99, 109);
|
||||
border-left-color: rgb(72, 88, 99);
|
||||
border-right-color: rgb(72, 88, 99);
|
||||
}
|
||||
|
||||
.j-voice-work-image-container {
|
||||
@@ -381,6 +402,15 @@ code {
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
|
||||
.j-product-title > a,
|
||||
.j-product-title > a:hover {
|
||||
color: var(--product-title-text-color);
|
||||
}
|
||||
|
||||
.j-product-title > a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.j-product-contributors {
|
||||
font-size: 1rem;
|
||||
font-family: "Poppins", "M+ 1p";
|
||||
@@ -437,6 +467,19 @@ code {
|
||||
gap: .5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #afe07d;
|
||||
}
|
||||
|
||||
.j-trial-container {
|
||||
border: 1px solid rgb(30, 53, 69);
|
||||
padding: .5rem;
|
||||
border-radius: 100%;
|
||||
border-color: var(--product-title-text-color);
|
||||
}
|
||||
|
||||
.j-trial-container > .j-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
@@ -461,7 +504,9 @@ code {
|
||||
|
||||
/* Icons */
|
||||
.j-icon {
|
||||
mask-size: auto;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
align-self: center;
|
||||
background: rgb(180,200, 214);
|
||||
height: 16px;
|
||||
@@ -474,6 +519,7 @@ code {
|
||||
|
||||
.j-icon-color-green {
|
||||
background: #388E3C;
|
||||
background: #afe07d;
|
||||
}
|
||||
|
||||
.j-icon-calendar {
|
||||
@@ -491,3 +537,31 @@ code {
|
||||
.j-icon-bag-fill {
|
||||
mask-image: url("../svg/bag-fill.svg");
|
||||
}
|
||||
|
||||
.j-icon-headphones {
|
||||
mask-image: url("../svg/headphones.svg");
|
||||
}
|
||||
|
||||
.j-icon-2 {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.j-icon-2-flag-jp {
|
||||
background-image: url("../svg/flag-jp.svg");
|
||||
}
|
||||
|
||||
.j-icon-2-flag-us {
|
||||
background-image: url("../svg/flag-us.svg");
|
||||
}
|
||||
|
||||
.j-icon-2-flag-cn {
|
||||
background-image: url("../svg/flag-cn.svg");
|
||||
}
|
||||
|
||||
.j-icon-2-flag-kr {
|
||||
background-image: url("../svg/flag-kr.svg");
|
||||
}
|
||||
Reference in New Issue
Block a user