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 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 { 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 DeserializeItems(string? storedValue) { if (string.IsNullOrWhiteSpace(storedValue)) { return []; } var items = JsonSerializer.Deserialize>(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(); } }