Updated search logic. More UI updates.
All checks were successful
ci / build-test (push) Successful in 2m17s
ci / publish-image (push) Has been skipped

This commit is contained in:
2025-11-17 21:05:55 -05:00
parent 9ef1972472
commit 2418bd0a8f
13 changed files with 430 additions and 108 deletions

View File

@@ -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)

View File

@@ -2,11 +2,14 @@
public enum VoiceWorkSortField
{
ExpectedReleaseDate,
ScheduledReleaseDate,
ReleaseDate,
AnyReleaseDate,
Downloads,
WishlistCount,
SalesToWishlistRatio,
StarRating,
FavoriteCircle
FavoriteCircle,
ProductId
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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>());
}
}

View File

@@ -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>());
}
}

View File

@@ -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)

View File

@@ -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)
]
};

View File

@@ -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)
{

View File

@@ -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";
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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");
}