Compare commits

...

6 Commits

Author SHA1 Message Date
Evan Husted
1972a47f39 UI: Game stats button on right click for Grid view users 2025-02-04 19:32:17 -06:00
Evan Husted
222ceb818b misc: chore: Use ApplicationLibrary helpers for getting DLCs & Updates for a game 2025-02-04 18:21:49 -06:00
Evan Husted
b0fcc5bee1 misc: chore: Simplify HasCompatibilityEntry
(Totally didn't realize that SelectedApplication is already an ApplicationData)
2025-02-04 18:21:24 -06:00
Evan Husted
820e8f7375 [ci skip] UI: Strip dumped file information out of the DLC name 2025-02-04 18:10:28 -06:00
Evan Husted
e8a7d5b0b7 UI: Only show DLC RomFS button under Extract Data when DLCs are available.
Also convert the constructor of DlcSelectViewModel to expect a normal title id and not one already converted to the base ID.
2025-02-04 17:21:54 -06:00
Evan Husted
fafb99c702 misc: chore: [ci skip] don't even bother looking up the application; the tag present on the control *is* a valid title ID and can't reasonably change in between the tag being set and playability information being requested.
Even if it does, worst case scenario the compat list that pops up has no results.
2025-02-04 15:57:32 -06:00
12 changed files with 307 additions and 33 deletions

View File

@@ -2572,6 +2572,56 @@
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameData",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Game Stats",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameDataToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuOpenModsDirectory",
"Translations": {

View File

@@ -32,6 +32,9 @@ namespace Ryujinx.Ava
public static MainWindow MainWindow => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
.MainWindow.Cast<MainWindow>();
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
public static bool IsClipboardAvailable(out IClipboard clipboard)
{

View File

@@ -25,6 +25,12 @@
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
<MenuItem
IsVisible="{Binding IsGrid}"
Click="OpenApplicationData_Click"
Header="{ext:Locale GameListContextMenuShowGameData}"
Icon="{ext:Icon mdi-chart-line}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
<Separator />
<MenuItem
Click="OpenUserSaveDirectory_Click"
@@ -117,6 +123,7 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
IsVisible="{Binding HasDlc}"
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />

View File

@@ -334,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
@@ -392,6 +392,12 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
}
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await ApplicationDataView.Show(viewModel.SelectedApplication);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args)
{
@@ -401,12 +407,8 @@ namespace Ryujinx.Ava.UI.Controls
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@@ -0,0 +1,116 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:appLibrary="using:Ryujinx.Ava.Utilities.AppLibrary"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
x:DataType="appLibrary:ApplicationData">
<StackPanel Orientation="Vertical">
<Grid
RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
HorizontalAlignment="Center"
Padding="0, -2, 0, 5"
Classes="h1"
Text="{Binding Name}" />
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock Grid.Row="2"
HorizontalAlignment="Center"
Text="{Binding Developer}"
TextAlignment="Start"
TextWrapping="Wrap" />
<StackPanel Grid.ColumnSpan="2" Grid.Row="3"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0 10, 0, 0"
Spacing="5">
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
<Button
Click="IdString_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
HorizontalAlignment="Stretch"
Text="{Binding IdString}"
TextAlignment="Start"
TextWrapping="Wrap" />
</Button>
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" />
<Grid ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Column="2"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileSizeString}"
TextAlignment="End"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,84 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class ApplicationDataView : UserControl
{
public static async Task Show(ApplicationData appData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new ApplicationDataView { DataContext = appData }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Center));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
}
public ApplicationDataView()
{
InitializeComponent();
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
window.Close(ContentDialogResult.None);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock idText })
return;
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
return;
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
if (appData is null)
return;
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}
}
}

View File

@@ -39,13 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (!ulong.TryParse((string)playabilityLabel.Tag, NumberStyles.HexNumber, null, out ulong titleId))
return;
if (!mwvm.ApplicationLibrary.FindApplication(titleId, out ApplicationData appData))
return;
await CompatibilityList.Show(appData.IdString);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)

View File

@@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
_dlcs = appLibrary.FindDlcsFor(titleId)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();

View File

@@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents()
{
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
bool hasBundledContent = false;
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)

View File

@@ -349,16 +349,9 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool HasCompatibilityEntry
{
get
{
DynamicData.Kernel.Optional<ApplicationData> appData =
ApplicationLibrary.Applications.Lookup(SelectedApplication.Id);
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
return appData.HasValue && appData.Value.HasPlayabilityInfo;
}
}
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;

View File

@@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates()
{
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
bool hasBundledContent = false;
SelectedUpdate = new TitleUpdateViewModelNoUpdate();

View File

@@ -128,11 +128,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
if (appData.HasValue)
return appData.Value.Name;
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return Path.GetFileNameWithoutExtension(dlcData.FileName);
return id.ToString("X16");
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return id.ToString("X16");
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
int idx = name.IndexOf('[');
if (idx != -1)
name = name[..idx];
return name;
}
public bool FindApplication(ulong id, out ApplicationData foundData)
@@ -142,6 +147,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
return appData.HasValue;
}
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
{
Gommon.Optional<TitleUpdateModel> appData =
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public TitleUpdateModel[] FindUpdatesFor(ulong id)
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public DownloadableContentModel[] FindDlcsFor(ulong id)
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public bool HasDlcs(ulong id)
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>