From df9e6e481211d8e064bb8c7d86cef40e6fb77114 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 15:51:27 -0600 Subject: [PATCH 01/13] UI: Added the ability to view Compat information on right click, and on clicking the status itself like the title ID button. --- src/Ryujinx/Assets/locales.json | 50 +++++++++++++++++++ .../UI/Controls/ApplicationContextMenu.axaml | 7 ++- .../Controls/ApplicationContextMenu.axaml.cs | 7 +++ .../UI/Controls/ApplicationListView.axaml | 28 ++++++++--- .../UI/Controls/ApplicationListView.axaml.cs | 19 +++++++ .../UI/ViewModels/MainWindowViewModel.cs | 11 ++++ .../UI/Views/Main/MainMenuBarView.axaml.cs | 2 +- .../AppLibrary/ApplicationLibrary.cs | 8 +++ .../Utilities/Compat/CompatibilityCsv.cs | 25 ++++------ .../Utilities/Compat/CompatibilityList.axaml | 2 +- .../Compat/CompatibilityList.axaml.cs | 7 ++- 11 files changed, 141 insertions(+), 25 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 18842ce72..3da0b1728 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2522,6 +2522,56 @@ "zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式" } }, + { + "ID": "GameListContextMenuShowCompatEntry", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show Compatibility Entry", + "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": "GameListContextMenuShowCompatEntryToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.", + "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": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 475b26787..797bc27e0 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -19,6 +19,12 @@ Header="{ext:Locale GameListContextMenuCreateShortcut}" Icon="{ext:Icon fa-solid fa-bookmark}" ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> + - - + Background="{DynamicResource AppListBackgroundColor}" + Margin="-1, 0, 0, 0" + Padding="0" > + + + + + appData = + ApplicationLibrary.Applications.Lookup(SelectedApplication.Id); + + return appData.HasValue && appData.Value.HasPlayabilityInfo; + } + } + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index f6c43aade..a0bcd1aa2 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes); XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show); AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show); - CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show); + CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show()); UpdateMenuItem.Command = Commands.Create(async () => { diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index dec265623..ee86a4a33 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -135,6 +135,14 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return id.ToString("X16"); } + public bool FindApplication(ulong id, out ApplicationData foundData) + { + DynamicData.Kernel.Optional appData = Applications.Lookup(id); + foundData = appData.HasValue ? appData.Value : null; + + return appData.HasValue; + } + /// The configured key set is missing a key. /// The NCA header could not be decrypted. /// The NCA version is not supported. diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs index d0e251fe0..c3fcf99ca 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs @@ -113,20 +113,17 @@ namespace Ryujinx.Ava.Utilities.Compat .Select(FormatLabelName) .JoinToString(", "); - public override string ToString() - { - StringBuilder sb = new("CompatibilityEntry: {"); - sb.Append($"{nameof(GameName)}=\"{GameName}\", "); - sb.Append($"{nameof(TitleId)}={TitleId}, "); - sb.Append($"{nameof(Labels)}={ - Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]") - }, "); - sb.Append($"{nameof(Status)}=\"{Status}\", "); - sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\""); - sb.Append('}'); - - return sb.ToString(); - } + public override string ToString() => + new StringBuilder("CompatibilityEntry: {") + .Append($"{nameof(GameName)}=\"{GameName}\", ") + .Append($"{nameof(TitleId)}={TitleId}, ") + .Append($"{nameof(Labels)}={ + Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]") + }, ") + .Append($"{nameof(Status)}=\"{Status}\", ") + .Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"") + .Append('}') + .ToString(); public static string FormatLabelName(string labelName) => labelName.ToLower() switch { diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml index 73ec84c53..132b10e26 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml @@ -34,7 +34,7 @@ Text="{ext:Locale CompatibilityListWarning}" /> - + diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs index e0d3b0c56..30d2649bc 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs @@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat { public partial class CompatibilityList : UserControl { - public static async Task Show() + public static async Task Show(string titleId = null) { ContentDialog contentDialog = new() { @@ -18,7 +18,10 @@ namespace Ryujinx.Ava.Utilities.Compat CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], Content = new CompatibilityList { - DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary) + DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary), + SearchBox = { + Text = titleId ?? "" + } } }; From fafb99c702a83a294838359535100f5883b86822 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 15:57:32 -0600 Subject: [PATCH 02/13] 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. --- src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs index 95fc911d0..7c6b0cf15 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -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) From e8a7d5b0b74d1d5ad2ba09cc7c07f6ba333972d3 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 17:21:54 -0600 Subject: [PATCH 03/13] 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. --- .../UI/Controls/ApplicationContextMenu.axaml | 1 + .../Controls/ApplicationContextMenu.axaml.cs | 2 +- .../UI/ViewModels/DlcSelectViewModel.cs | 4 +--- .../UI/ViewModels/MainWindowViewModel.cs | 2 ++ .../Utilities/AppLibrary/ApplicationLibrary.cs | 18 ++++++++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 797bc27e0..2804485fe 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -117,6 +117,7 @@ Header="{ext:Locale GameListContextMenuExtractDataRomFS}" ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index f29f70432..0d81484ba 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -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) { diff --git a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs index d50d8249a..b486aa766 100644 --- a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs @@ -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(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f0e05d517..632e3b4f0 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -360,6 +360,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id); + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index ee86a4a33..75737c3e5 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -142,6 +142,24 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return appData.HasValue; } + + public bool FindUpdate(ulong id, out TitleUpdateModel foundData) + { + Gommon.Optional 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 DownloadableContentModel[] FindDlcsFor(ulong id) + => DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + + public bool HasDlcs(ulong id) + => DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL)); /// The configured key set is missing a key. /// The NCA header could not be decrypted. From 820e8f73750b7348f73b69f38b033ccb8d87adff Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:10:28 -0600 Subject: [PATCH 04/13] [ci skip] UI: Strip dumped file information out of the DLC name --- .../Utilities/AppLibrary/ApplicationLibrary.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index 75737c3e5..9571394fe 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -128,11 +128,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary DynamicData.Kernel.Optional 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) From b0fcc5bee1674c075c125ccb65773db8b3c466fe Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:21:24 -0600 Subject: [PATCH 05/13] misc: chore: Simplify HasCompatibilityEntry (Totally didn't realize that SelectedApplication is already an ApplicationData) --- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 632e3b4f0..d7a09a0e3 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -349,16 +349,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool HasCompatibilityEntry - { - get - { - DynamicData.Kernel.Optional appData = - ApplicationLibrary.Applications.Lookup(SelectedApplication.Id); - - return appData.HasValue && appData.Value.HasPlayabilityInfo; - } - } + public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo; public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id); From 222ceb818b9f5c49f854762a9b544b349a9a6ea0 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 18:21:49 -0600 Subject: [PATCH 06/13] misc: chore: Use ApplicationLibrary helpers for getting DLCs & Updates for a game --- .../UI/ViewModels/DownloadableContentManagerViewModel.cs | 3 +-- src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs | 3 +-- src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 1533b7d5d..a16a06ff5 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -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) diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index aaafc3913..2b88aceed 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -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(); diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index 9571394fe..79cac1a0e 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -160,9 +160,15 @@ namespace Ryujinx.Ava.Utilities.AppLibrary 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)); From 1972a47f39014e8df9bcfcf3a61ca7d27eaaa030 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 19:32:17 -0600 Subject: [PATCH 07/13] UI: Game stats button on right click for Grid view users --- src/Ryujinx/Assets/locales.json | 50 ++++++++ src/Ryujinx/RyujinxApp.axaml.cs | 3 + .../UI/Controls/ApplicationContextMenu.axaml | 6 + .../Controls/ApplicationContextMenu.axaml.cs | 12 +- .../UI/Controls/ApplicationDataView.axaml | 116 ++++++++++++++++++ .../UI/Controls/ApplicationDataView.axaml.cs | 84 +++++++++++++ 6 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 src/Ryujinx/UI/Controls/ApplicationDataView.axaml create mode 100644 src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 3da0b1728..9f9053ae0 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -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": { diff --git a/src/Ryujinx/RyujinxApp.axaml.cs b/src/Ryujinx/RyujinxApp.axaml.cs index be24315f6..32318776a 100644 --- a/src/Ryujinx/RyujinxApp.axaml.cs +++ b/src/Ryujinx/RyujinxApp.axaml.cs @@ -32,6 +32,9 @@ namespace Ryujinx.Ava public static MainWindow MainWindow => Current! .ApplicationLifetime.Cast() .MainWindow.Cast(); + + public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current! + .ApplicationLifetime.Cast(); public static bool IsClipboardAvailable(out IClipboard clipboard) { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 2804485fe..acade1df9 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -25,6 +25,12 @@ Header="{ext:Locale GameListContextMenuShowCompatEntry}" Icon="{ext:Icon mdi-gamepad}" ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs new file mode 100644 index 000000000..0bd22a243 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -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})"); + } + } +} + From bd08a111a8829a3e958f606890b2ad4b4642fc6b Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 22:47:12 -0600 Subject: [PATCH 08/13] UI: Show what each value is in the Game Info dialog, add game icon --- src/Ryujinx/Assets/locales.json | 6 +- .../UI/Controls/ApplicationContextMenu.axaml | 1 - .../UI/Controls/ApplicationDataView.axaml | 212 +++++++++--------- .../UI/Controls/ApplicationDataView.axaml.cs | 3 +- .../UI/Controls/ApplicationListView.axaml | 1 + .../Converters/MultiplayerInfoConverter.cs | 7 +- .../UI/ViewModels/ApplicationDataViewModel.cs | 34 +++ .../Utilities/AppLibrary/ApplicationData.cs | 3 + 8 files changed, 155 insertions(+), 112 deletions(-) create mode 100644 src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 9f9053ae0..d597e8a4c 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2578,7 +2578,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Show Game Stats", + "en_US": "Show Game Info", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -2603,7 +2603,7 @@ "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.", + "en_US": "Show stats & details about the currently selected game.", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -23298,4 +23298,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index acade1df9..3e47a1910 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -26,7 +26,6 @@ Icon="{ext:Icon mdi-gamepad}" ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs index 0bd22a243..cc8091d4d 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -26,7 +26,8 @@ namespace Ryujinx.Ava.UI.Controls PrimaryButtonText = string.Empty, SecondaryButtonText = string.Empty, CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], - Content = new ApplicationDataView { DataContext = appData } + MinWidth = 256, + Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) } }; Style closeButton = new(x => x.Name("CloseButton")); diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 151bf5b32..c01c4e8be 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -140,6 +140,7 @@ TextWrapping="Wrap" /> diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index 47d0b94d0..dc36098a1 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -12,12 +12,9 @@ namespace Ryujinx.Ava.UI.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is ApplicationData applicationData) + if (value is ApplicationData { HasLdnGames: true } applicationData) { - if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) - { - return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; - } + return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; } return ""; diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs new file mode 100644 index 000000000..73d555389 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs @@ -0,0 +1,34 @@ +using Gommon; +using Ryujinx.Ava.Utilities.AppLibrary; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ApplicationDataViewModel : BaseModel + { + private const string FormatVersion = "Current Version: {0}"; + private const string FormatDeveloper = "Developed by {0}"; + + private const string FormatExtension = "Game type: {0}"; + private const string FormatLastPlayed = "Last played: {0}"; + private const string FormatPlayTime = "Play time: {0}"; + private const string FormatSize = "Size: {0}"; + + private const string FormatHostedGames = "Hosted Games: {0}"; + private const string FormatPlayerCount = "Online Players: {0}"; + + public ApplicationData AppData { get; } + + public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; + + public string FormattedVersion => FormatVersion.Format(AppData.Version); + public string FormattedDeveloper => FormatDeveloper.Format(AppData.Developer); + + public string FormattedFileExtension => FormatExtension.Format(AppData.FileExtension); + public string FormattedLastPlayed => FormatLastPlayed.Format(AppData.LastPlayedString); + public string FormattedPlayTime => FormatPlayTime.Format(AppData.TimePlayedString); + public string FormattedFileSize => FormatSize.Format(AppData.FileSizeString); + + public string FormattedLdnInfo => + $"{FormatHostedGames.Format(AppData.GameCount)}\n{FormatPlayerCount.Format(AppData.PlayerCount)}"; + } +} diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs index aef54819e..48e30e663 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs @@ -49,6 +49,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary public int PlayerCount { get; set; } public int GameCount { get; set; } + + public bool HasLdnGames => PlayerCount != 0 && GameCount != 0; + public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } From 717851985e352b87934ab74739d601ea30c34758 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:28:37 -0600 Subject: [PATCH 09/13] UI: Reorganize Game Info dialog popup + localization --- src/Ryujinx/Assets/locales.json | 336 ++++++++++++------ .../UI/Controls/ApplicationDataView.axaml | 186 +++++----- .../UI/Controls/ApplicationDataView.axaml.cs | 1 + .../UI/ViewModels/ApplicationDataViewModel.cs | 28 +- 4 files changed, 317 insertions(+), 234 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index d597e8a4c..cb222e935 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1525,151 +1525,151 @@ { "ID": "GameListHeaderDeveloper", "Translations": { - "ar_SA": "المطور", - "de_DE": "Entwickler", - "el_GR": "Προγραμματιστής", - "en_US": "Developer", - "es_ES": "Desarrollador", - "fr_FR": "Développeur", - "he_IL": "מפתח", - "it_IT": "Sviluppatore", - "ja_JP": "開発元", - "ko_KR": "개발자", - "no_NO": "Utvikler", - "pl_PL": "Twórca", - "pt_BR": "Desenvolvedor", - "ru_RU": "Разработчик", - "sv_SE": "Utvecklare", - "th_TH": "ผู้พัฒนา", - "tr_TR": "Geliştirici", - "uk_UA": "Розробник", - "zh_CN": "制作商", - "zh_TW": "開發者" + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Developed by {0}", + "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": "GameListHeaderVersion", "Translations": { - "ar_SA": "الإصدار", + "ar_SA": "", "de_DE": "", - "el_GR": "Έκδοση", - "en_US": "Version", - "es_ES": "Versión", + "el_GR": "Έκδοση: {0}", + "en_US": "Version: {0}", + "es_ES": "Versión: {0}", "fr_FR": "", - "he_IL": "גרסה", - "it_IT": "Versione", - "ja_JP": "バージョン", - "ko_KR": "버전", - "no_NO": "Versjon", - "pl_PL": "Wersja", - "pt_BR": "Versão", - "ru_RU": "Версия", + "he_IL": "", + "it_IT": "Versione: {0}", + "ja_JP": "バージョン: {0}", + "ko_KR": "버전: {0}", + "no_NO": "Versjon: {0}", + "pl_PL": "Wersja: {0}", + "pt_BR": "Versão: {0}", + "ru_RU": "Версия: {0}", "sv_SE": "", - "th_TH": "เวอร์ชั่น", - "tr_TR": "Sürüm", - "uk_UA": "Версія", - "zh_CN": "版本", - "zh_TW": "版本" + "th_TH": "เวอร์ชั่น: {0}", + "tr_TR": "Sürüm: {0}", + "uk_UA": "Версія: {0}", + "zh_CN": "版本: {0}", + "zh_TW": "版本: {0}" } }, { "ID": "GameListHeaderTimePlayed", "Translations": { - "ar_SA": "وقت اللعب", - "de_DE": "Spielzeit", - "el_GR": "Χρόνος", - "en_US": "Play Time", - "es_ES": "Tiempo jugado", - "fr_FR": "Temps de jeu", - "he_IL": "זמן משחק", - "it_IT": "Tempo di gioco", - "ja_JP": "プレイ時間", - "ko_KR": "플레이 타임", - "no_NO": "Spilletid", - "pl_PL": "Czas w grze:", - "pt_BR": "Tempo de jogo", - "ru_RU": "Время в игре", - "sv_SE": "Speltid", - "th_TH": "เล่นไปแล้ว", - "tr_TR": "Oynama Süresi", - "uk_UA": "Зіграно часу", - "zh_CN": "游玩时长", - "zh_TW": "遊玩時數" + "ar_SA": "", + "de_DE": "Spielzeit: {0}", + "el_GR": "Χρόνος: {0}", + "en_US": "Play Time: {0}", + "es_ES": "Tiempo jugado: {0}", + "fr_FR": "Temps de jeu: {0}", + "he_IL": "", + "it_IT": "Tempo di gioco: {0}", + "ja_JP": "プレイ時間: {0}", + "ko_KR": "플레이 타임: {0}", + "no_NO": "Spilletid: {0}", + "pl_PL": "Czas w grze: {0}", + "pt_BR": "Tempo de jogo: {0}", + "ru_RU": "Время в игре: {0}", + "sv_SE": "Speltid: {0}", + "th_TH": "เล่นไปแล้ว: {0}", + "tr_TR": "Oynama Süresi: {0}", + "uk_UA": "Зіграно часу: {0}", + "zh_CN": "游玩时长: {0}", + "zh_TW": "遊玩時數: {0}" } }, { "ID": "GameListHeaderLastPlayed", "Translations": { - "ar_SA": "آخر مرة لُعبت", - "de_DE": "Zuletzt gespielt", - "el_GR": "Παίχτηκε", - "en_US": "Last Played", - "es_ES": "Jugado por última vez", - "fr_FR": "Dernière partie jouée", - "he_IL": "שוחק לאחרונה", - "it_IT": "Ultima partita", - "ja_JP": "最終プレイ日時", - "ko_KR": "마지막 플레이", - "no_NO": "Sist Spilt", - "pl_PL": "Ostatnio grane", - "pt_BR": "Último jogo", - "ru_RU": "Последний запуск", - "sv_SE": "Senast spelad", - "th_TH": "เล่นล่าสุด", - "tr_TR": "Son Oynama Tarihi", - "uk_UA": "Востаннє зіграно", - "zh_CN": "最近游玩", - "zh_TW": "最近遊玩" + "ar_SA": "", + "de_DE": "Zuletzt gespielt: {0}", + "el_GR": "Παίχτηκε: {0}", + "en_US": "Last Played: {0}", + "es_ES": "Jugado por última vez: {0}", + "fr_FR": "Dernière partie jouée: {0}", + "he_IL": "", + "it_IT": "Ultima partita: {0}", + "ja_JP": "最終プレイ日時: {0}", + "ko_KR": "마지막 플레이: {0}", + "no_NO": "Sist Spilt: {0}", + "pl_PL": "Ostatnio grane: {0}", + "pt_BR": "Último jogo: {0}", + "ru_RU": "Последний запуск: {0}", + "sv_SE": "Senast spelad: {0}", + "th_TH": "เล่นล่าสุด: {0}", + "tr_TR": "Son Oynama Tarihi: {0}", + "uk_UA": "Востаннє зіграно: {0}", + "zh_CN": "最近游玩: {0}", + "zh_TW": "最近遊玩: {0}" } }, { "ID": "GameListHeaderFileExtension", "Translations": { - "ar_SA": "صيغة الملف", - "de_DE": "Dateiformat", - "el_GR": "Κατάληξη", - "en_US": "File Ext", - "es_ES": "Extensión", - "fr_FR": "Extension du Fichier", - "he_IL": "סיומת קובץ", - "it_IT": "Estensione", - "ja_JP": "ファイル拡張子", - "ko_KR": "파일 확장자", - "no_NO": "Fil Eks.", - "pl_PL": "Rozszerzenie pliku", - "pt_BR": "Extensão", - "ru_RU": "Расширение файла", - "sv_SE": "Filänd", - "th_TH": "นามสกุลไฟล์", - "tr_TR": "Dosya Uzantısı", - "uk_UA": "Розширення файлу", - "zh_CN": "扩展名", - "zh_TW": "副檔名" + "ar_SA": "", + "de_DE": "Dateiformat: {0}", + "el_GR": "Κατάληξη: {0}", + "en_US": "Extension: {0}", + "es_ES": "Extensión: {0}", + "fr_FR": "Extension du Fichier: {0}", + "he_IL": "", + "it_IT": "Estensione: {0}", + "ja_JP": "ファイル拡張子: {0}", + "ko_KR": "파일 확장자: {0}", + "no_NO": "Fil Eks.: {0}", + "pl_PL": "Rozszerzenie pliku: {0}", + "pt_BR": "Extensão: {0}", + "ru_RU": "Расширение файла: {0}", + "sv_SE": "Filänd: {0}", + "th_TH": "นามสกุลไฟล์: {0}", + "tr_TR": "Dosya Uzantısı: {0}", + "uk_UA": "Розширення файлу: {0}", + "zh_CN": "扩展名: {0}", + "zh_TW": "副檔名: {0}" } }, { "ID": "GameListHeaderFileSize", "Translations": { - "ar_SA": "حجم الملف", - "de_DE": "Dateigröße", - "el_GR": "Μέγεθος Αρχείου", - "en_US": "File Size", - "es_ES": "Tamaño del archivo", - "fr_FR": "Taille du Fichier", - "he_IL": "גודל הקובץ", - "it_IT": "Dimensione file", - "ja_JP": "ファイルサイズ", - "ko_KR": "파일 크기", - "no_NO": "Fil Størrelse", - "pl_PL": "Rozmiar pliku", - "pt_BR": "Tamanho", - "ru_RU": "Размер файла", - "sv_SE": "Filstorlek", - "th_TH": "ขนาดไฟล์", - "tr_TR": "Dosya Boyutu", - "uk_UA": "Розмір файлу", - "zh_CN": "大小", - "zh_TW": "檔案大小" + "ar_SA": "", + "de_DE": "Dateigröße: {0}", + "el_GR": "Μέγεθος Αρχείου: {0}", + "en_US": "File Size: {0}", + "es_ES": "Tamaño del archivo: {0}", + "fr_FR": "Taille du Fichier: {0}", + "he_IL": "", + "it_IT": "Dimensione file: {0}", + "ja_JP": "ファイルサイズ: {0}", + "ko_KR": "파일 크기: {0}", + "no_NO": "Fil Størrelse: {0}", + "pl_PL": "Rozmiar pliku: {0}", + "pt_BR": "Tamanho: {0}", + "ru_RU": "Размер файла: {0}", + "sv_SE": "Filstorlek: {0}", + "th_TH": "ขนาดไฟล์: {0}", + "tr_TR": "Dosya Boyutu: {0}", + "uk_UA": "Розмір файлу: {0}", + "zh_CN": "大小: {0}", + "zh_TW": "檔案大小: {0}" } }, { @@ -1697,6 +1697,106 @@ "zh_TW": "路徑" } }, + { + "ID": "GameListHeaderCompatibilityStatus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Compatibility:", + "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": "GameListHeaderTitleId", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Title ID:", + "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": "GameListHeaderHostedGames", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Hosted Games: {0}", + "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": "GameListHeaderPlayerCount", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Online Players: {0}", + "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": "GameListContextMenuOpenUserSaveDirectory", "Translations": { @@ -23298,4 +23398,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index dafd75087..a18fec656 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -2,7 +2,7 @@ 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:appLibrary="using:Ryujinx.Ava.Utilities.AppLibrary" + xmlns:ext="using:Ryujinx.Ava.Common.Markup" xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -13,112 +13,102 @@ MaxWidth="256" MinWidth="256" Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> - + - - - - - - + + + + + - - - - + - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs index cc8091d4d..e85e1188e 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -23,6 +23,7 @@ namespace Ryujinx.Ava.UI.Controls { ContentDialog contentDialog = new() { + Title = appData.Name, PrimaryButtonText = string.Empty, SecondaryButtonText = string.Empty, CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs index 73d555389..9e0a3554a 100644 --- a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs @@ -1,34 +1,26 @@ using Gommon; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; namespace Ryujinx.Ava.UI.ViewModels { public class ApplicationDataViewModel : BaseModel { - private const string FormatVersion = "Current Version: {0}"; - private const string FormatDeveloper = "Developed by {0}"; - - private const string FormatExtension = "Game type: {0}"; - private const string FormatLastPlayed = "Last played: {0}"; - private const string FormatPlayTime = "Play time: {0}"; - private const string FormatSize = "Size: {0}"; - - private const string FormatHostedGames = "Hosted Games: {0}"; - private const string FormatPlayerCount = "Online Players: {0}"; - public ApplicationData AppData { get; } public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; - public string FormattedVersion => FormatVersion.Format(AppData.Version); - public string FormattedDeveloper => FormatDeveloper.Format(AppData.Developer); + public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version); + public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer); - public string FormattedFileExtension => FormatExtension.Format(AppData.FileExtension); - public string FormattedLastPlayed => FormatLastPlayed.Format(AppData.LastPlayedString); - public string FormattedPlayTime => FormatPlayTime.Format(AppData.TimePlayedString); - public string FormattedFileSize => FormatSize.Format(AppData.FileSizeString); + public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension); + public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString); + public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString); + public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString); public string FormattedLdnInfo => - $"{FormatHostedGames.Format(AppData.GameCount)}\n{FormatPlayerCount.Format(AppData.PlayerCount)}"; + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" + + $"\n" + + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}"; } } From 4ae9f1c0d210d8a0af9b43e8ebaca59292ec5510 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:31:31 -0600 Subject: [PATCH 10/13] UI: Use Hosted Games & Player Count localization keys in list view too --- .../Converters/MultiplayerInfoConverter.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index dc36098a1..f23a2beae 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -1,8 +1,11 @@ using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; +using Gommon; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Globalization; +using System.Text; namespace Ryujinx.Ava.UI.Helpers { @@ -12,13 +15,17 @@ namespace Ryujinx.Ava.UI.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is ApplicationData { HasLdnGames: true } applicationData) - { - return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; - } - - return ""; + if (value is not ApplicationData { HasLdnGames: true } applicationData) + return ""; + return new StringBuilder() + .AppendLine( + LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames] + .Format(applicationData.GameCount)) + .Append( + LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount] + .Format(applicationData.PlayerCount)) + .ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) From 4b1d94ccd8e468979eff1c061d51584a1b901747 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:36:36 -0600 Subject: [PATCH 11/13] misc: chore: [ci skip] use MultiplayerInfoConverter instance instead of constructing for every use --- src/Ryujinx/UI/Controls/ApplicationListView.axaml | 2 +- src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index c01c4e8be..ab4b38621 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -141,7 +141,7 @@ diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index f23a2beae..7694e8883 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Helpers { internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter { - private static readonly MultiplayerInfoConverter _instance = new(); + public static readonly MultiplayerInfoConverter Instance = new(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { @@ -35,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers public override object ProvideValue(IServiceProvider serviceProvider) { - return _instance; + return Instance; } } } From 3ecc7819cc1b259e06adf1340b5e5ac843921c1d Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 23:47:24 -0600 Subject: [PATCH 12/13] UI: Fix the app list sort types using the newly changed localization keys --- src/Ryujinx/Assets/locales.json | 127 +++++++++++++++++- .../UI/ViewModels/MainWindowViewModel.cs | 14 +- .../UI/Views/Main/MainViewControls.axaml | 12 +- 3 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index cb222e935..8ec664c81 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1673,7 +1673,132 @@ } }, { - "ID": "GameListHeaderPath", + "ID": "GameListSortDeveloper", + "Translations": { + "ar_SA": "المطور", + "de_DE": "Entwickler", + "el_GR": "Προγραμματιστής", + "en_US": "Developer", + "es_ES": "Desarrollador", + "fr_FR": "Développeur", + "he_IL": "מפתח", + "it_IT": "Sviluppatore", + "ja_JP": "開発元", + "ko_KR": "개발자", + "no_NO": "Utvikler", + "pl_PL": "Twórca", + "pt_BR": "Desenvolvedor", + "ru_RU": "Разработчик", + "sv_SE": "Utvecklare", + "th_TH": "ผู้พัฒนา", + "tr_TR": "Geliştirici", + "uk_UA": "Розробник", + "zh_CN": "制作商", + "zh_TW": "開發者" + } + }, + { + "ID": "GameListSortTimePlayed", + "Translations": { + "ar_SA": "وقت اللعب", + "de_DE": "Spielzeit", + "el_GR": "Χρόνος", + "en_US": "Play Time", + "es_ES": "Tiempo jugado", + "fr_FR": "Temps de jeu", + "he_IL": "זמן משחק", + "it_IT": "Tempo di gioco", + "ja_JP": "プレイ時間", + "ko_KR": "플레이 타임", + "no_NO": "Spilletid", + "pl_PL": "Czas w grze:", + "pt_BR": "Tempo de jogo", + "ru_RU": "Время в игре", + "sv_SE": "Speltid", + "th_TH": "เล่นไปแล้ว", + "tr_TR": "Oynama Süresi", + "uk_UA": "Зіграно часу", + "zh_CN": "游玩时长", + "zh_TW": "遊玩時數" + } + }, + { + "ID": "GameListSortLastPlayed", + "Translations": { + "ar_SA": "آخر مرة لُعبت", + "de_DE": "Zuletzt gespielt", + "el_GR": "Παίχτηκε", + "en_US": "Last Played", + "es_ES": "Jugado por última vez", + "fr_FR": "Dernière partie jouée", + "he_IL": "שוחק לאחרונה", + "it_IT": "Ultima partita", + "ja_JP": "最終プレイ日時", + "ko_KR": "마지막 플레이", + "no_NO": "Sist Spilt", + "pl_PL": "Ostatnio grane", + "pt_BR": "Último jogo", + "ru_RU": "Последний запуск", + "sv_SE": "Senast spelad", + "th_TH": "เล่นล่าสุด", + "tr_TR": "Son Oynama Tarihi", + "uk_UA": "Востаннє зіграно", + "zh_CN": "最近游玩", + "zh_TW": "最近遊玩" + } + }, + { + "ID": "GameListSortFileExtension", + "Translations": { + "ar_SA": "صيغة الملف", + "de_DE": "Dateiformat", + "el_GR": "Κατάληξη", + "en_US": "File Ext", + "es_ES": "Extensión", + "fr_FR": "Extension du Fichier", + "he_IL": "סיומת קובץ", + "it_IT": "Estensione", + "ja_JP": "ファイル拡張子", + "ko_KR": "파일 확장자", + "no_NO": "Fil Eks.", + "pl_PL": "Rozszerzenie pliku", + "pt_BR": "Extensão", + "ru_RU": "Расширение файла", + "sv_SE": "Filänd", + "th_TH": "นามสกุลไฟล์", + "tr_TR": "Dosya Uzantısı", + "uk_UA": "Розширення файлу", + "zh_CN": "扩展名", + "zh_TW": "副檔名" + } + }, + { + "ID": "GameListSortFileSize", + "Translations": { + "ar_SA": "حجم الملف", + "de_DE": "Dateigröße", + "el_GR": "Μέγεθος Αρχείου", + "en_US": "File Size", + "es_ES": "Tamaño del archivo", + "fr_FR": "Taille du Fichier", + "he_IL": "גודל הקובץ", + "it_IT": "Dimensione file", + "ja_JP": "ファイルサイズ", + "ko_KR": "파일 크기", + "no_NO": "Fil Størrelse", + "pl_PL": "Rozmiar pliku", + "pt_BR": "Tamanho", + "ru_RU": "Размер файла", + "sv_SE": "Filstorlek", + "th_TH": "ขนาดไฟล์", + "tr_TR": "Dosya Boyutu", + "uk_UA": "Розмір файлу", + "zh_CN": "大小", + "zh_TW": "檔案大小" + } + }, + { + "ID": "GameListSortPath", "Translations": { "ar_SA": "المسار", "de_DE": "Pfad", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index d7a09a0e3..499eedc8d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -633,15 +633,15 @@ namespace Ryujinx.Ava.UI.ViewModels { return SortMode switch { - ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], - ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], - ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], - ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], - ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], - ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], - ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel], + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed], + ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed], + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath], _ => string.Empty, }; } diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml index cdc66a138..db557b417 100644 --- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -113,37 +113,37 @@ Tag="TitleId" /> From 479b38f035b35ceffe2d4d87999d18eafc29573f Mon Sep 17 00:00:00 2001 From: FluffyOMC <45863583+FluffyOMC@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:42:20 -0500 Subject: [PATCH 13/13] Add tooltips to game status (#625) --- src/Ryujinx/Assets/locales.json | 125 ++++++++++++++++++ .../UI/Controls/ApplicationDataView.axaml | 4 +- .../UI/Controls/ApplicationListView.axaml | 3 +- .../Utilities/AppLibrary/ApplicationData.cs | 14 +- .../Utilities/Compat/CompatibilityCsv.cs | 15 ++- .../Utilities/Compat/CompatibilityList.axaml | 2 + 6 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 8ec664c81..c3044f639 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -23497,6 +23497,131 @@ "zh_TW": "無法啟動" } }, + { + "ID": "CompatibilityListPlayableTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.", + "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": "CompatibilityListIngameTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.", + "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": "CompatibilityListMenusTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes past the title screen but does not make it into main gameplay.", + "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": "CompatibilityListBootsTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots but does not make it past the title screen.", + "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": "CompatibilityListNothingTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Does not boot or shows no signs of activity.", + "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": "ExtractAocListHeader", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index a18fec656..45ae75639 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -41,13 +41,12 @@ HorizontalAlignment="Left" Orientation="Vertical" Spacing="5"> - +