From 5b99f898601b07dc670c20135f4e8f846747c9d9 Mon Sep 17 00:00:00 2001
From: Patchzy <64382339+patchzyy@users.noreply.github.com>
Date: Wed, 18 Feb 2026 00:29:19 +0100
Subject: [PATCH 1/8] Add leaderboard page and podium card component
---
.../RrRooms/Domain/RwfcLeaderboardEntry.cs | 2 +-
WheelWizard/Views/App.axaml | 3 +-
.../WhWzLibrary/LeaderboardPodiumCard.axaml | 468 +++++++++++++++++
.../LeaderboardPodiumCard.axaml.cs | 100 ++++
WheelWizard/Views/Layout.axaml | 3 +
WheelWizard/Views/Pages/LeaderboardPage.axaml | 247 +++++++++
.../Views/Pages/LeaderboardPage.axaml.cs | 485 ++++++++++++++++++
.../Views/Pages/LeaderboardPlayerItem.cs | 26 +
8 files changed, 1332 insertions(+), 2 deletions(-)
create mode 100644 WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
create mode 100644 WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs
create mode 100644 WheelWizard/Views/Pages/LeaderboardPage.axaml
create mode 100644 WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
create mode 100644 WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
diff --git a/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs b/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs
index c72a4fcf..dae9630a 100644
--- a/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs
+++ b/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs
@@ -16,5 +16,5 @@ public sealed class RwfcLeaderboardEntry
public RwfcLeaderboardVrStats? VrStats { get; set; }
- public string? MiiImageBase64 { get; set; }
+ public string? MiiData { get; set; }
}
diff --git a/WheelWizard/Views/App.axaml b/WheelWizard/Views/App.axaml
index 65f76ef6..f7afb145 100644
--- a/WheelWizard/Views/App.axaml
+++ b/WheelWizard/Views/App.axaml
@@ -70,5 +70,6 @@
+
-
\ No newline at end of file
+
diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
new file mode 100644
index 00000000..64104b03
--- /dev/null
+++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
@@ -0,0 +1,468 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs
new file mode 100644
index 00000000..d07c1096
--- /dev/null
+++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs
@@ -0,0 +1,100 @@
+using Avalonia;
+using Avalonia.Controls.Primitives;
+using WheelWizard.WheelWizardData.Domain;
+using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
+
+namespace WheelWizard.Views.Components;
+
+public class LeaderboardPodiumCard : TemplatedControl
+{
+ public static readonly StyledProperty RankProperty = AvaloniaProperty.Register(nameof(Rank));
+
+ public int Rank
+ {
+ get => GetValue(RankProperty);
+ set => SetValue(RankProperty, value);
+ }
+
+ public static readonly StyledProperty PlacementLabelProperty = AvaloniaProperty.Register(
+ nameof(PlacementLabel),
+ string.Empty
+ );
+
+ public string PlacementLabel
+ {
+ get => GetValue(PlacementLabelProperty);
+ set => SetValue(PlacementLabelProperty, value);
+ }
+
+ public static readonly StyledProperty PlayerNameProperty = AvaloniaProperty.Register(
+ nameof(PlayerName),
+ string.Empty
+ );
+
+ public string PlayerName
+ {
+ get => GetValue(PlayerNameProperty);
+ set => SetValue(PlayerNameProperty, value);
+ }
+
+ public static readonly StyledProperty VrTextProperty = AvaloniaProperty.Register(
+ nameof(VrText),
+ "--"
+ );
+
+ public string VrText
+ {
+ get => GetValue(VrTextProperty);
+ set => SetValue(VrTextProperty, value);
+ }
+
+ public static readonly StyledProperty MiiProperty = AvaloniaProperty.Register(nameof(Mii));
+
+ public Mii? Mii
+ {
+ get => GetValue(MiiProperty);
+ set => SetValue(MiiProperty, value);
+ }
+
+ public static readonly StyledProperty AvatarSizeProperty = AvaloniaProperty.Register(
+ nameof(AvatarSize),
+ 104
+ );
+
+ public double AvatarSize
+ {
+ get => GetValue(AvatarSizeProperty);
+ set => SetValue(AvatarSizeProperty, value);
+ }
+
+ public static readonly StyledProperty BadgeVariantProperty = AvaloniaProperty.Register<
+ LeaderboardPodiumCard,
+ BadgeVariant
+ >(nameof(BadgeVariant), BadgeVariant.None);
+
+ public BadgeVariant BadgeVariant
+ {
+ get => GetValue(BadgeVariantProperty);
+ set => SetValue(BadgeVariantProperty, value);
+ }
+
+ public static readonly StyledProperty ShowBadgeProperty = AvaloniaProperty.Register(
+ nameof(ShowBadge)
+ );
+
+ public bool ShowBadge
+ {
+ get => GetValue(ShowBadgeProperty);
+ set => SetValue(ShowBadgeProperty, value);
+ }
+
+ public static readonly StyledProperty IsSuspiciousProperty = AvaloniaProperty.Register(
+ nameof(IsSuspicious)
+ );
+
+ public bool IsSuspicious
+ {
+ get => GetValue(IsSuspiciousProperty);
+ set => SetValue(IsSuspiciousProperty, value);
+ }
+}
diff --git a/WheelWizard/Views/Layout.axaml b/WheelWizard/Views/Layout.axaml
index e0512497..c91c38ee 100644
--- a/WheelWizard/Views/Layout.axaml
+++ b/WheelWizard/Views/Layout.axaml
@@ -134,6 +134,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
new file mode 100644
index 00000000..f7b9b363
--- /dev/null
+++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
@@ -0,0 +1,485 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using WheelWizard.Models;
+using WheelWizard.Resources.Languages;
+using WheelWizard.RrRooms;
+using WheelWizard.Services.Settings;
+using WheelWizard.Shared.DependencyInjection;
+using WheelWizard.Utilities.Generators;
+using WheelWizard.Views.Popups;
+using WheelWizard.Views.Popups.MiiManagement;
+using WheelWizard.WheelWizardData;
+using WheelWizard.WheelWizardData.Domain;
+using WheelWizard.WiiManagement.GameLicense;
+using WheelWizard.WiiManagement.MiiManagement;
+using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
+
+namespace WheelWizard.Views.Pages;
+
+public partial class LeaderboardPage : UserControlBase, INotifyPropertyChanged
+{
+ private CancellationTokenSource? _loadCts;
+
+ [Inject]
+ private IRrLeaderboardSingletonService LeaderboardService { get; set; } = null!;
+
+ [Inject]
+ private IWhWzDataSingletonService BadgeService { get; set; } = null!;
+
+ [Inject]
+ private IGameLicenseSingletonService GameDataService { get; set; } = null!;
+
+ private bool _hasLoadedOnce;
+ private bool _hasError;
+ private bool _hasNoData;
+ private bool _hasData;
+ private string _errorMessage = string.Empty;
+ private int _loadedPlayerCount;
+ private LeaderboardPlayerItem? _podiumFirst;
+ private LeaderboardPlayerItem? _podiumSecond;
+ private LeaderboardPlayerItem? _podiumThird;
+
+ public ObservableCollection RemainingPlayers { get; } = [];
+
+ public bool HasError
+ {
+ get => _hasError;
+ private set
+ {
+ if (_hasError == value)
+ return;
+ _hasError = value;
+ OnPropertyChanged(nameof(HasError));
+ }
+ }
+
+ public bool HasNoData
+ {
+ get => _hasNoData;
+ private set
+ {
+ if (_hasNoData == value)
+ return;
+ _hasNoData = value;
+ OnPropertyChanged(nameof(HasNoData));
+ }
+ }
+
+ public bool HasData
+ {
+ get => _hasData;
+ private set
+ {
+ if (_hasData == value)
+ return;
+ _hasData = value;
+ OnPropertyChanged(nameof(HasData));
+ }
+ }
+
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ private set
+ {
+ if (_errorMessage == value)
+ return;
+ _errorMessage = value;
+ OnPropertyChanged(nameof(ErrorMessage));
+ }
+ }
+
+ public string TotalPlayerCountText => _loadedPlayerCount.ToString();
+
+ public string RemainingCountText => $"{RemainingPlayers.Count} players";
+
+ public LeaderboardPlayerItem? PodiumFirst
+ {
+ get => _podiumFirst;
+ private set
+ {
+ if (_podiumFirst == value)
+ return;
+ _podiumFirst = value;
+ OnPropertyChanged(nameof(PodiumFirst));
+ OnPropertyChanged(nameof(HasPodiumFirst));
+ }
+ }
+
+ public LeaderboardPlayerItem? PodiumSecond
+ {
+ get => _podiumSecond;
+ private set
+ {
+ if (_podiumSecond == value)
+ return;
+ _podiumSecond = value;
+ OnPropertyChanged(nameof(PodiumSecond));
+ OnPropertyChanged(nameof(HasPodiumSecond));
+ }
+ }
+
+ public LeaderboardPlayerItem? PodiumThird
+ {
+ get => _podiumThird;
+ private set
+ {
+ if (_podiumThird == value)
+ return;
+ _podiumThird = value;
+ OnPropertyChanged(nameof(PodiumThird));
+ OnPropertyChanged(nameof(HasPodiumThird));
+ }
+ }
+
+ public bool HasPodiumFirst => PodiumFirst != null;
+ public bool HasPodiumSecond => PodiumSecond != null;
+ public bool HasPodiumThird => PodiumThird != null;
+
+ public LeaderboardPage()
+ {
+ InitializeComponent();
+ DataContext = this;
+ RemainingPlayers.CollectionChanged += RemainingPlayers_OnCollectionChanged;
+
+ Loaded += LeaderboardPage_Loaded;
+ Unloaded += LeaderboardPage_Unloaded;
+ }
+
+ private async void LeaderboardPage_Loaded(object? sender, RoutedEventArgs e)
+ {
+ if (_hasLoadedOnce)
+ return;
+
+ _hasLoadedOnce = true;
+ await ReloadLeaderboardAsync();
+ }
+
+ private void LeaderboardPage_Unloaded(object? sender, RoutedEventArgs e)
+ {
+ CancelCurrentLoad();
+ }
+
+ private void RemainingPlayers_OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnPropertyChanged(nameof(RemainingCountText));
+ }
+
+ private async void RetryButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ await ReloadLeaderboardAsync();
+ }
+
+ private async Task ReloadLeaderboardAsync()
+ {
+ CancelCurrentLoad();
+ _loadCts = new();
+ var cancellationToken = _loadCts.Token;
+
+ SetLoadingState();
+ ClearLeaderboardData();
+
+ var leaderboardResult = await LeaderboardService.GetTopPlayersAsync(50);
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ if (leaderboardResult.IsFailure)
+ {
+ SetErrorState(leaderboardResult.Error?.Message ?? "Unable to fetch leaderboard.");
+ return;
+ }
+
+ var orderedEntries = leaderboardResult
+ .Value.Select((entry, index) => new { Entry = entry, Rank = ResolveRank(entry, index) })
+ .OrderBy(entry => entry.Rank)
+ .Take(50)
+ .ToList();
+
+ if (orderedEntries.Count == 0)
+ {
+ SetEmptyState();
+ return;
+ }
+
+ var mappedPlayers = orderedEntries.Select((entry, index) => CreateLeaderboardPlayer(entry.Entry, entry.Rank, index)).ToList();
+
+ _loadedPlayerCount = mappedPlayers.Count;
+ OnPropertyChanged(nameof(TotalPlayerCountText));
+
+ PodiumFirst = mappedPlayers.ElementAtOrDefault(0);
+ PodiumSecond = mappedPlayers.ElementAtOrDefault(1);
+ PodiumThird = mappedPlayers.ElementAtOrDefault(2);
+
+ foreach (var player in mappedPlayers.Skip(3))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ RemainingPlayers.Add(player);
+
+ try
+ {
+ await Task.Delay(12, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+
+ SetDataState();
+ }
+
+ private LeaderboardPlayerItem CreateLeaderboardPlayer(RwfcLeaderboardEntry entry, int rank, int index)
+ {
+ var friendCode = entry.FriendCode ?? string.Empty;
+ var badges = string.IsNullOrWhiteSpace(friendCode) ? [] : BadgeService.GetBadges(friendCode);
+ var primaryBadge = badges.FirstOrDefault(BadgeVariant.None);
+
+ return new()
+ {
+ Rank = rank,
+ PlacementLabel = GetPlacementLabel(rank),
+ Name = string.IsNullOrWhiteSpace(entry.Name) ? "Unknown Player" : entry.Name,
+ FriendCode = friendCode,
+ VrText = entry.Vr?.ToString("N0") ?? "--",
+ Mii = DeserializeMii(entry.MiiData),
+ PrimaryBadge = primaryBadge,
+ HasBadge = primaryBadge != BadgeVariant.None,
+ IsSuspicious = entry.IsSuspicious,
+ IsEvenRow = index % 2 == 0,
+ };
+ }
+
+ private static int ResolveRank(RwfcLeaderboardEntry entry, int index)
+ {
+ if (entry.Rank is > 0 and <= 50000)
+ return entry.Rank.Value;
+
+ if (entry.ActiveRank is > 0 and <= 50000)
+ return entry.ActiveRank.Value;
+
+ return index + 1;
+ }
+
+ private static string GetPlacementLabel(int rank) =>
+ rank switch
+ {
+ 1 => "Champion",
+ 2 => "2nd Place",
+ 3 => "3rd Place",
+ _ => $"#{rank}",
+ };
+
+ private static Mii? DeserializeMii(string? miiData)
+ {
+ if (string.IsNullOrWhiteSpace(miiData))
+ return null;
+
+ try
+ {
+ var result = MiiSerializer.Deserialize(miiData);
+ return result.IsSuccess ? result.Value : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private void SetLoadingState()
+ {
+ HasError = false;
+ HasNoData = false;
+ HasData = false;
+ ErrorMessage = string.Empty;
+ }
+
+ private void SetErrorState(string message)
+ {
+ HasError = true;
+ HasNoData = false;
+ HasData = false;
+ ErrorMessage = string.IsNullOrWhiteSpace(message) ? "Failed to load leaderboard." : message;
+ }
+
+ private void SetEmptyState()
+ {
+ HasError = false;
+ HasNoData = true;
+ HasData = false;
+ }
+
+ private void SetDataState()
+ {
+ HasError = false;
+ HasNoData = false;
+ HasData = true;
+ }
+
+ private void ClearLeaderboardData()
+ {
+ PodiumFirst = null;
+ PodiumSecond = null;
+ PodiumThird = null;
+ RemainingPlayers.Clear();
+ _loadedPlayerCount = 0;
+
+ OnPropertyChanged(nameof(TotalPlayerCountText));
+ OnPropertyChanged(nameof(RemainingCountText));
+ }
+
+ private void CancelCurrentLoad()
+ {
+ if (_loadCts == null)
+ return;
+
+ _loadCts.Cancel();
+ _loadCts.Dispose();
+ _loadCts = null;
+ }
+
+ private void CopyFriendCode_OnClick(object sender, RoutedEventArgs e)
+ {
+ var player = GetContextPlayer(sender);
+ if (player == null || string.IsNullOrWhiteSpace(player.FriendCode))
+ return;
+
+ TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(player.FriendCode);
+ ViewUtils.ShowSnackbar(Phrases.SnackbarSuccess_CopiedFC);
+ }
+
+ private void OpenCarousel_OnClick(object sender, RoutedEventArgs e)
+ {
+ var player = GetContextPlayer(sender);
+ if (player?.FirstMii == null)
+ return;
+
+ new MiiCarouselWindow().SetMii(player.FirstMii).Show();
+ }
+
+ private void ViewProfile_OnClick(object sender, RoutedEventArgs e)
+ {
+ var player = GetContextPlayer(sender);
+ if (player == null || string.IsNullOrWhiteSpace(player.FriendCode))
+ return;
+
+ new PlayerProfileWindow(player.FriendCode).Show();
+ }
+
+ private async void AddFriend_OnClick(object sender, RoutedEventArgs e)
+ {
+ var player = GetContextPlayer(sender);
+ if (player == null)
+ return;
+
+ if (player.FirstMii == null)
+ {
+ ViewUtils.ShowSnackbar("This player has no valid Mii data.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get();
+ if (focusedUserIndex is < 0 or > 3)
+ {
+ ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var activeUserPid = FriendCodeGenerator.FriendCodeToProfileId(GameDataService.ActiveUser.FriendCode);
+ if (activeUserPid == 0)
+ {
+ ViewUtils.ShowSnackbar("Select a valid license before adding friends.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ if (GameDataService.ActiveCurrentFriends.Count >= 30)
+ {
+ ViewUtils.ShowSnackbar("Your friend list is full.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var normalizedFriendCodeResult = NormalizeFriendCode(player.FriendCode);
+ if (normalizedFriendCodeResult.IsFailure)
+ {
+ ViewUtils.ShowSnackbar(normalizedFriendCodeResult.Error.Message, ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var normalizedFriendCode = normalizedFriendCodeResult.Value;
+ var friendProfileId = FriendCodeGenerator.FriendCodeToProfileId(normalizedFriendCode);
+ if (activeUserPid == friendProfileId)
+ {
+ ViewUtils.ShowSnackbar("You cannot add your own friend code.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var duplicateFriend = GameDataService.ActiveCurrentFriends.Any(friend =>
+ {
+ var existingPid = FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode);
+ return existingPid != 0 && existingPid == friendProfileId;
+ });
+
+ if (duplicateFriend)
+ {
+ ViewUtils.ShowSnackbar("This friend is already in your list.", ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ var profile = new PlayerProfileResponse
+ {
+ Name = player.Name,
+ FriendCode = normalizedFriendCode,
+ Vr = int.TryParse(player.VrText.Replace(",", string.Empty), out var vr) ? vr : 0,
+ };
+
+ var shouldAdd = await new AddFriendConfirmationWindow(profile, player.FirstMii).AwaitAnswer();
+ if (!shouldAdd)
+ return;
+
+ var addResult = GameDataService.AddFriend(focusedUserIndex, normalizedFriendCode, player.FirstMii, (uint)Math.Max(profile.Vr, 0));
+
+ if (addResult.IsFailure)
+ {
+ ViewUtils.ShowSnackbar(addResult.Error.Message, ViewUtils.SnackbarType.Warning);
+ return;
+ }
+
+ ViewUtils.GetLayout().UpdateFriendCount();
+ ViewUtils.ShowSnackbar($"Added {player.Name} to your friend list.");
+ }
+
+ private static LeaderboardPlayerItem? GetContextPlayer(object sender)
+ {
+ if (sender is not Control control)
+ return null;
+ return control.DataContext as LeaderboardPlayerItem;
+ }
+
+ private static OperationResult NormalizeFriendCode(string friendCode)
+ {
+ if (string.IsNullOrWhiteSpace(friendCode))
+ return Fail("Friend code cannot be empty.");
+
+ var digits = new string(friendCode.Where(char.IsDigit).ToArray());
+ if (digits.Length != 12 || !ulong.TryParse(digits, out _))
+ return Fail("Friend code must be exactly 12 digits.");
+
+ var formatted = $"{digits[..4]}-{digits.Substring(4, 4)}-{digits.Substring(8, 4)}";
+ var profileId = FriendCodeGenerator.FriendCodeToProfileId(formatted);
+ if (profileId == 0)
+ return Fail("Invalid friend code.");
+
+ return formatted;
+ }
+
+ public new event PropertyChangedEventHandler? PropertyChanged;
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new(propertyName));
+ }
+}
diff --git a/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs b/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
new file mode 100644
index 00000000..ddd4dbd3
--- /dev/null
+++ b/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
@@ -0,0 +1,26 @@
+using WheelWizard.WheelWizardData.Domain;
+using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
+
+namespace WheelWizard.Views.Pages;
+
+public sealed class LeaderboardPlayerItem
+{
+ public required int Rank { get; init; }
+ public required string PlacementLabel { get; init; }
+ public required string Name { get; init; }
+ public required string FriendCode { get; init; }
+ public required string VrText { get; init; }
+ public Mii? Mii { get; init; }
+ public BadgeVariant PrimaryBadge { get; init; }
+ public bool HasBadge { get; init; }
+ public bool IsSuspicious { get; init; }
+ public bool IsEvenRow { get; init; }
+
+ // Keep parity with RoomDetailsPage player template bindings.
+ public string VrDisplay => VrText;
+ public Mii? FirstMii => Mii;
+ public bool HasBadges => HasBadge;
+ public bool IsTopLeaderboardPlayer => true;
+ public string TopLabel => $"#{Rank}";
+ public bool IsOpenHost => false;
+}
From c3f0a41c1caa9abb410626b7a86d5dc64597ec7c Mon Sep 17 00:00:00 2001
From: Patchzy <64382339+patchzyy@users.noreply.github.com>
Date: Wed, 18 Feb 2026 01:36:50 +0100
Subject: [PATCH 2/8] Add leaderboard caching and loading UI
---
.../RrRooms/RrLeaderboardSingletonService.cs | 83 ++++++++++-
.../WhWzLibrary/LeaderboardPodiumCard.axaml | 2 +-
.../WhWzLibrary/PlayerListItem.axaml | 20 ++-
WheelWizard/Views/Pages/LeaderboardPage.axaml | 27 +++-
.../Views/Pages/LeaderboardPage.axaml.cs | 134 +++++++++++-------
5 files changed, 205 insertions(+), 61 deletions(-)
diff --git a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
index bedddc6c..54dac2c9 100644
--- a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
+++ b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Logging;
using WheelWizard.Shared.Services;
namespace WheelWizard.RrRooms;
@@ -7,11 +8,89 @@ public interface IRrLeaderboardSingletonService
Task>> GetTopPlayersAsync(int limit = 50);
}
-public class RrLeaderboardSingletonService(IApiCaller apiCaller) : IRrLeaderboardSingletonService
+public class RrLeaderboardSingletonService(IApiCaller apiCaller, ILogger logger)
+ : IRrLeaderboardSingletonService
{
+ private static readonly TimeSpan CacheLifetime = TimeSpan.FromSeconds(90);
+ private readonly SemaphoreSlim _refreshGate = new(1, 1);
+ private readonly object _cacheLock = new();
+
+ private DateTimeOffset _cacheFetchedAt = DateTimeOffset.MinValue;
+ private List _cachedEntries = [];
+
public async Task>> GetTopPlayersAsync(int limit = 50)
{
var boundedLimit = Math.Clamp(limit, 1, 200);
- return await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));
+
+ if (TryGetFreshCache(boundedLimit, out var freshCache))
+ return freshCache;
+
+ await _refreshGate.WaitAsync();
+
+ try
+ {
+ if (TryGetFreshCache(boundedLimit, out freshCache))
+ return freshCache;
+
+ var fetchResult = await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));
+ if (fetchResult.IsFailure)
+ {
+ if (TryGetAnyCache(boundedLimit, out var staleCache))
+ {
+ logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit);
+ return staleCache;
+ }
+
+ return fetchResult;
+ }
+
+ lock (_cacheLock)
+ {
+ _cachedEntries = fetchResult.Value.ToList();
+ _cacheFetchedAt = DateTimeOffset.UtcNow;
+ }
+
+ return TrimToLimit(fetchResult.Value, boundedLimit);
+ }
+ finally
+ {
+ _refreshGate.Release();
+ }
+ }
+
+ private bool TryGetFreshCache(int limit, out OperationResult> result)
+ {
+ lock (_cacheLock)
+ {
+ var hasFreshCache = DateTimeOffset.UtcNow - _cacheFetchedAt <= CacheLifetime;
+ if (!hasFreshCache || _cachedEntries.Count < limit)
+ {
+ result = default!;
+ return false;
+ }
+
+ result = TrimToLimit(_cachedEntries, limit);
+ return true;
+ }
+ }
+
+ private bool TryGetAnyCache(int limit, out OperationResult> result)
+ {
+ lock (_cacheLock)
+ {
+ if (_cachedEntries.Count < limit)
+ {
+ result = default!;
+ return false;
+ }
+
+ result = TrimToLimit(_cachedEntries, limit);
+ return true;
+ }
+ }
+
+ private static OperationResult> TrimToLimit(IEnumerable entries, int limit)
+ {
+ return entries.Take(limit).ToList();
}
}
diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
index 64104b03..41938974 100644
--- a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
+++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml
@@ -66,7 +66,7 @@
BorderThickness="1"
CornerRadius="20"
Padding="10,14,10,10"
- Margin="4,5,4,5"
+ Margin="4,8,4,5"
ClipToBounds="True"
BoxShadow="0 18 30 0 #22000000">
-
-
+
-
\ No newline at end of file
+
+
+
+
+
diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml b/WheelWizard/Views/Pages/LeaderboardPage.axaml
index d11de3cb..202b4b81 100644
--- a/WheelWizard/Views/Pages/LeaderboardPage.axaml
+++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml
@@ -55,6 +55,14 @@
+
+
@@ -90,6 +98,21 @@
+
+
+
+
+
+ IsTopPlayer="True"
+ TopLabel="{Binding TopLabel}">
From 0dcc00961039c22331814dffaaa19089faa60c3c Mon Sep 17 00:00:00 2001
From: Patchzy <64382339+patchzyy@users.noreply.github.com>
Date: Wed, 18 Feb 2026 13:08:01 +0100
Subject: [PATCH 4/8] Add friend/online flags and join-room UI
---
WheelWizard/Models/RRInfo/RrPlayer.cs | 1 +
WheelWizard/Services/LiveData/RRLiveRooms.cs | 24 +++++-
.../WhWzLibrary/PlayerListItem.axaml | 28 ++++++-
.../WhWzLibrary/PlayerListItem.axaml.cs | 73 +++++++++++++++----
WheelWizard/Views/Pages/LeaderboardPage.axaml | 3 +
.../Views/Pages/LeaderboardPage.axaml.cs | 46 +++++++++++-
.../Views/Pages/LeaderboardPlayerItem.cs | 2 +
WheelWizard/Views/Pages/RoomsPage.axaml | 2 +-
8 files changed, 156 insertions(+), 23 deletions(-)
diff --git a/WheelWizard/Models/RRInfo/RrPlayer.cs b/WheelWizard/Models/RRInfo/RrPlayer.cs
index cadd5c18..7a12bca0 100644
--- a/WheelWizard/Models/RRInfo/RrPlayer.cs
+++ b/WheelWizard/Models/RRInfo/RrPlayer.cs
@@ -14,6 +14,7 @@ public class RrPlayer : IEquatable
public bool IsOpenHost { get; set; }
public bool IsSuspended { get; set; }
+ public bool IsFriend { get; set; }
public int? LeaderboardRank { get; set; }
public bool IsTopLeaderboardPlayer => LeaderboardRank.HasValue;
diff --git a/WheelWizard/Services/LiveData/RRLiveRooms.cs b/WheelWizard/Services/LiveData/RRLiveRooms.cs
index 99aef535..e4594769 100644
--- a/WheelWizard/Services/LiveData/RRLiveRooms.cs
+++ b/WheelWizard/Services/LiveData/RRLiveRooms.cs
@@ -1,9 +1,11 @@
using WheelWizard.Models.RRInfo;
using WheelWizard.RrRooms;
+using WheelWizard.Utilities.Generators;
using WheelWizard.Utilities.RepeatedTasks;
using WheelWizard.Views;
using WheelWizard.WheelWizardData;
using WheelWizard.WiiManagement;
+using WheelWizard.WiiManagement.GameLicense;
using WheelWizard.WiiManagement.MiiManagement;
using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
@@ -26,6 +28,7 @@ protected override async Task ExecuteTaskAsync()
var whWzService = App.Services.GetRequiredService();
var roomsService = App.Services.GetRequiredService();
var leaderboardService = App.Services.GetRequiredService();
+ var gameLicenseService = App.Services.GetRequiredService();
var roomsTask = roomsService.GetRoomsAsync();
var leaderboardTask = leaderboardService.GetTopPlayersAsync(50);
@@ -59,7 +62,14 @@ protected override async Task ExecuteTaskAsync()
var raw = roomsResult.Value;
var splitRaw = SplitMergedRooms(raw);
- var rrRooms = splitRaw.Select(room => MapRoom(room, whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList();
+ var friendProfileIds = gameLicenseService
+ .ActiveCurrentFriends.Select(friend => FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode))
+ .Where(profileId => profileId != 0)
+ .ToHashSet();
+
+ var rrRooms = splitRaw
+ .Select(room => MapRoom(room, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds))
+ .ToList();
CurrentRooms = rrRooms;
}
@@ -68,7 +78,8 @@ private static RrRoom MapRoom(
RwfcRoomStatusRoom room,
IWhWzDataSingletonService whWzService,
IReadOnlyDictionary leaderboardByPid,
- IReadOnlyDictionary leaderboardByFriendCode
+ IReadOnlyDictionary leaderboardByFriendCode,
+ IReadOnlySet friendProfileIds
)
{
return new()
@@ -78,7 +89,9 @@ IReadOnlyDictionary leaderboardByFriendCode
Type = room.Type,
Suspend = room.Suspend,
Rk = room.Rk,
- Players = room.Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList(),
+ Players = room
+ .Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds))
+ .ToList(),
};
}
@@ -86,7 +99,8 @@ private static RrPlayer MapPlayer(
RwfcRoomStatusPlayer p,
IWhWzDataSingletonService whWzService,
IReadOnlyDictionary leaderboardByPid,
- IReadOnlyDictionary leaderboardByFriendCode
+ IReadOnlyDictionary leaderboardByFriendCode,
+ IReadOnlySet friendProfileIds
)
{
Mii? mii = null;
@@ -106,6 +120,7 @@ IReadOnlyDictionary leaderboardByFriendCode
}
var friendCode = p.FriendCode ?? string.Empty;
+ var profileId = FriendCodeGenerator.FriendCodeToProfileId(friendCode);
var leaderboardEntry = GetLeaderboardEntry(p, friendCode, leaderboardByPid, leaderboardByFriendCode);
@@ -122,6 +137,7 @@ IReadOnlyDictionary leaderboardByFriendCode
Mii = mii,
BadgeVariants = whWzService.GetBadges(friendCode),
LeaderboardRank = leaderboardEntry?.Rank ?? leaderboardEntry?.ActiveRank,
+ IsFriend = profileId != 0 && friendProfileIds.Contains(profileId),
};
}
diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml
index 9dbe492d..c463717a 100644
--- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml
+++ b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml
@@ -73,7 +73,7 @@
Margin="10,0,0,0" />
-
+
+
+
-
+
+
diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs
index 969ac2b6..8b26cc67 100644
--- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs
+++ b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs
@@ -1,6 +1,7 @@
-using Avalonia;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
using WheelWizard.WheelWizardData;
using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
@@ -8,6 +9,13 @@ namespace WheelWizard.Views.Components;
public class PlayerListItem : TemplatedControl
{
+ private Button? _joinRoomButton;
+
+ public static readonly StyledProperty IsOnlineProperty = AvaloniaProperty.Register(nameof(IsOnline));
+ public static readonly StyledProperty ShowJoinRoomButtonProperty = AvaloniaProperty.Register(
+ nameof(ShowJoinRoomButton)
+ );
+
public static readonly StyledProperty HasBadgesProperty = AvaloniaProperty.Register(nameof(HasBadges));
public static readonly StyledProperty IsOpenHostProperty = AvaloniaProperty.Register(nameof(IsOpenHost));
@@ -25,6 +33,18 @@ public bool HasBadges
set => SetValue(HasBadgesProperty, value);
}
+ public bool IsOnline
+ {
+ get => GetValue(IsOnlineProperty);
+ set => SetValue(IsOnlineProperty, value);
+ }
+
+ public bool ShowJoinRoomButton
+ {
+ get => GetValue(ShowJoinRoomButtonProperty);
+ set => SetValue(ShowJoinRoomButtonProperty, value);
+ }
+
public bool IsOpenHost
{
get => GetValue(IsOpenHostProperty);
@@ -85,23 +105,50 @@ public string UserName
set => SetValue(UserNameProperty, value);
}
+ public static readonly StyledProperty?> JoinRoomActionProperty = AvaloniaProperty.Register<
+ PlayerListItem,
+ Action?
+ >(nameof(JoinRoomAction));
+
+ public Action? JoinRoomAction
+ {
+ get => GetValue(JoinRoomActionProperty);
+ set => SetValue(JoinRoomActionProperty, value);
+ }
+
+ private void JoinRoom_OnClick(object? sender, RoutedEventArgs e)
+ {
+ if (string.IsNullOrWhiteSpace(FriendCode))
+ return;
+
+ JoinRoomAction?.Invoke(FriendCode);
+ }
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
- var container = e.NameScope.Find("PART_BadgeContainer");
- if (container == null)
- return;
- container.Children.Clear();
- var badges = App
- .Services.GetRequiredService()
- .GetBadges(FriendCode)
- .Select(variant => new Badge { Variant = variant });
- foreach (var badge in badges)
+ var container = e.NameScope.Find("PART_BadgeContainer");
+ if (container != null)
{
- badge.Height = 30;
- badge.Width = 30;
- container.Children.Add(badge);
+ container.Children.Clear();
+ var badges = App
+ .Services.GetRequiredService()
+ .GetBadges(FriendCode)
+ .Select(variant => new Badge { Variant = variant });
+ foreach (var badge in badges)
+ {
+ badge.Height = 30;
+ badge.Width = 30;
+ container.Children.Add(badge);
+ }
}
+
+ if (_joinRoomButton != null)
+ _joinRoomButton.Click -= JoinRoom_OnClick;
+
+ _joinRoomButton = e.NameScope.Find
-
\ No newline at end of file
+
From bf1eb9ee2bb3884940062c28c4c609d81372ee17 Mon Sep 17 00:00:00 2001
From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com>
Date: Sun, 22 Feb 2026 21:24:14 +0100
Subject: [PATCH 5/8] combine leaderboard Player Items
---
.../Views/Pages/LeaderboardPage.axaml.cs | 24 ++++++++++++++++
.../Views/Pages/LeaderboardPlayerItem.cs | 28 -------------------
2 files changed, 24 insertions(+), 28 deletions(-)
delete mode 100644 WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
index 3526a822..71b970e7 100644
--- a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
+++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
@@ -20,6 +20,30 @@
namespace WheelWizard.Views.Pages;
+public sealed record LeaderboardPlayerItem
+{
+ public required int Rank { get; init; }
+ public required string PlacementLabel { get; init; }
+ public required string Name { get; init; }
+ public required string FriendCode { get; init; }
+ public required string VrText { get; init; }
+ public Mii? Mii { get; init; }
+ public BadgeVariant PrimaryBadge { get; init; }
+ public bool HasBadge { get; init; }
+ public bool IsSuspicious { get; init; }
+ public bool IsEvenRow { get; init; }
+ public bool IsFriend { get; init; }
+ public bool IsOnline { get; init; }
+
+ // Keep parity with RoomDetailsPage player template bindings.
+ public string VrDisplay => VrText;
+ public Mii? FirstMii => Mii;
+ public bool HasBadges => HasBadge;
+ public bool IsTopLeaderboardPlayer => true;
+ public string TopLabel => $"#{Rank}";
+ public bool IsOpenHost => false;
+}
+
public partial class LeaderboardPage : UserControlBase, INotifyPropertyChanged
{
private static readonly LeaderboardPlayerItem EmptyPodiumPlayer = new()
diff --git a/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs b/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
deleted file mode 100644
index 83bcb513..00000000
--- a/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using WheelWizard.WheelWizardData.Domain;
-using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
-
-namespace WheelWizard.Views.Pages;
-
-public sealed class LeaderboardPlayerItem
-{
- public required int Rank { get; init; }
- public required string PlacementLabel { get; init; }
- public required string Name { get; init; }
- public required string FriendCode { get; init; }
- public required string VrText { get; init; }
- public Mii? Mii { get; init; }
- public BadgeVariant PrimaryBadge { get; init; }
- public bool HasBadge { get; init; }
- public bool IsSuspicious { get; init; }
- public bool IsEvenRow { get; init; }
- public bool IsFriend { get; init; }
- public bool IsOnline { get; init; }
-
- // Keep parity with RoomDetailsPage player template bindings.
- public string VrDisplay => VrText;
- public Mii? FirstMii => Mii;
- public bool HasBadges => HasBadge;
- public bool IsTopLeaderboardPlayer => true;
- public string TopLabel => $"#{Rank}";
- public bool IsOpenHost => false;
-}
From 7d7931d330b9eb2b699a7ed5a02b3858b070bee0 Mon Sep 17 00:00:00 2001
From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com>
Date: Sun, 22 Feb 2026 22:18:42 +0100
Subject: [PATCH 6/8] make cache based on DI cache
---
.../RrRooms/RrLeaderboardSingletonService.cs | 89 +++++++------------
1 file changed, 30 insertions(+), 59 deletions(-)
diff --git a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
index 54dac2c9..b2458e15 100644
--- a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
+++ b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using WheelWizard.Shared.Services;
@@ -8,85 +9,55 @@ public interface IRrLeaderboardSingletonService
Task>> GetTopPlayersAsync(int limit = 50);
}
-public class RrLeaderboardSingletonService(IApiCaller apiCaller, ILogger logger)
- : IRrLeaderboardSingletonService
+public class RrLeaderboardSingletonService(
+ IApiCaller apiCaller,
+ IMemoryCache cache,
+ ILogger logger
+) : IRrLeaderboardSingletonService
{
+ private const string FreshCacheKey = "rrrooms:leaderboard:fresh";
+ private const string StaleCacheKey = "rrrooms:leaderboard:stale";
private static readonly TimeSpan CacheLifetime = TimeSpan.FromSeconds(90);
- private readonly SemaphoreSlim _refreshGate = new(1, 1);
- private readonly object _cacheLock = new();
-
- private DateTimeOffset _cacheFetchedAt = DateTimeOffset.MinValue;
- private List _cachedEntries = [];
public async Task>> GetTopPlayersAsync(int limit = 50)
{
var boundedLimit = Math.Clamp(limit, 1, 200);
- if (TryGetFreshCache(boundedLimit, out var freshCache))
+ if (TryGetCached(FreshCacheKey, boundedLimit, out var freshCache))
return freshCache;
- await _refreshGate.WaitAsync();
-
- try
+ var fetchResult = await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));
+ if (fetchResult.IsFailure)
{
- if (TryGetFreshCache(boundedLimit, out freshCache))
- return freshCache;
-
- var fetchResult = await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));
- if (fetchResult.IsFailure)
- {
- if (TryGetAnyCache(boundedLimit, out var staleCache))
- {
- logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit);
- return staleCache;
- }
-
+ if (!TryGetCached(StaleCacheKey, boundedLimit, out var staleCache))
return fetchResult;
- }
+
+ logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit);
+ return staleCache;
- lock (_cacheLock)
- {
- _cachedEntries = fetchResult.Value.ToList();
- _cacheFetchedAt = DateTimeOffset.UtcNow;
- }
-
- return TrimToLimit(fetchResult.Value, boundedLimit);
- }
- finally
- {
- _refreshGate.Release();
}
- }
- private bool TryGetFreshCache(int limit, out OperationResult> result)
- {
- lock (_cacheLock)
- {
- var hasFreshCache = DateTimeOffset.UtcNow - _cacheFetchedAt <= CacheLifetime;
- if (!hasFreshCache || _cachedEntries.Count < limit)
- {
- result = default!;
- return false;
- }
+ var fetchedEntries = fetchResult.Value.ToList();
- result = TrimToLimit(_cachedEntries, limit);
- return true;
- }
+ cache.Set(FreshCacheKey, fetchedEntries, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = CacheLifetime });
+ cache.Set(StaleCacheKey, fetchedEntries);
+
+ return TrimToLimit(fetchedEntries, boundedLimit);
}
- private bool TryGetAnyCache(int limit, out OperationResult> result)
+ private bool TryGetCached(string cacheKey, int limit, out OperationResult> result)
{
- lock (_cacheLock)
+ if (
+ !cache.TryGetValue(cacheKey, out List? cachedEntries)
+ || cachedEntries == null || cachedEntries.Count < limit
+ )
{
- if (_cachedEntries.Count < limit)
- {
- result = default!;
- return false;
- }
-
- result = TrimToLimit(_cachedEntries, limit);
- return true;
+ result = default!;
+ return false;
}
+
+ result = TrimToLimit(cachedEntries, limit);
+ return true;
}
private static OperationResult> TrimToLimit(IEnumerable entries, int limit)
From 9509bd9fefd9f7a98a656dea0776b7eb57a89e98 Mon Sep 17 00:00:00 2001
From: patchzyy <64382339+patchzyy@users.noreply.github.com>
Date: Mon, 23 Feb 2026 14:47:22 +0100
Subject: [PATCH 7/8] Localize 'Leaderboard' page title
---
WheelWizard/Resources/Languages/Common.Designer.cs | 9 +++++++++
WheelWizard/Resources/Languages/Common.resx | 3 +++
WheelWizard/Views/Layout.axaml | 2 +-
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/WheelWizard/Resources/Languages/Common.Designer.cs b/WheelWizard/Resources/Languages/Common.Designer.cs
index 47190ee5..b11771e9 100644
--- a/WheelWizard/Resources/Languages/Common.Designer.cs
+++ b/WheelWizard/Resources/Languages/Common.Designer.cs
@@ -1165,6 +1165,15 @@ public static string PageTitle_Friends {
return ResourceManager.GetString("PageTitle_Friends", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Leaderboard.
+ ///
+ public static string PageTitle_Leaderboard {
+ get {
+ return ResourceManager.GetString("PageTitle_Leaderboard", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Home.
diff --git a/WheelWizard/Resources/Languages/Common.resx b/WheelWizard/Resources/Languages/Common.resx
index 32b1de77..4a0c130c 100644
--- a/WheelWizard/Resources/Languages/Common.resx
+++ b/WheelWizard/Resources/Languages/Common.resx
@@ -41,6 +41,9 @@
Friends
+
+ Leaderboard
+
Online
diff --git a/WheelWizard/Views/Layout.axaml b/WheelWizard/Views/Layout.axaml
index c91c38ee..97b94519 100644
--- a/WheelWizard/Views/Layout.axaml
+++ b/WheelWizard/Views/Layout.axaml
@@ -136,7 +136,7 @@
Text="{x:Static lang:Common.PageTitle_Rooms}" x:Name="RoomsButton" />
+ Text="{x:Static lang:Common.PageTitle_Leaderboard}" />
Date: Mon, 23 Feb 2026 15:18:41 +0100
Subject: [PATCH 8/8] Change to use new settingsmanager
---
.../Features/RrRooms/RrLeaderboardSingletonService.cs | 6 +++---
WheelWizard/Services/LiveData/RRLiveRooms.cs | 7 +++++--
WheelWizard/Views/Pages/LeaderboardPage.axaml.cs | 7 +++++--
3 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
index b2458e15..72576dce 100644
--- a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
+++ b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
@@ -31,10 +31,9 @@ public async Task>> GetTopPlayersAsyn
{
if (!TryGetCached(StaleCacheKey, boundedLimit, out var staleCache))
return fetchResult;
-
+
logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit);
return staleCache;
-
}
var fetchedEntries = fetchResult.Value.ToList();
@@ -49,7 +48,8 @@ private bool TryGetCached(string cacheKey, int limit, out OperationResult? cachedEntries)
- || cachedEntries == null || cachedEntries.Count < limit
+ || cachedEntries == null
+ || cachedEntries.Count < limit
)
{
result = default!;
diff --git a/WheelWizard/Services/LiveData/RRLiveRooms.cs b/WheelWizard/Services/LiveData/RRLiveRooms.cs
index 6050243b..bc33368d 100644
--- a/WheelWizard/Services/LiveData/RRLiveRooms.cs
+++ b/WheelWizard/Services/LiveData/RRLiveRooms.cs
@@ -16,6 +16,7 @@ public class RRLiveRooms : RepeatedTaskManager
private readonly IWhWzDataSingletonService _whWzService;
private readonly IRrRoomsSingletonService _roomsService;
private readonly IRrLeaderboardSingletonService _leaderboardService;
+ private readonly IGameLicenseSingletonService _gameLicenseService;
public List CurrentRooms { get; private set; } = [];
public int PlayerCount => CurrentRooms.Sum(room => room.PlayerCount);
@@ -26,13 +27,15 @@ public class RRLiveRooms : RepeatedTaskManager
public RRLiveRooms(
IWhWzDataSingletonService whWzService,
IRrRoomsSingletonService roomsService,
- IRrLeaderboardSingletonService leaderboardService
+ IRrLeaderboardSingletonService leaderboardService,
+ IGameLicenseSingletonService gameLicenseService
)
: base(40)
{
_whWzService = whWzService;
_roomsService = roomsService;
_leaderboardService = leaderboardService;
+ _gameLicenseService = gameLicenseService;
}
protected override async Task ExecuteTaskAsync()
@@ -69,7 +72,7 @@ protected override async Task ExecuteTaskAsync()
var raw = roomsResult.Value;
var splitRaw = SplitMergedRooms(raw);
- var friendProfileIds = gameLicenseService
+ var friendProfileIds = _gameLicenseService
.ActiveCurrentFriends.Select(friend => FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode))
.Where(profileId => profileId != 0)
.ToHashSet();
diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
index 71b970e7..acef0734 100644
--- a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
+++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs
@@ -7,7 +7,7 @@
using WheelWizard.Resources.Languages;
using WheelWizard.RrRooms;
using WheelWizard.Services.LiveData;
-using WheelWizard.Services.Settings;
+using WheelWizard.Settings;
using WheelWizard.Shared.DependencyInjection;
using WheelWizard.Utilities.Generators;
using WheelWizard.Views.Popups;
@@ -70,6 +70,9 @@ public partial class LeaderboardPage : UserControlBase, INotifyPropertyChanged
[Inject]
private IGameLicenseSingletonService GameDataService { get; set; } = null!;
+ [Inject]
+ private ISettingsManager SettingsManager { get; set; } = null!;
+
private bool _hasLoadedOnce;
private bool _isLoading;
private bool _hasError;
@@ -463,7 +466,7 @@ private async void AddFriend_OnClick(object sender, RoutedEventArgs e)
return;
}
- var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get();
+ var focusedUserIndex = SettingsManager.Get(SettingsManager.FOCUSED_USER);
if (focusedUserIndex is < 0 or > 3)
{
ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning);