Add shared recent tables state
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
@using System.Linq
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject LookupService LookupService
|
||||
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
|
||||
|
||||
<PageTitle>Critical Tables</PageTitle>
|
||||
|
||||
@@ -292,7 +293,10 @@
|
||||
if (tableDetail is null)
|
||||
{
|
||||
detailError = "The selected table could not be loaded.";
|
||||
return;
|
||||
}
|
||||
|
||||
await RecordRecentTableVisitAsync();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -769,6 +773,20 @@
|
||||
private Task PersistSelectedTableAsync(string tableSlug) =>
|
||||
JSRuntime.InvokeVoidAsync("localStorage.setItem", SelectedTableStorageKey, tableSlug).AsTask();
|
||||
|
||||
private Task RecordRecentTableVisitAsync()
|
||||
{
|
||||
if (SelectedTableReference is not { } selectedTable)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return RecentTablesState.RecordVisitAsync(
|
||||
selectedTable.Key,
|
||||
selectedTable.Label,
|
||||
selectedTable.Family,
|
||||
selectedTable.CurationPercentage);
|
||||
}
|
||||
|
||||
private string GetTableOptionCssClass(CriticalTableReference table)
|
||||
{
|
||||
var classes = new List<string>();
|
||||
|
||||
@@ -3,4 +3,5 @@ namespace RolemasterDb.App.Frontend.AppState;
|
||||
public static class BrowserStorageKeys
|
||||
{
|
||||
public const string ThemeMode = "rolemaster.theme.mode";
|
||||
public const string RecentTables = "rolemaster.tables.recent";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public sealed record RecentTableEntry(
|
||||
string Slug,
|
||||
string Label,
|
||||
string Family,
|
||||
int CurationPercentage,
|
||||
DateTimeOffset ViewedAtUtc);
|
||||
108
src/RolemasterDb.App/Frontend/AppState/RecentTablesState.cs
Normal file
108
src/RolemasterDb.App/Frontend/AppState/RecentTablesState.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public sealed class RecentTablesState(BrowserStorageService browserStorage)
|
||||
{
|
||||
private const int MaxItems = 8;
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private bool isInitialized;
|
||||
|
||||
public IReadOnlyList<RecentTableEntry> Items { get; private set; } = [];
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var storedValue = await browserStorage.GetItemAsync(BrowserStorageKeys.RecentTables);
|
||||
Items = DeserializeItems(storedValue);
|
||||
isInitialized = true;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Items = [];
|
||||
isInitialized = true;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// JS interop is unavailable during prerender. Retry on the next interactive render.
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RecordVisitAsync(string slug, string label, string family, int curationPercentage)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slug) || string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
var updatedItems = new List<RecentTableEntry>
|
||||
{
|
||||
new(
|
||||
slug.Trim(),
|
||||
label.Trim(),
|
||||
family.Trim(),
|
||||
curationPercentage,
|
||||
DateTimeOffset.UtcNow)
|
||||
};
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (string.Equals(item.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
updatedItems.Add(item);
|
||||
if (updatedItems.Count == MaxItems)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Items = updatedItems;
|
||||
await PersistAsync();
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private async Task PersistAsync()
|
||||
{
|
||||
var serialized = JsonSerializer.Serialize(Items, SerializerOptions);
|
||||
await browserStorage.SetItemAsync(BrowserStorageKeys.RecentTables, serialized);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RecentTableEntry> DeserializeItems(string? storedValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(storedValue))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var items = JsonSerializer.Deserialize<List<RecentTableEntry>>(storedValue, SerializerOptions);
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return items
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item.Slug) && !string.IsNullOrWhiteSpace(item.Label))
|
||||
.GroupBy(item => item.Slug, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group
|
||||
.OrderByDescending(item => item.ViewedAtUtc)
|
||||
.First())
|
||||
.OrderByDescending(item => item.ViewedAtUtc)
|
||||
.Take(MaxItems)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.Use
|
||||
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
||||
builder.Services.AddScoped<LookupService>();
|
||||
builder.Services.AddScoped<BrowserStorageService>();
|
||||
builder.Services.AddScoped<RecentTablesState>();
|
||||
builder.Services.AddScoped<ThemeState>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
Reference in New Issue
Block a user