From 2418bd0a8ff7de2ad9cd5574cc91bc20185a1529 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Mon, 17 Nov 2025 21:05:55 -0500 Subject: [PATCH] Updated search logic. More UI updates. --- JSMR.Application/Logging/LoggingExtensions.cs | 4 + .../Queries/Search/VoiceWorkSortField.cs | 5 +- .../Common/Queries/SearchProvider.cs | 43 +++++++-- .../Circles/CircleSearchProvider.cs | 82 ++++++++++++++++- .../Creators/CreatorSearchProvider.cs | 16 +++- .../Repositories/Tags/TagSearchProvider.cs | 16 +++- .../VoiceWorks/VoiceWorkSearchProvider.cs | 92 ++++++++++++++++--- .../VoiceWork/VoiceWorkSearchProviderTests.cs | 18 ++-- JSMR.UI.Blazor/Components/JPagination.razor | 4 +- JSMR.UI.Blazor/Components/JProduct.razor | 35 ++++++- JSMR.UI.Blazor/Pages/Home.razor | 61 ------------ JSMR.UI.Blazor/Pages/VoiceWorks.razor | 84 ++++++++++++++++- JSMR.UI.Blazor/wwwroot/css/app.css | 78 +++++++++++++++- 13 files changed, 430 insertions(+), 108 deletions(-) diff --git a/JSMR.Application/Logging/LoggingExtensions.cs b/JSMR.Application/Logging/LoggingExtensions.cs index 26e0338..a187acb 100644 --- a/JSMR.Application/Logging/LoggingExtensions.cs +++ b/JSMR.Application/Logging/LoggingExtensions.cs @@ -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) diff --git a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSortField.cs b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSortField.cs index 13bd52a..6727bed 100644 --- a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSortField.cs +++ b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSortField.cs @@ -2,11 +2,14 @@ public enum VoiceWorkSortField { + ExpectedReleaseDate, ScheduledReleaseDate, ReleaseDate, + AnyReleaseDate, Downloads, WishlistCount, SalesToWishlistRatio, StarRating, - FavoriteCircle + FavoriteCircle, + ProductId } \ No newline at end of file diff --git a/JSMR.Infrastructure/Common/Queries/SearchProvider.cs b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs index 9dde625..c34c961 100644 --- a/JSMR.Infrastructure/Common/Queries/SearchProvider.cs +++ b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs @@ -8,6 +8,8 @@ public abstract class SearchProvider : where TCriteria : new() where TSortField : struct, Enum { + protected abstract bool UseSelectIdQuery { get; } + public async Task> SearchAsync(SearchOptions options, CancellationToken cancellationToken = default) { IQueryable baseQuery = GetBaseQuery(); @@ -16,12 +18,7 @@ public abstract class SearchProvider : int total = await filteredQuery.CountAsync(cancellationToken); IOrderedQueryable orderedQuery = ApplySorting(filteredQuery, options.SortOptions); - IQueryable 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 : var (field, direction) = (sortOptions[i].Field, sortOptions[i].Direction); bool isDescending = direction == SortDirection.Descending; - IOrderedQueryable applyFirst(Expression> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector); - IOrderedQueryable applyNext(Expression> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector); + IOrderedQueryable applyFirst(Expression> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector); + IOrderedQueryable applyNext(Expression> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector); - Expression> selector = GetSortExpression(field); + Expression> selector = GetSortExpression(field); ordered = (i == 0) ? applyFirst(selector) : applyNext(selector); } @@ -83,8 +80,34 @@ public abstract class SearchProvider : } } - protected abstract Expression> GetSortExpression(TSortField field); + private async Task GetItemsAsync(SearchOptions options, IOrderedQueryable orderedQuery, CancellationToken cancellationToken) + { + if (UseSelectIdQuery) + { + int[] ids = await GetSelectIdQuery(orderedQuery) + .Skip((options.PageNumber - 1) * options.PageSize) + .Take(options.PageSize) + .ToArrayAsync(cancellationToken); + + Dictionary items = await GetItems(ids); + + return [.. ids.Select(uniqueId => items[uniqueId])]; + } + else + { + IQueryable selectQuery = GetSelectQuery(orderedQuery); + + return await selectQuery + .Skip((options.PageNumber - 1) * options.PageSize) + .Take(options.PageSize) + .ToArrayAsync(cancellationToken); + } + } + + protected abstract Expression> GetSortExpression(TSortField field); protected abstract IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain(); + protected abstract IQueryable GetSelectIdQuery(IOrderedQueryable query); protected abstract IQueryable GetSelectQuery(IOrderedQueryable query); + protected abstract Task> GetItems(int[] ids); protected virtual Task PostLoadAsync(IList items, CancellationToken cancellationToken) => Task.CompletedTask; } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs index 320932b..d3a3e1e 100644 --- a/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs @@ -14,6 +14,8 @@ public class CircleQuery public class CircleSearchProvider(AppDbContext context) : SearchProvider, ICircleSearchProvider { + protected override bool UseSelectIdQuery => false; + protected override IQueryable GetBaseQuery() { // Project from Circles so we can use correlated subqueries per CircleId. @@ -120,7 +122,7 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider> GetSortExpression(CircleSortField field) => field switch + protected override Expression> 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 x.Circle.MakerId, SortDirection.Ascending); } + protected override IQueryable GetSelectIdQuery(IOrderedQueryable query) + { + return query.Select(x => x.Circle.CircleId); + } + protected override IQueryable GetSelectQuery(IOrderedQueryable query) { // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate @@ -205,4 +212,77 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider> 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); + } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs index 0da300e..9312e2b 100644 --- a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs @@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Data.Repositories.Creators; public class CreatorSearchProvider(AppDbContext context) : SearchProvider, ICreatorSearchProvider { + protected override bool UseSelectIdQuery => false; + protected override IQueryable GetBaseQuery() { return @@ -37,9 +39,9 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider> GetSortExpression(CreatorSortField field) + protected override Expression> GetSortExpression(CreatorSortField field) { - Expression> selector = field switch + Expression> selector = field switch { CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount, CreatorSortField.Favorite => x => !x.Favorite, @@ -59,4 +61,14 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider GetSelectIdQuery(IOrderedQueryable query) + { + throw new NotImplementedException(); + } + + protected override Task> GetItems(int[] ids) + { + return Task.FromResult(new Dictionary()); + } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs index 6fab37a..09800c7 100644 --- a/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs @@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Data.Repositories.Tags; public class TagSearchProvider(AppDbContext context) : SearchProvider, ITagSearchProvider { + protected override bool UseSelectIdQuery => false; + protected override IQueryable GetBaseQuery() { return @@ -41,9 +43,9 @@ public class TagSearchProvider(AppDbContext context) : SearchProvider> GetSortExpression(TagSortField field) + protected override Expression> GetSortExpression(TagSortField field) { - Expression> selector = field switch + Expression> selector = field switch { TagSortField.EnglishName => x => x.EnglishName ?? "", TagSortField.VoiceWorkCount => x => x.VoiceWorkCount, @@ -64,4 +66,14 @@ public class TagSearchProvider(AppDbContext context) : SearchProvider GetSelectIdQuery(IOrderedQueryable query) + { + throw new NotImplementedException(); + } + + protected override Task> GetItems(int[] ids) + { + return Task.FromResult(new Dictionary()); + } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs index fd5e13b..314d90c 100644 --- a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs @@ -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, IVoiceWorkSearchProvider { + protected override bool UseSelectIdQuery => true; + protected override IQueryable GetBaseQuery() { return @@ -45,14 +48,6 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea IQueryable 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> Selector, SortDirection Dir)> GetDefaultSortChain() { - yield return (x => x.VoiceWork.ProductId, SortDirection.Ascending); + yield return (x => x.VoiceWork.ProductId, SortDirection.Descending); } - protected override Expression> GetSortExpression(VoiceWorkSortField field) => field switch + protected override Expression> 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 GetSelectIdQuery(IOrderedQueryable query) + { + return query.Select(x => x.VoiceWork.VoiceWorkId); + } + protected override IQueryable GetSelectQuery(IOrderedQueryable query) { var result = @@ -363,6 +390,43 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea return result; } + protected override async Task> 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 items, CancellationToken cancellationToken) { if (items.Count == 0) diff --git a/JSMR.Tests/Search/VoiceWork/VoiceWorkSearchProviderTests.cs b/JSMR.Tests/Search/VoiceWork/VoiceWorkSearchProviderTests.cs index 3f29908..406dcd7 100644 --- a/JSMR.Tests/Search/VoiceWork/VoiceWorkSearchProviderTests.cs +++ b/JSMR.Tests/Search/VoiceWork/VoiceWorkSearchProviderTests.cs @@ -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) ] }; diff --git a/JSMR.UI.Blazor/Components/JPagination.razor b/JSMR.UI.Blazor/Components/JPagination.razor index 090d6e6..f827687 100644 --- a/JSMR.UI.Blazor/Components/JPagination.razor +++ b/JSMR.UI.Blazor/Components/JPagination.razor @@ -2,8 +2,8 @@
- -
+ +
@foreach (int value in PageSizes) { diff --git a/JSMR.UI.Blazor/Components/JProduct.razor b/JSMR.UI.Blazor/Components/JProduct.razor index cbb9126..18e3a28 100644 --- a/JSMR.UI.Blazor/Components/JProduct.razor +++ b/JSMR.UI.Blazor/Components/JProduct.razor @@ -7,18 +7,20 @@
-
@Product.ProductName
+
@Product.Maker @foreach (var creator in Product.Creators) { @creator.Name @@ -36,7 +38,7 @@
- @GetSomething(Product) + @GetReleaseDateText(Product)
@@ -49,6 +51,14 @@ @((Product.Downloads ?? 0).ToString("n0"))
} + @*
*@ +
+ @if (Product.HasTrial || Product.HasChobit) + { +
+
+
+ }
@@ -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"; + } + } } diff --git a/JSMR.UI.Blazor/Pages/Home.razor b/JSMR.UI.Blazor/Pages/Home.razor index 1daf45a..115c502 100644 --- a/JSMR.UI.Blazor/Pages/Home.razor +++ b/JSMR.UI.Blazor/Pages/Home.razor @@ -18,67 +18,6 @@ - - @code { VoiceWorkSearchResult[]? availableVoiceWorks; VoiceWorkSearchResult[]? upcomingVoiceWorks; diff --git a/JSMR.UI.Blazor/Pages/VoiceWorks.razor b/JSMR.UI.Blazor/Pages/VoiceWorks.razor index 568f0ca..6a9f470 100644 --- a/JSMR.UI.Blazor/Pages/VoiceWorks.razor +++ b/JSMR.UI.Blazor/Pages/VoiceWorks.razor @@ -14,6 +14,39 @@
+
+ + Available + Upcoming + All + +
+
+ + Not Blacklisted + Favorite + Blacklisted + All + +
+
+ + Not Blacklisted + Favorite (Exclude Blacklisted) + Favorite (Include Blacklisted) + Blacklisted + All + +
+
+ + Not Blacklisted + Favorite (Exclude Blacklisted) + Favorite (Include Blacklisted) + Blacklisted + All + +
@@ -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(SelectedSaleStatus) : null, + CircleStatus = string.IsNullOrWhiteSpace(SelectedCircleStatus) == false ? Enum.Parse(SelectedCircleStatus) : null, + TagStatus = string.IsNullOrWhiteSpace(SelectedTagStatus) == false ? Enum.Parse(SelectedTagStatus) : null, + CreatorStatus = string.IsNullOrWhiteSpace(SelectedCreatorStatus) == false ? Enum.Parse(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(SelectedSaleStatus) : null; + + switch (saleStatus) + { + case SaleStatus.Available: + return VoiceWorkSortField.ReleaseDate; + case SaleStatus.Upcoming: + return VoiceWorkSortField.ExpectedReleaseDate; + default: + return VoiceWorkSortField.AnyReleaseDate; + } + } } \ No newline at end of file diff --git a/JSMR.UI.Blazor/wwwroot/css/app.css b/JSMR.UI.Blazor/wwwroot/css/app.css index 9a34f43..cdcb175 100644 --- a/JSMR.UI.Blazor/wwwroot/css/app.css +++ b/JSMR.UI.Blazor/wwwroot/css/app.css @@ -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 { @@ -490,4 +536,32 @@ 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"); } \ No newline at end of file