feature: UI: LDN Games Viewer

This window can be accessed via "Help" menu in the title bar.
This menu's data is synced with the in-app-list LDN game data, and that has been modified to hide unjoinable games (in-progress and/or private (needing a passphrase)). You can still see these games in the list.
This commit is contained in:
GreemDev
2025-08-30 19:54:00 -05:00
parent da340f5615
commit 6e47d8548c
28 changed files with 1574 additions and 132 deletions

View File

@@ -51,7 +51,7 @@ namespace Ryujinx.Ava.UI.Windows
if (DataContext is not CompatibilityViewModel cvm)
return;
cvm.NameSorting(int.Parse(sortStrategy));
cvm.NameSorting(int.Parse(sortStrategy));
}
}
@@ -65,6 +65,5 @@ namespace Ryujinx.Ava.UI.Windows
cvm.StatusSorting(int.Parse(sortStrategy));
}
}
}
}

View File

@@ -0,0 +1,349 @@
<window:StyleableAppWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:facontrols="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
CanResize="False"
mc:Ignorable="d"
MinWidth="800"
MinHeight="745"
x:Class="Ryujinx.Ava.UI.Windows.LdnGamesListWindow"
x:DataType="viewModels:LdnGamesListViewModel">
<window:StyleableAppWindow.DataContext>
<viewModels:LdnGamesListViewModel />
</window:StyleableAppWindow.DataContext>
<Grid RowDefinitions="Auto,*">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo
Grid.Column="0"
Margin="15, 0, 7, 0"
ToolTip.Tip="{ext:WindowTitle LdnGameListTitle, False}"/>
<TextBox Grid.Column="1"
Name="SearchBoxFlush"
Margin="0, 5, 0, 5"
HorizontalAlignment="Stretch"
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}"
TextChanged="TextBox_OnTextChanged"/>
<Button
Grid.Column="2"
Name="InfoFlush"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{ext:Icon fa-solid fa-info}"
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
<Button
Grid.Column="3"
Name="RefreshFlush"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsEnabled="{Binding !IsRefreshing}"
ToolTip.Tip="{ext:Locale LdnGameListRefreshToolTip}">
<facontrols:SymbolIcon Symbol="Refresh" />
</Button>
<StackPanel Grid.Column="4" Orientation="Horizontal" Margin="10, 5, 0, 5">
<DropDownButton
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale CommonSort}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="True"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDisable}"
GroupName="Order"
IsChecked="true"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<DropDownButton
Grid.Column="5"
Margin="10, 0, 148, 0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="Filters"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
<TextBlock Text="{ext:Locale LdnGameListOnlyShowPublicGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
<TextBlock Text="{ext:Locale LdnGameListOnlyShowJoinableGames}" />
</CheckBox>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</Grid>
<!-- UI NormalControls -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Name="NormalControls">
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="20, 5, 0, 5" HorizontalAlignment="Stretch"
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<Button
Grid.Column="1"
Name="InfoNormal"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{ext:Icon fa-solid fa-info}"
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
<Button Grid.Column="2" Name="RefreshNormal" Margin="10, 5, 0, 5" MinWidth="32" MinHeight="32" ClipToBounds="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsEnabled="{Binding !IsRefreshing}">
<facontrols:SymbolIcon Symbol="Refresh" />
</Button>
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="10, 5, 0, 5">
<DropDownButton
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale CommonSort}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="True"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDisable}"
GroupName="Order"
IsChecked="true"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<DropDownButton
Grid.Column="4"
Margin="10, 5, 20, 5"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="Filters"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
<TextBlock Text="{ext:Locale LdnGameListOnlyShowPublicGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
<TextBlock Text="{ext:Locale LdnGameListOnlyShowJoinableGames}" />
</CheckBox>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</Grid>
<!-- List of open LDN games -->
<ScrollViewer Grid.Row="1">
<ListBox Margin="12, 0, 13, 0"
Background="Transparent"
ItemsSource="{Binding VisibleEntries}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
ItemHeight="125"
ItemWidth="450"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:LdnGameModel}">
<Border
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="4"
Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto,*" RowDefinitions="Auto,Auto,Auto,*" Width="420" Height="110" HorizontalAlignment="Center">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{Binding Title.Name}"
Margin="7, 0,0, 0"
Width="250"
ToolTip.Tip="{Binding Title.Id}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left"
TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{Binding Title.Version}"
Margin="7, 0,0, 0"
Width="250"
ToolTip.Tip="{Binding Title.Id}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{Binding FormattedCreatedAt}"
ToolTip.Tip="{Binding CreatedAtToolTip}"
Margin="7, 0,0, 0"
Width="250"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding IsJoinable}"
Text="{ext:Locale LdnGameListJoinable}"
ToolTip.Tip="{ext:Locale LdnGameListJoinableToolTip}"
Foreground="MediumSeaGreen"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="0" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding !IsJoinable}"
Text="{ext:Locale LdnGameListNotJoinable}"
ToolTip.Tip="{ext:Locale LdnGameListNotJoinableToolTip}"
Foreground="IndianRed"
TextDecorations="Underline"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding IsPublic}"
Text="{ext:Locale LdnGameListPublic}"
ToolTip.Tip="{ext:Locale LdnGameListPublicToolTip}"
Foreground="LawnGreen"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding !IsPublic}"
Text="{ext:Locale LdnGameListPrivate}"
ToolTip.Tip="{ext:Locale LdnGameListPrivateToolTip}"
Foreground="DarkRed"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="2"
Margin="0, 0,7, 0"
Text="{Binding ConnectionTypeLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
ToolTip.Tip="{Binding ConnectionTypeToolTipLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
VerticalAlignment="Center"
HorizontalAlignment="Right"
TextWrapping="NoWrap"
ClipToBounds="False"
TextAlignment="Right" />
<StackPanel Margin="7, 0,0, 0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding PlayersLabel}" TextAlignment="Left" />
<TextBlock Text="{Binding FormattedPlayers}" TextAlignment="Center" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</window:StyleableAppWindow>

View File

@@ -0,0 +1,79 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
{
public partial class LdnGamesListWindow : StyleableAppWindow
{
public static async Task Show(string searchTerm = null)
{
using LdnGamesListViewModel ldnGamesListVm = new(RyujinxApp.MainWindow.ViewModel);
await ShowAsync(new LdnGamesListWindow
{
DataContext = ldnGamesListVm,
SearchBoxFlush = { Text = searchTerm ?? string.Empty },
SearchBoxNormal = { Text = searchTerm ?? string.Empty }
});
}
public LdnGamesListWindow() : base(useCustomTitleBar: true, 37)
{
Title = RyujinxApp.FormatTitle(LocaleKeys.LdnGameListTitle);
InitializeComponent();
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
NormalControls.IsVisible = ConfigurationState.Instance.ShowOldUI;
RefreshFlush.Command = RefreshNormal.Command =
Commands.Create(() => (DataContext as LdnGamesListViewModel)?.RefreshAsync().OrCompleted());
InfoFlush.Command = InfoNormal.Command =
Commands.Create(() => OpenHelper.OpenUrl(SharedConstants.MultiplayerWikiUrl));
}
// ReSharper disable once UnusedMember.Local
// its referenced in the axaml but rider keeps yelling at me that its unused so
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
if (sender is not TextBox searchBox)
return;
cvm.Search(searchBox.Text);
}
public void Sort_Name_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
cvm.NameSorting(int.Parse(sortStrategy));
}
}
public void Sort_PlayerCount_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
cvm.StatusSorting(int.Parse(sortStrategy));
}
}
}
}

View File

@@ -8,7 +8,6 @@ using Avalonia.Threading;
using DynamicData;
using FluentAvalonia.UI.Controls;
using Gommon;
using LibHac.Ns;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
@@ -18,6 +17,7 @@ using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.UI;
using Ryujinx.Ava.UI.Applet;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
@@ -185,12 +185,11 @@ namespace Ryujinx.Ava.UI.Windows
{
Dispatcher.UIThread.Post(() =>
{
ViewModel.LdnData.Clear();
ViewModel.LdnModels = e.LdnData;
ViewModel.UsableLdnData.Clear();
foreach (ApplicationData application in ViewModel.Applications.Where(it => it.HasControlHolder))
{
ref ApplicationControlProperty controlHolder = ref application.ControlHolder.Value;
ViewModel.LdnData[application.IdString] = e.LdnData.Where(ref controlHolder);
ViewModel.UsableLdnData[application.IdString] = LdnGameModel.GetArrayForApp(e.LdnData, ref application.ControlHolder.Value);
UpdateApplicationWithLdnData(application);
}
@@ -201,7 +200,7 @@ namespace Ryujinx.Ava.UI.Windows
private void UpdateApplicationWithLdnData(ApplicationData application)
{
if (application.HasControlHolder && ViewModel.LdnData.TryGetValue(application.IdString, out LdnGameData.Array ldnGameDatas))
if (application.HasControlHolder && ViewModel.UsableLdnData.TryGetValue(application.IdString, out LdnGameModel.Array ldnGameDatas))
{
application.PlayerCount = ldnGameDatas.PlayerCount;
application.GameCount = ldnGameDatas.GameCount;