From f39a7697d71148c263896ec59925a6fa694f4263 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Tue, 25 Nov 2025 12:27:19 -0500 Subject: [PATCH] Simplified and enhnaced voice work page. --- .../Components/VoiceWorkFilters.razor | 208 ++++++++ JSMR.UI.Blazor/Enums/VoiceWorkSort.cs | 9 + .../Filters/VoiceWorkFilterState.cs | 233 +++++++++ JSMR.UI.Blazor/Pages/VoiceWorks.razor | 484 +++--------------- JSMR.UI.Blazor/Program.cs | 1 + JSMR.UI.Blazor/Services/ILookupDataService.cs | 22 + JSMR.UI.Blazor/Services/LookupDataService.cs | 134 +++++ 7 files changed, 680 insertions(+), 411 deletions(-) create mode 100644 JSMR.UI.Blazor/Components/VoiceWorkFilters.razor create mode 100644 JSMR.UI.Blazor/Enums/VoiceWorkSort.cs create mode 100644 JSMR.UI.Blazor/Filters/VoiceWorkFilterState.cs create mode 100644 JSMR.UI.Blazor/Services/ILookupDataService.cs create mode 100644 JSMR.UI.Blazor/Services/LookupDataService.cs diff --git a/JSMR.UI.Blazor/Components/VoiceWorkFilters.razor b/JSMR.UI.Blazor/Components/VoiceWorkFilters.razor new file mode 100644 index 0000000..8611fb9 --- /dev/null +++ b/JSMR.UI.Blazor/Components/VoiceWorkFilters.razor @@ -0,0 +1,208 @@ +@using JSMR.Application.Enums +@using JSMR.Application.VoiceWorks.Queries.Search +@using JSMR.Domain.Enums +@using JSMR.Domain.ValueObjects +@using JSMR.UI.Blazor.Enums +@using JSMR.UI.Blazor.Filters +@using JSMR.UI.Blazor.Services + +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ @*
+ +
*@ +
+ +@code { + [Inject] + ILookupDataService Lookups { get; set; } = default!; + + [Parameter] + public VoiceWorkFilterState Value { get; set; } = new(); + + [Parameter] + public EventCallback ValueChanged { get; set; } + + List> locales = []; + List> saleStatuses = []; + List> circleStatuses = []; + List> tagStatuses = []; + List> creatorStatuses = []; + List> languages = []; + List> ageRatings = []; + List> sortOptions = []; + List> tags = []; + List> creators = []; + + protected override async Task OnInitializedAsync() + { + locales = Lookups.GetLocales(); + saleStatuses = Lookups.GetSaleStatuses(); + circleStatuses = Lookups.GetCircleStatuses(); + tagStatuses = Lookups.GetTagStatuses(); + creatorStatuses = Lookups.GetCreatorStatuses(); + languages = Lookups.GetLanguages(); + ageRatings = Lookups.GetAgeRatings(); + sortOptions = Lookups.GetSortOptions(); + + (tags, creators) = (await Lookups.GetTagsAsync(), await Lookups.GetCreatorsAsync()); + } + + private Task Update(VoiceWorkFilterState next) + => ValueChanged.InvokeAsync(next with { PageNumber = 1 }); + + // Map DateOnly? -> DateTimeOffset? (UTC midnight to avoid TZ drift) + private static DateTimeOffset? ToDto(DateOnly? d) => + d is null ? null + : new DateTimeOffset(d.Value.ToDateTime(TimeOnly.MinValue, DateTimeKind.Unspecified), TimeSpan.Zero); + + // Map DateTimeOffset? -> DateOnly? + private static DateOnly? FromDto(DateTimeOffset? dto) => + dto is null ? null : DateOnly.FromDateTime(dto.Value.Date); +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/Enums/VoiceWorkSort.cs b/JSMR.UI.Blazor/Enums/VoiceWorkSort.cs new file mode 100644 index 0000000..007f959 --- /dev/null +++ b/JSMR.UI.Blazor/Enums/VoiceWorkSort.cs @@ -0,0 +1,9 @@ +namespace JSMR.UI.Blazor.Enums; + +public enum VoiceWorkSort +{ + ReleaseDateNewToOld, + ReleaseDateOldToNew, + BestSelling, + MostFavorited +} diff --git a/JSMR.UI.Blazor/Filters/VoiceWorkFilterState.cs b/JSMR.UI.Blazor/Filters/VoiceWorkFilterState.cs new file mode 100644 index 0000000..a75873e --- /dev/null +++ b/JSMR.UI.Blazor/Filters/VoiceWorkFilterState.cs @@ -0,0 +1,233 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Enums; +using JSMR.Application.VoiceWorks.Queries.Search; +using JSMR.Domain.Enums; +using JSMR.UI.Blazor.Enums; +using Microsoft.AspNetCore.WebUtilities; +using System.Globalization; + +namespace JSMR.UI.Blazor.Filters; + +public sealed record VoiceWorkFilterState +{ + // Core + public string? Keywords { get; init; } + public Locale Locale { get; init; } = Locale.English; + + // Statuses + public SaleStatus? SaleStatus { get; init; } + public CircleStatus? CircleStatus { get; init; } + public TagStatus? TagStatus { get; init; } + public CreatorStatus? CreatorStatus { get; init; } + + // Multi-selects + public int[] TagIds { get; init; } = Array.Empty(); + public bool IncludeAllTags { get; init; } + public int[] CreatorIds { get; init; } = Array.Empty(); + public bool IncludeAllCreators { get; init; } + public Language[] SupportedLanguages { get; init; } = Array.Empty(); + public AgeRating[] AgeRatings { get; init; } = Array.Empty(); + + // Misc flags + public bool ShowFavorite { get; init; } + public bool ShowInvalid { get; init; } + + // Dates (use yyyy-MM-dd in URL) + public DateOnly? ReleaseDateStart { get; init; } + public DateOnly? ReleaseDateEnd { get; init; } + + // Sorting & paging + public VoiceWorkSort Sort { get; init; } = VoiceWorkSort.ReleaseDateNewToOld; + public int PageNumber { get; init; } = 1; + public int PageSize { get; init; } = 100; + + // ---- Query <-> State helpers ---- + public Dictionary ToQuery() + { + Dictionary query = []; + + void Set(string key, string? value, bool includeWhenEmpty = false) + { + if (includeWhenEmpty || !string.IsNullOrWhiteSpace(value)) + query[key] = value; + } + + string Join(IEnumerable xs) => string.Join(",", xs); + + Set("keywords", Keywords); + + if (Locale != Locale.English) + Set("locale", Locale.ToString()); + + if (SaleStatus is not null) + Set("sale", SaleStatus.ToString()); + + if (CircleStatus is not null) + Set("circle", CircleStatus.ToString()); + + if (TagStatus is not null) + Set("tag", TagStatus.ToString()); + + if (CreatorStatus is not null) + Set("creator", CreatorStatus.ToString()); + + if (TagIds.Length > 0) + Set("tagIds", Join(TagIds)); + + if (IncludeAllTags) + Set("allTags", "1"); + + if (CreatorIds.Length > 0) + Set("creatorIds", Join(CreatorIds)); + + if (IncludeAllCreators) + Set("allCreators", "1"); + + if (SupportedLanguages.Length > 0) + Set("langs", Join(SupportedLanguages)); + + if (AgeRatings.Length > 0) + Set("ages", Join(AgeRatings)); + + if (ShowFavorite) + Set("fav", "1"); + + if (ShowInvalid) + Set("invalid", "1"); + + if (ReleaseDateStart is not null) + Set("rs", ReleaseDateStart.Value.ToString("yyyy-MM-dd")); + + if (ReleaseDateEnd is not null) + Set("re", ReleaseDateEnd.Value.ToString("yyyy-MM-dd")); + + if (Sort != VoiceWorkSort.ReleaseDateNewToOld) + Set("sort", Sort.ToString()); + + if (PageNumber != 1) + Set("pageNumber", PageNumber.ToString(CultureInfo.InvariantCulture)); + + if (PageSize != 100) + Set("pageSize", PageSize.ToString(CultureInfo.InvariantCulture)); + + return query; + } + + public static VoiceWorkFilterState FromQuery(string query) + { + var dict = QueryHelpers.ParseQuery(query); + + T? GetEnum(string key) where T : struct, Enum + => dict.TryGetValue(key, out var v) && Enum.TryParse(v!, true, out var e) ? e : null; + + string? Get(string key) => dict.TryGetValue(key, out var v) ? v!.ToString() : null; + int[] GetInts(string key) => Get(key)?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray() ?? Array.Empty(); + TEnum[] GetEnums(string key) where TEnum : struct, Enum + => Get(key)?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => Enum.TryParse(s, true, out var e) ? e : (TEnum?)null).Where(e => e is not null).Select(e => e!.Value).ToArray() + ?? Array.Empty(); + DateOnly? GetDate(string key) => DateOnly.TryParseExact(Get(key), "yyyy-MM-dd", out var d) ? d : null; + bool Has(string key) => dict.ContainsKey(key); + + return new VoiceWorkFilterState + { + Keywords = Get("keywords"), + Locale = GetEnum("locale") ?? Locale.English, + + SaleStatus = GetEnum("sale"), + CircleStatus = GetEnum("circle"), + TagStatus = GetEnum("tag"), + CreatorStatus = GetEnum("creator"), + + TagIds = GetInts("tagIds"), + IncludeAllTags = Has("allTags"), + CreatorIds = GetInts("creatorIds"), + IncludeAllCreators = Has("allCreators"), + SupportedLanguages = GetEnums("langs"), + AgeRatings = GetEnums("ages"), + + ShowFavorite = Has("fav"), + ShowInvalid = Has("invalid"), + + ReleaseDateStart = GetDate("rs"), + ReleaseDateEnd = GetDate("re"), + + Sort = GetEnum("sort") ?? VoiceWorkSort.ReleaseDateNewToOld, + PageNumber = int.TryParse(Get("pageNumber"), out var pageNumber) ? Math.Max(1, pageNumber) : 1, + PageSize = int.TryParse(Get("pageSize"), out var pageSize) ? pageSize : 100 + }; + } + + public SearchVoiceWorksRequest ToSearchRequest() + { + return new( + Options: new() + { + Criteria = new() + { + Keywords = Keywords, + Locale = Locale, + SaleStatus = SaleStatus, + CircleStatus = CircleStatus, + TagStatus = TagStatus, + CreatorStatus = CreatorStatus, + TagIds = TagIds, + IncludeAllTags = IncludeAllTags, + CreatorIds = CreatorIds, + IncludeAllCreators = IncludeAllCreators, + ShowFavoriteVoiceWorks = ShowFavorite, + ShowInvalidVoiceWorks = ShowInvalid, + SupportedLanguages = SupportedLanguages, + AgeRatings = AgeRatings, + ReleaseDateStart = ReleaseDateStart, + ReleaseDateEnd = ReleaseDateEnd, + //MinDownloads = MinDownloads + }, + SortOptions = GetSortOptions(), + PageNumber = PageNumber, + PageSize = PageSize + } + ); + } + + private SortOption[] GetSortOptions() + { + switch (Sort) + { + case VoiceWorkSort.ReleaseDateNewToOld: + return + [ + new(GetReleaseDateVoiceWorkSortField(), SortDirection.Descending) + ]; + case VoiceWorkSort.ReleaseDateOldToNew: + return + [ + new(GetReleaseDateVoiceWorkSortField(), SortDirection.Ascending) + ]; + case VoiceWorkSort.BestSelling: + return + [ + new(VoiceWorkSortField.Downloads, SortDirection.Descending) + ]; + case VoiceWorkSort.MostFavorited: + return + [ + new(VoiceWorkSortField.WishlistCount, SortDirection.Descending) + ]; + } + + return []; + } + + private VoiceWorkSortField GetReleaseDateVoiceWorkSortField() + { + switch (SaleStatus) + { + case Application.VoiceWorks.Queries.Search.SaleStatus.Available: + return VoiceWorkSortField.ReleaseDate; + case Application.VoiceWorks.Queries.Search.SaleStatus.Upcoming: + return VoiceWorkSortField.ExpectedReleaseDate; + default: + return VoiceWorkSortField.AnyReleaseDate; + } + } +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/Pages/VoiceWorks.razor b/JSMR.UI.Blazor/Pages/VoiceWorks.razor index 0bad9b3..1bbb102 100644 --- a/JSMR.UI.Blazor/Pages/VoiceWorks.razor +++ b/JSMR.UI.Blazor/Pages/VoiceWorks.razor @@ -1,452 +1,114 @@ @page "/voiceworks" @using JSMR.Application.Common.Search -@using JSMR.Application.Creators.Queries.Search -@using JSMR.Application.Enums -@using JSMR.Application.Tags.Queries.Search @using JSMR.Application.VoiceWorks.Queries.Search -@using JSMR.Domain.Enums -@using JSMR.Domain.ValueObjects @using JSMR.UI.Blazor.Components +@using JSMR.UI.Blazor.Filters @using JSMR.UI.Blazor.Services +@using Microsoft.AspNetCore.WebUtilities @inject VoiceWorksClient Client @inject IJSRuntime JS +@inject NavigationManager NavigationManager +@implements IAsyncDisposable; Voice Works

Voice Works

-
- @*
- -
*@ -
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
- -
-
- -
-
- -
- -
-
- -
-
-
- -
-
- -
-
- -
- @*
- -
*@ -
+ @if (searchResults is not null) { - + } @code { - public string? Keywords { get; set; } - public Locale Locale { get; set; } = Locale.English; - public SaleStatus? SelectedSaleStatus { get; set; } - public CircleStatus? SelectedCircleStatus { get; set; } - public TagStatus? SelectedTagStatus { get; set; } - public CreatorStatus? SelectedCreatorStatus { get; set; } - public int[] SelectedTagIds { get; set; } = []; - public bool IncludeAllTags { get; set; } - public int[] SelectedCreatorIds { get; set; } = []; - public bool IncludeAllCreators { get; set; } - public bool ShowOnlyFavoriteWorks { get; set; } - public bool ShowOnlyInvalidWorks { get; set; } - public IEnumerable SupportedLanguages { get; set; } = []; - public IEnumerable SelectedAgeRatings { get; set; } = []; - public DateTimeOffset? ReleaseDateStart { get; set; } - public DateTimeOffset? ReleaseDateEnd { get; set; } - public int MinDownloads { get; set; } - public int PageNumber { get; set; } = 1; - public int PageSize { get; set; } = 100; - - List> Locales = - [ - new() { Text = "Japanese", Value = Locale.Japanese }, - new() { Text = "English", Value = Locale.English } - ]; - - List> SaleStatuses = - [ - new() { Text = "Available", Value = SaleStatus.Available }, - new() { Text = "Upcoming", Value = SaleStatus.Upcoming }, - new() { Text = "All", Value = null } - ]; - - List> CircleStatuses = - [ - new() { Text = "Not Blacklisted", Value = CircleStatus.NotBlacklisted }, - new() { Text = "Favorites", Value = CircleStatus.Favorited }, - new() { Text = "Blacklisted", Value = CircleStatus.Blacklisted }, - new() { Text = "All", Value = null } - ]; - - List> TagStatuses = - [ - new() { Text = "Not Blacklisted", Value = TagStatus.NotBlacklisted }, - new() { Text = "Favorites (Exclude Blacklisted)", Value = TagStatus.FavoriteExcludeBlacklist }, - new() { Text = "Favorites (Include Blacklisted)", Value = TagStatus.FavoriteIncludeBlacklist }, - new() { Text = "Blacklisted", Value = TagStatus.Blacklisted }, - new() { Text = "All", Value = null } - ]; - - List> CreatorStatuses = - [ - new() { Text = "Not Blacklisted", Value = CreatorStatus.NotBlacklisted }, - new() { Text = "Favorites (Exclude Blacklisted)", Value = CreatorStatus.FavoriteExcludeBlacklist }, - new() { Text = "Favorites (Include Blacklisted)", Value = CreatorStatus.FavoriteIncludeBlacklist }, - new() { Text = "Blacklisted", Value = CreatorStatus.Blacklisted }, - new() { Text = "All", Value = null } - ]; - - List> Languages = - [ - new() { Text = "Japanese", Value = Language.Japanese }, - new() { Text = "English", Value = Language.English }, - new() { Text = "Chinese (Traditional)", Value = Language.ChineseTraditional }, - new() { Text = "Chinese (Simplified)", Value = Language.ChineseSimplified }, - new() { Text = "Korean", Value = Language.Korean } - ]; - - List> AgeRatings = - [ - new() { Text = "All Ages", Value = AgeRating.AllAges }, - new() { Text = "R15", Value = AgeRating.R15 }, - new() { Text = "R18", Value = AgeRating.R18 } - ]; - - List> Tags = []; - List> Creators = []; + private bool _isAlive = true; + private CancellationTokenSource _cts = new(); + VoiceWorkFilterState FilterState = new(); SearchResult? searchResults; protected override async Task OnInitializedAsync() { - await UpdateDataAsync(true); - await GetTags(); - await GetCreators(); + NavigationManager.LocationChanged += OnLocationChanged; + + FilterState = VoiceWorkFilterState.FromQuery(new Uri(NavigationManager.Uri).Query); + await RunSearchAsync(); } - private async Task GetTags() + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { - SearchTagsRequest request = new( - Options: new() - { - PageNumber = 1, - PageSize = 99999 - } - ); - - SearchTagsResponse? response = await Client.SearchAsync(request); - - if (response is null) + if (!_isAlive) return; - Tags = response.Results.Items - .OrderByDescending(x => x.Favorite) - .ThenBy(x => x.EnglishName ?? x.Name) - .Select(x => new BitDropdownItem() { Text = x.EnglishName ?? x.Name, Value = x.TagId }).ToList(); - } - - private async Task GetCreators() - { - SearchCreatorsRequest request = new( - Options: new() - { - PageNumber = 1, - PageSize = 99999 - } - ); - - SearchCreatorsResponse? response = await Client.SearchAsync(request); - - if (response is null) + if (!IsThisPage(e.Location)) return; - Creators = response.Results.Items.Select(x => new BitDropdownItem() { Text = x.Name, Value = x.CreatorId }).ToList(); + // Parse query from the new URL and update state if it actually changed. + string query = NavigationManager.ToAbsoluteUri(e.Location).Query; + VoiceWorkFilterState next = VoiceWorkFilterState.FromQuery(query); + + if (next != FilterState) + { + FilterState = next; + await RunSearchAsync(); + await InvokeAsync(StateHasChanged); + } } - private async Task UpdateDataAsync(bool resetPageNumber) + private bool IsThisPage(string absoluteUri) + { + string baseRelativePath = NavigationManager.ToBaseRelativePath(absoluteUri); + + return baseRelativePath.StartsWith("voiceworks", StringComparison.OrdinalIgnoreCase); + } + + async Task OnFilterStateChanged(VoiceWorkFilterState next) + { + if (next == FilterState) + return; + + UpdateUrl(next, false); + } + + void UpdateUrl(VoiceWorkFilterState next, bool replace) + { + string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path); + string uri = QueryHelpers.AddQueryString(basePath, next.ToQuery()); + + NavigationManager.NavigateTo(uri, replace: replace); + } + + async Task RunSearchAsync() { await JS.InvokeVoidAsync("pageHelpers.scrollToTop"); - if (resetPageNumber) - PageNumber = 1; - - SearchVoiceWorksRequest request = new( - Options: new() - { - Criteria = new() - { - Keywords = Keywords, - Locale = Locale, - SaleStatus = SelectedSaleStatus, - CircleStatus = SelectedCircleStatus, - TagStatus = SelectedTagStatus, - CreatorStatus = SelectedCreatorStatus, - TagIds = [.. SelectedTagIds], - IncludeAllTags = IncludeAllTags, - CreatorIds = [.. SelectedCreatorIds], - IncludeAllCreators = IncludeAllCreators, - ShowFavoriteVoiceWorks = ShowOnlyFavoriteWorks, - ShowInvalidVoiceWorks = ShowOnlyInvalidWorks, - SupportedLanguages = [.. SupportedLanguages], - AgeRatings = [.. SelectedAgeRatings], - ReleaseDateStart = ReleaseDateStart != null ? DateOnly.FromDateTime(ReleaseDateStart.Value.Date) : null, - ReleaseDateEnd = ReleaseDateEnd != null ? DateOnly.FromDateTime(ReleaseDateEnd.Value.Date) : null, - //MinDownloads = MinDownloads - }, - SortOptions = - [ - new(GetSortField(), Application.Common.Search.SortDirection.Descending) - ], - PageNumber = PageNumber, - PageSize = PageSize - } - ); - - SearchVoiceWorksResponse? response = await Client.SearchAsync(request); - - searchResults = response?.Results; - } - - public async Task OnKeywordsChanged(string? newKeywords) - { - Keywords = newKeywords; - await UpdateDataAsync(true); - } - - public async Task OnLocaleChanged(Locale locale) - { - Locale = locale; - await UpdateDataAsync(true); - } - - public async Task OnSaleStatusChanged(SaleStatus? saleStatus) - { - SelectedSaleStatus = saleStatus; - await UpdateDataAsync(true); - } - - public async Task OnCircleStatusChanged(CircleStatus? circleStatus) - { - SelectedCircleStatus = circleStatus; - await UpdateDataAsync(true); - } - - public async Task OnTagStatusChanged(TagStatus? tagStatus) - { - SelectedTagStatus = tagStatus; - await UpdateDataAsync(true); - } - - public async Task OnCreatorStatusChanged(CreatorStatus? creatorStatus) - { - SelectedCreatorStatus = creatorStatus; - await UpdateDataAsync(true); - } - - public async Task OnTagIdsChanged(IEnumerable tagIds) - { - SelectedTagIds = [..tagIds]; - await UpdateDataAsync(true); - } - - public async Task OnIncludeAllTagsChanged(bool includeAllTags) - { - IncludeAllTags = includeAllTags; - await UpdateDataAsync(true); - } - - public async Task OnCreatorIdsChanged(IEnumerable creatorIds) - { - SelectedCreatorIds = [..creatorIds]; - await UpdateDataAsync(true); - } - - public async Task OnIncludeAllCreatorsChanged(bool includeAllCreators) - { - IncludeAllCreators = includeAllCreators; - await UpdateDataAsync(true); - } - - public async Task OnSupportedLanguagesChanged(IEnumerable languages) - { - SupportedLanguages = languages; - await UpdateDataAsync(true); - } - - public async Task OnAgeRatingsChanged(IEnumerable ageRatings) - { - SelectedAgeRatings = ageRatings; - await UpdateDataAsync(true); - } - - public async Task OnShowOnlyFavoriteWorksChanged(bool showOnlyFavoriteWorks) - { - ShowOnlyFavoriteWorks = showOnlyFavoriteWorks; - await UpdateDataAsync(true); - } - - public async Task OnShowOnlyInvalidWorksChanged(bool showOnlyInvalidWorks) - { - ShowOnlyInvalidWorks = showOnlyInvalidWorks; - await UpdateDataAsync(true); - } - - public async Task OnReleaseDateStartChanged(DateTimeOffset? releaseDateStart) - { - ReleaseDateStart = releaseDateStart; - await UpdateDataAsync(true); - } - - public async Task OnReleaseDateEndChanged(DateTimeOffset? releaseDateEnd) - { - ReleaseDateEnd = releaseDateEnd; - await UpdateDataAsync(true); - } - - public async Task OnMinDownloadsChanged(double minDownloads) - { - MinDownloads = (int)minDownloads; - await UpdateDataAsync(true); - } - - public async Task OnPageNumberChanged(int newPageNumber) - { - PageNumber = newPageNumber; - await UpdateDataAsync(false); - } - - public async Task OnPageSizeChanged(int newPageSize) - { - PageSize = newPageSize; - await UpdateDataAsync(true); - } - - private VoiceWorkSortField GetSortField() - { - switch (SelectedSaleStatus) + try { - case SaleStatus.Available: - return VoiceWorkSortField.ReleaseDate; - case SaleStatus.Upcoming: - return VoiceWorkSortField.ExpectedReleaseDate; - default: - return VoiceWorkSortField.AnyReleaseDate; + _cts.Cancel(); + _cts = new(); + + SearchVoiceWorksResponse? response = await Client.SearchAsync(FilterState.ToSearchRequest(), _cts.Token); + searchResults = response?.Results; + } + catch (OperationCanceledException) + { + } } + + public async ValueTask DisposeAsync() + { + _isAlive = false; + NavigationManager.LocationChanged -= OnLocationChanged; + _cts.Cancel(); + _cts.Dispose(); + await Task.CompletedTask; + } } \ No newline at end of file diff --git a/JSMR.UI.Blazor/Program.cs b/JSMR.UI.Blazor/Program.cs index 4ac2619..0b88936 100644 --- a/JSMR.UI.Blazor/Program.cs +++ b/JSMR.UI.Blazor/Program.cs @@ -20,5 +20,6 @@ builder.Services.AddRadzenComponents(); builder.Services.AddBitBlazorUIServices(); builder.Services.AddScoped(); +builder.Services.AddScoped(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/JSMR.UI.Blazor/Services/ILookupDataService.cs b/JSMR.UI.Blazor/Services/ILookupDataService.cs new file mode 100644 index 0000000..48b52dc --- /dev/null +++ b/JSMR.UI.Blazor/Services/ILookupDataService.cs @@ -0,0 +1,22 @@ +using Bit.BlazorUI; +using JSMR.Application.Enums; +using JSMR.Application.VoiceWorks.Queries.Search; +using JSMR.Domain.Enums; +using JSMR.UI.Blazor.Enums; + +namespace JSMR.UI.Blazor.Services; + +public interface ILookupDataService +{ + List> GetLocales(); + List> GetSaleStatuses(); + List> GetCircleStatuses(); + List> GetTagStatuses(); + List> GetCreatorStatuses(); + List> GetLanguages(); + List> GetAgeRatings(); + List> GetSortOptions(); + + Task>> GetTagsAsync(CancellationToken cancellationToken = default); + Task>> GetCreatorsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/Services/LookupDataService.cs b/JSMR.UI.Blazor/Services/LookupDataService.cs new file mode 100644 index 0000000..7c86049 --- /dev/null +++ b/JSMR.UI.Blazor/Services/LookupDataService.cs @@ -0,0 +1,134 @@ +using Bit.BlazorUI; +using JSMR.Application.Creators.Queries.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Enums; +using JSMR.Application.Tags.Queries.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.VoiceWorks.Queries.Search; +using JSMR.Domain.Enums; +using JSMR.UI.Blazor.Enums; + +namespace JSMR.UI.Blazor.Services; + +public sealed class LookupDataService(VoiceWorksClient client) : ILookupDataService +{ + // simple in-memory caches for WASM + private List>? _tags; + private List>? _creators; + + public List> GetLocales() => + [ + new() { Text = "Japanese", Value = Locale.Japanese }, + new() { Text = "English", Value = Locale.English } + ]; + + public List> GetSaleStatuses() => + [ + new() { Text = "Available", Value = SaleStatus.Available }, + new() { Text = "Upcoming", Value = SaleStatus.Upcoming }, + new() { Text = "All", Value = null } + ]; + + public List> GetCircleStatuses() => + [ + new() { Text = "Not Blacklisted", Value = CircleStatus.NotBlacklisted }, + new() { Text = "Favorites", Value = CircleStatus.Favorited }, + new() { Text = "Blacklisted", Value = CircleStatus.Blacklisted }, + new() { Text = "All", Value = null } + ]; + + public List> GetTagStatuses() => + [ + new() { Text = "Not Blacklisted", Value = TagStatus.NotBlacklisted }, + new() { Text = "Favorites (Exclude Blacklisted)", Value = TagStatus.FavoriteExcludeBlacklist }, + new() { Text = "Favorites (Include Blacklisted)", Value = TagStatus.FavoriteIncludeBlacklist }, + new() { Text = "Blacklisted", Value = TagStatus.Blacklisted }, + new() { Text = "All", Value = null } + ]; + + public List> GetCreatorStatuses() => + [ + new() { Text = "Not Blacklisted", Value = CreatorStatus.NotBlacklisted }, + new() { Text = "Favorites (Exclude Blacklisted)", Value = CreatorStatus.FavoriteExcludeBlacklist }, + new() { Text = "Favorites (Include Blacklisted)", Value = CreatorStatus.FavoriteIncludeBlacklist }, + new() { Text = "Blacklisted", Value = CreatorStatus.Blacklisted }, + new() { Text = "All", Value = null } + ]; + + public List> GetLanguages() => + [ + new() { Text = "Japanese", Value = Language.Japanese }, + new() { Text = "English", Value = Language.English }, + new() { Text = "Chinese (Traditional)",Value = Language.ChineseTraditional }, + new() { Text = "Chinese (Simplified)", Value = Language.ChineseSimplified }, + new() { Text = "Korean", Value = Language.Korean } + ]; + + public List> GetAgeRatings() => + [ + new() { Text = "All Ages", Value = AgeRating.AllAges }, + new() { Text = "R15", Value = AgeRating.R15 }, + new() { Text = "R18", Value = AgeRating.R18 } + ]; + + public List> GetSortOptions() => + [ + new() + { + Text = "Release Date - New to Old", + Value = VoiceWorkSort.ReleaseDateNewToOld + }, + new() + { + Text = "Release Date - Old to New", + Value = VoiceWorkSort.ReleaseDateOldToNew + }, + new() + { + Text = "Best Selling", + Value = VoiceWorkSort.BestSelling + }, + new() + { + Text = "Most Favorited", + Value = VoiceWorkSort.MostFavorited + } + ]; + + public async Task>> GetTagsAsync(CancellationToken ct = default) + { + if (_tags is not null) return _tags; + + var resp = await client.SearchAsync(new SearchTagsRequest(new() + { + PageNumber = 1, + PageSize = 99999 + }), ct); + + _tags = (resp?.Results.Items ?? Array.Empty()) + .OrderByDescending(x => x.Favorite) + .ThenBy(x => x.EnglishName ?? x.Name, StringComparer.Ordinal) + .Select(x => new BitDropdownItem { Text = x.EnglishName ?? x.Name, Value = x.TagId }) + .ToList(); + + return _tags; + } + + public async Task>> GetCreatorsAsync(CancellationToken ct = default) + { + if (_creators is not null) return _creators; + + var resp = await client.SearchAsync(new SearchCreatorsRequest(new() + { + PageNumber = 1, + PageSize = 99999 + }), ct); + + _creators = (resp?.Results.Items ?? Array.Empty()) + .OrderBy(x => x.Name, StringComparer.Ordinal) + .Select(x => new BitDropdownItem { Text = x.Name, Value = x.CreatorId }) + .ToList(); + + return _creators; + } +} \ No newline at end of file