diff --git a/assets/locales.json b/assets/locales.json index 2542c0f15..d78ec0651 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24316,6 +24316,606 @@ "zh_CN": "在执行首条指令前挂起应用程序,这样就可以从最早的点开始调试。", "zh_TW": "" } + }, + { + "ID": "LdnGameListOpen", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Open LDN Game List", + "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": "LdnGameListTitle", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "LDN Game Browser - {0} games", + "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": "LdnGameListSearchBoxWatermark", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Search {0} LDN games...", + "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": "LdnGameListInfoButtonToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "What is LDN?", + "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": "LdnGameListRefreshToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Refresh available games from the server at {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": "LdnGameListPlayerSortDisable", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Player Count - Disable", + "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": "LdnGameListPlayerSortAscending", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Player Count - Ascending", + "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": "LdnGameListPlayerSortDescending", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Player Count - Descending", + "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": "LdnGameListOnlyShowPublicGames", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Only show public games", + "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": "LdnGameListOnlyShowJoinableGames", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Only show joinable games", + "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": "LdnGameListConnectionTypeMasterServerProxy", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Master Server Proxy", + "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": "LdnGameListConnectionTypeP2P", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "P2P", + "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": "LdnGameListConnectionTypeMasterServerProxyToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Connects through the RyuLDN server (slower).", + "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": "LdnGameListConnectionTypeP2PToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Connects via Peer-to-Peer via UPnP (faster).", + "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": "LdnGameListCreatedAt", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Created: {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": "LdnGameListPlayersAndPlayerCount", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Players ({0} out of {1}):", + "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": "LdnGameListJoinable", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Joinable", + "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": "LdnGameListJoinableToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Game is joinable if it is public or if you know the passphrase.", + "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": "LdnGameListNotJoinable", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Not Joinable", + "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": "LdnGameListNotJoinableToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Game is currently in progress.", + "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": "LdnGameListPublic", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Public", + "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": "LdnGameListPublicToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Anyone can join this game.", + "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": "LdnGameListPrivate", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Private", + "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": "LdnGameListPrivateToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "You can only join this game if you also have the same LDN Passphrase in your settings.", + "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": "" + } } ] } diff --git a/src/Ryujinx.Common/SharedConstants.cs b/src/Ryujinx.Common/SharedConstants.cs index 1b45c0cd0..53b6f1350 100644 --- a/src/Ryujinx.Common/SharedConstants.cs +++ b/src/Ryujinx.Common/SharedConstants.cs @@ -7,5 +7,13 @@ namespace Ryujinx.Common public const string DefaultLanPlayWebHost = DefaultLanPlayHost; public const string AmiiboTagsUrl = "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json"; + + public const string FaqWikiUrl = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting"; + + public const string SetupGuideWikiUrl = + "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide"; + + public const string MultiplayerWikiUrl = + "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide"; } } diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index e5538603f..0b0a2caad 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -47,8 +47,12 @@ namespace Ryujinx.Ava.Common.Locale private void Load() { - string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ? - ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_'); + string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_'); + if (Program.PreviewerDetached && ConfigurationState.Instance.UI.LanguageCode.Value is { } lang) + { + if (!string.IsNullOrEmpty(lang)) + localeLanguageCode = lang; + } LoadLanguage(localeLanguageCode); @@ -63,6 +67,15 @@ namespace Ryujinx.Ava.Common.Locale public static string GetUnformatted(LocaleKeys key) => Instance.Get(key); + public static string GetFormatted(LocaleKeys key, params object[] values) + => GetUnformatted(key).Format(values); + + public static string FormatDynamicValue(LocaleKeys key, params object[] values) + => Instance.UpdateAndGetDynamicValue(key, values); + + public static void Associate(LocaleKeys key, params object[] values) + => Instance.SetDynamicValues(key, values); + public string Get(LocaleKeys key) => _localeStrings.TryGetValue(key, out string value) ? value @@ -107,9 +120,6 @@ namespace Ryujinx.Ava.Common.Locale _ => false }; - public static string FormatDynamicValue(LocaleKeys key, params object[] values) - => Instance.UpdateAndGetDynamicValue(key, values); - public void SetDynamicValues(LocaleKeys key, params object[] values) { _dynamicValues[key] = values; @@ -161,12 +171,14 @@ namespace Ryujinx.Ava.Common.Locale { if (locale.Translations.Count < _localeData.Value.Languages.Count) { - throw new Exception($"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); + throw new Exception( + $"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); } if (locale.Translations.Count > _localeData.Value.Languages.Count) { - throw new Exception($"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); + throw new Exception( + $"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); } if (!Enum.TryParse(locale.ID, out LocaleKeys localeKey)) @@ -178,7 +190,8 @@ namespace Ryujinx.Ava.Common.Locale if (string.IsNullOrEmpty(str)) { - throw new Exception($"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); + throw new Exception( + $"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); } localeStrings[localeKey] = str; diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 0f9350902..29eb0a8ec 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -1169,10 +1169,8 @@ namespace Ryujinx.Ava.Systems string frameTime = Device.Statistics.GetGameFrameTime().ToString("00.00"); return Device.TurboMode - ? LocaleManager.GetUnformatted(LocaleKeys.FpsTurboStatusBarText) - .Format(frameRate, frameTime, Device.TickScalar) - : LocaleManager.GetUnformatted(LocaleKeys.FpsStatusBarText) - .Format(frameRate, frameTime); + ? LocaleManager.GetFormatted(LocaleKeys.FpsTurboStatusBarText, frameRate, frameTime, Device.TickScalar) + : LocaleManager.GetFormatted(LocaleKeys.FpsStatusBarText, frameRate, frameTime); } public async Task ShowExitPrompt() diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 1db2332b8..313a43358 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -14,6 +14,7 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Models; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration.System; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -29,7 +30,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; @@ -85,7 +85,6 @@ namespace Ryujinx.Ava.Systems.AppLibrary private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { @@ -865,16 +864,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary { try { - string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer; - if (string.IsNullOrEmpty(ldnWebHost)) - { - ldnWebHost = SharedConstants.DefaultLanPlayWebHost; - } - - using HttpClient httpClient = new(); - string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); - LdnGameData[] ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData).ToArray(); - LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(ldnGameDataArray)); + LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(await LdnGameModel.GetAllAsync())); return; } catch (Exception ex) diff --git a/src/Ryujinx/Systems/AppLibrary/LdnGameData.cs b/src/Ryujinx/Systems/AppLibrary/LdnGameData.cs deleted file mode 100644 index e5e7abc42..000000000 --- a/src/Ryujinx/Systems/AppLibrary/LdnGameData.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Gommon; -using LibHac.Ns; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Ryujinx.Ava.Systems.AppLibrary -{ - public struct LdnGameData - { - public string Id { get; set; } - public int PlayerCount { get; set; } - public int MaxPlayerCount { get; set; } - public string GameName { get; set; } - public string TitleId { get; set; } - public string Mode { get; set; } - public string Status { get; set; } - public IEnumerable Players { get; set; } - - public static Array GetArrayForApp( - LdnGameData[] receivedData, ref ApplicationControlProperty acp) - { - LibHac.Common.FixedArrays.Array8 communicationId = acp.LocalCommunicationId; - - return new Array(receivedData.Where(game => - communicationId.AsReadOnlySpan().Contains(game.TitleId.ToULong()) - )); - } - - public class Array - { - private readonly LdnGameData[] _ldnDatas; - - internal Array(IEnumerable receivedData) - { - _ldnDatas = receivedData.ToArray(); - } - - public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount); - public int GameCount => _ldnDatas.Length; - } - } - - public static class LdnGameDataHelper - { - public static LdnGameData.Array Where(this LdnGameData[] unfilteredDatas, ref ApplicationControlProperty acp) - => LdnGameData.GetArrayForApp(unfilteredDatas, ref acp); - } -} diff --git a/src/Ryujinx/Systems/AppLibrary/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx/Systems/AppLibrary/LdnGameDataReceivedEventArgs.cs index 8b207c6bd..2c8aba905 100644 --- a/src/Ryujinx/Systems/AppLibrary/LdnGameDataReceivedEventArgs.cs +++ b/src/Ryujinx/Systems/AppLibrary/LdnGameDataReceivedEventArgs.cs @@ -1,4 +1,7 @@ +using Ryujinx.Ava.UI.Models; using System; +using System.Collections.Generic; +using System.Linq; namespace Ryujinx.Ava.Systems.AppLibrary { @@ -6,12 +9,17 @@ namespace Ryujinx.Ava.Systems.AppLibrary { public static new readonly LdnGameDataReceivedEventArgs Empty = new(null); - public LdnGameDataReceivedEventArgs(LdnGameData[] ldnData) + public LdnGameDataReceivedEventArgs(LdnGameModel[] ldnData) { LdnData = ldnData ?? []; } + + public LdnGameDataReceivedEventArgs(IEnumerable ldnData) + { + LdnData = ldnData?.ToArray() ?? []; + } - public LdnGameData[] LdnData { get; set; } + public LdnGameModel[] LdnData { get; } } } diff --git a/src/Ryujinx/Systems/AppLibrary/LdnGameDataSerializerContext.cs b/src/Ryujinx/Systems/AppLibrary/LdnGameDataSerializerContext.cs deleted file mode 100644 index ff7718ed5..000000000 --- a/src/Ryujinx/Systems/AppLibrary/LdnGameDataSerializerContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Ryujinx.Ava.Systems.AppLibrary -{ - [JsonSerializable(typeof(IEnumerable))] - internal partial class LdnGameDataSerializerContext : JsonSerializerContext; -} diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index bc8fdb40a..405400b81 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -11,6 +11,7 @@ using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE; +using System; using System.Collections.Generic; using System.Linq; using RyuLogger = Ryujinx.Common.Logging.Logger; @@ -689,6 +690,16 @@ namespace Ryujinx.Ava.Systems.Configuration : ldnServer; } + public string GetLdnWebServer() + { + if (Environment.GetEnvironmentVariable("RYULDN_WEB_HOST") is not { } ldnWebServer) + ldnWebServer = LdnServer; + + return string.IsNullOrEmpty(ldnWebServer) + ? SharedConstants.DefaultLanPlayWebHost + : ldnWebServer; + } + public MultiplayerSection() { LanInterfaceId = new ReactiveObject(); diff --git a/src/Ryujinx/Systems/Rebooter.cs b/src/Ryujinx/Systems/Rebooter.cs index ac22dfb15..5360edee9 100644 --- a/src/Ryujinx/Systems/Rebooter.cs +++ b/src/Ryujinx/Systems/Rebooter.cs @@ -1,11 +1,9 @@ using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.Systems diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index c54cd390c..34e45114a 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,26 +1,5 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.Input; -using LibHac.Fs; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Ava.Common; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.Common.Models; -using Ryujinx.Ava.Systems.AppLibrary; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Ava.UI.Views.Dialog; -using Ryujinx.Ava.UI.Windows; -using Ryujinx.Ava.Utilities; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Helper; -using Ryujinx.HLE.HOS; -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.IO; -using Path = System.IO.Path; namespace Ryujinx.Ava.UI.Controls { diff --git a/src/Ryujinx/UI/Helpers/Converters/LocaleKeyValueConverter.cs b/src/Ryujinx/UI/Helpers/Converters/LocaleKeyValueConverter.cs new file mode 100644 index 000000000..fc1a46754 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/Converters/LocaleKeyValueConverter.cs @@ -0,0 +1,20 @@ +using Avalonia.Data.Converters; +using CommandLine; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class LocaleKeyValueConverter : IValueConverter + { + private static readonly Lazy _shared = new(() => new()); + public static LocaleKeyValueConverter Shared => _shared.Value; + + public object Convert(object value, Type _, object __, CultureInfo ___) + => LocaleManager.Instance[value.Cast()]; + + public object ConvertBack(object value, Type _, object __, CultureInfo ___) + => throw new NotSupportedException(); + } +} diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs index c7d217bf5..fc4193948 100644 --- a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; diff --git a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs index 0213d72fe..c8169b6d5 100644 --- a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs @@ -2,7 +2,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; -using System.Xml.Linq; namespace Ryujinx.Ava.UI.Models.Input { diff --git a/src/Ryujinx/UI/Models/LdnGameModel.cs b/src/Ryujinx/UI/Models/LdnGameModel.cs new file mode 100644 index 000000000..d677c55ee --- /dev/null +++ b/src/Ryujinx/UI/Models/LdnGameModel.cs @@ -0,0 +1,180 @@ +using Gommon; +using Humanizer; +using LibHac.Ns; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Systems.Configuration; +using Ryujinx.Common.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Models +{ + public record LdnGameModel + { + public string Id { get; private init; } + public bool IsPublic { get; private init; } + public short PlayerCount { get; private init; } + public short MaxPlayerCount { get; private init; } + public TitleTuple Title { get; private init; } + public ConnectionType ConnectionType { get; private init; } + public bool IsJoinable { get; private init; } + public ushort SceneId { get; private init; } + public string[] Players { get; private init; } + + public string PlayersLabel => + LocaleManager.GetFormatted(LocaleKeys.LdnGameListPlayersAndPlayerCount, PlayerCount, MaxPlayerCount); + + public string FormattedPlayers => + Players.Chunk(4) + .Select(x => x.FormatCollection(s => s, prefix: " ", separator: ", ")) + .JoinToString("\n "); + + public DateTimeOffset CreatedAt { get; init; } + + public string FormattedCreatedAt + => LocaleManager.GetFormatted(LocaleKeys.LdnGameListCreatedAt, CreatedAt.Humanize()); + + public string CreatedAtToolTip => CreatedAt.DateTime.ToString(CultureInfo.CurrentUICulture); + + public LocaleKeys ConnectionTypeLocaleKey => ConnectionType switch + { + ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxy, + ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2P, + _ => throw new ArgumentOutOfRangeException(nameof(ConnectionType), + $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'") + }; + + public LocaleKeys ConnectionTypeToolTipLocaleKey => ConnectionType switch + { + ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxyToolTip, + ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2PToolTip, + _ => throw new ArgumentOutOfRangeException(nameof(ConnectionType), + $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'") + }; + + public record struct TitleTuple + { + public required string Name { get; init; } + public required string Id { get; init; } + public required string Version { get; init; } + } + + public static Array GetArrayForApp( + LdnGameModel[] receivedData, + ref ApplicationControlProperty acp, + bool onlyJoinable = true, + bool onlyPublic = true) + { + LibHac.Common.FixedArrays.Array8 communicationId = acp.LocalCommunicationId; + + return new Array(receivedData.Where(game => + communicationId.AsReadOnlySpan().Contains(game.Title.Id.ToULong()) + ), onlyJoinable, onlyPublic); + } + + public class Array : IEnumerable + { + private readonly LdnGameModel[] _ldnDatas; + + internal Array(IEnumerable receivedData, bool onlyJoinable = false, bool onlyPublic = false) + { + if (onlyJoinable) + receivedData = receivedData.Where(x => x.IsJoinable); + + if (onlyPublic) + receivedData = receivedData.Where(x => x.IsPublic); + + _ldnDatas = receivedData.ToArray(); + } + + public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount); + public int GameCount => _ldnDatas.Length; + + public IEnumerator GetEnumerator() => (_ldnDatas as IEnumerable).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _ldnDatas.GetEnumerator(); + } + + public static async Task> GetAllAsync(HttpClient client = null) + => LdnGameJsonModel.ParseArray(await GetAllAsyncRequestImpl(client)) + .Select(FromJson); + + private static async Task GetAllAsyncRequestImpl(HttpClient client = null) + { + var ldnWebHost = ConfigurationState.Instance.Multiplayer.GetLdnWebServer(); + + LocaleManager.Associate(LocaleKeys.LdnGameListRefreshToolTip, ldnWebHost); + + try + { + if (client != null) + return await client.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + + using HttpClient httpClient = new(); + return await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + } + catch + { + return "[]"; + } + } + + private static LdnGameModel FromJson(LdnGameJsonModel json) => + new() + { + Id = json.Id, + IsPublic = json.IsPublic, + PlayerCount = json.PlayerCount, + MaxPlayerCount = json.MaxPlayerCount, + Title = new TitleTuple { Name = json.TitleName, Id = json.TitleId, Version = json.TitleVersion }, + ConnectionType = json.ConnectionType switch + { + "P2P" => ConnectionType.PeerToPeer, + "Master Server Proxy" => ConnectionType.MasterServerProxy, + _ => throw new ArgumentOutOfRangeException(nameof(json), + $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{json.ConnectionType}'") + }, + IsJoinable = json.Joinability is "Joinable", + SceneId = json.SceneId, + Players = json.Players, + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(json.CreatedAtUnixTimestamp).ToLocalTime() + }; + } + + public class LdnGameJsonModel + { + [JsonPropertyName("id")] public string Id { get; set; } + [JsonPropertyName("is_public")] public bool IsPublic { get; set; } + [JsonPropertyName("player_count")] public short PlayerCount { get; set; } + [JsonPropertyName("max_player_count")] public short MaxPlayerCount { get; set; } + [JsonPropertyName("game_name")] public string TitleName { get; set; } + [JsonPropertyName("title_id")] public string TitleId { get; set; } + [JsonPropertyName("title_version")] public string TitleVersion { get; set; } + [JsonPropertyName("mode")] public string ConnectionType { get; set; } + [JsonPropertyName("status")] public string Joinability { get; set; } + [JsonPropertyName("scene_id")] public ushort SceneId { get; set; } + [JsonPropertyName("players")] public string[] Players { get; set; } + [JsonPropertyName("created_at")] public long CreatedAtUnixTimestamp { get; set; } + + public static LdnGameJsonModel Parse(string value) + => JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModel); + + public static LdnGameJsonModel[] ParseArray(string value) + => JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModelArray); + } + + public enum ConnectionType + { + PeerToPeer, + MasterServerProxy + } + + [JsonSerializable(typeof(LdnGameJsonModel[]))] + partial class LdnGameJsonModelSerializerContext : JsonSerializerContext; +} diff --git a/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs b/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs index 8a5ae441b..e4cd793c7 100644 --- a/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs @@ -145,8 +145,7 @@ namespace Ryujinx.Ava.UI.ViewModels { } - - OnPropertyChanged(); + OnPropertyChanged(nameof(CurrentEntries)); } diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 81aae6b74..bc0f066d0 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -1,7 +1,6 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Svg.Skia; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Gommon; using Ryujinx.Ava.Common.Locale; diff --git a/src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs b/src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs new file mode 100644 index 000000000..4bd9aa92d --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs @@ -0,0 +1,207 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Gommon; +using System; +using System.Collections.Generic; +using System.Linq; +using Ryujinx.Ava.Systems.AppLibrary; +using Ryujinx.Ava.UI.Models; +using System.ComponentModel; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public partial class LdnGamesListViewModel : BaseModel, IDisposable + { + public MainWindowViewModel Mwvm { get; } + + private readonly HttpClient _refreshClient; + + private (int PlayerCount, int Name) _sorting; + + private IEnumerable _visibleEntries; + + private string[] _ownedGameTitleIds = []; + + private Func _sortKeySelector = x => x.Title.Name; // Default sort by Title name + + public IEnumerable VisibleEntries => ApplyFilters(); + + private IEnumerable ApplyFilters() + { + if (_visibleEntries is null) + { + _visibleEntries = Mwvm.LdnModels; + SortApply(); + } + + var filtered = _visibleEntries; + + if (OnlyShowForOwnedGames) + filtered = filtered.Where(x => _ownedGameTitleIds.ContainsIgnoreCase(x.Title.Id)); + + if (OnlyShowPublicGames) + filtered = filtered.Where(x => x.IsPublic); + + if (OnlyShowJoinableGames) + filtered = filtered.Where(x => x.IsJoinable); + + return filtered; + } + + public LdnGamesListViewModel() + { + if (Program.PreviewerDetached) + { + Mwvm = RyujinxApp.MainWindow.ViewModel; + } + } + + private void AppCountUpdated(object _, ApplicationCountUpdatedEventArgs __) + => _ownedGameTitleIds = Mwvm.ApplicationLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray(); + + public LdnGamesListViewModel(MainWindowViewModel mwvm) + { + if (Program.PreviewerDetached) + { + Mwvm = mwvm; + _visibleEntries = Mwvm.LdnModels; + _refreshClient = new HttpClient(); + AppCountUpdated(null, null); + Mwvm.ApplicationLibrary.ApplicationCountUpdated += AppCountUpdated; + Mwvm.PropertyChanged += Mwvm_OnPropertyChanged; + } + } + + void IDisposable.Dispose() + { + if (Program.PreviewerDetached) + { + _visibleEntries = null; + _refreshClient.Dispose(); + Mwvm.ApplicationLibrary.ApplicationCountUpdated -= AppCountUpdated; + Mwvm.PropertyChanged -= Mwvm_OnPropertyChanged; + } + GC.SuppressFinalize(this); + } + + private void Mwvm_OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(MainWindowViewModel.LdnModels)) + OnPropertyChanged(nameof(VisibleEntries)); + } + + [ObservableProperty] private bool _isRefreshing; + private bool _onlyShowForOwnedGames; + private bool _onlyShowPublicGames = true; + private bool _onlyShowJoinableGames = true; + + public async Task RefreshAsync() + { + IsRefreshing = true; + + await Mwvm.ApplicationLibrary.RefreshLdn(); + + IsRefreshing = false; + + OnPropertyChanged(nameof(VisibleEntries)); + } + + public bool OnlyShowForOwnedGames + { + get => _onlyShowForOwnedGames; + set + { + OnPropertyChanging(); + OnPropertyChanging(nameof(VisibleEntries)); + _onlyShowForOwnedGames = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VisibleEntries)); + } + } + + public bool OnlyShowPublicGames + { + get => _onlyShowPublicGames; + set + { + OnPropertyChanging(); + OnPropertyChanging(nameof(VisibleEntries)); + _onlyShowPublicGames = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VisibleEntries)); + } + } + + public bool OnlyShowJoinableGames + { + get => _onlyShowJoinableGames; + set + { + OnPropertyChanging(); + OnPropertyChanging(nameof(VisibleEntries)); + _onlyShowJoinableGames = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VisibleEntries)); + } + } + + + public void NameSorting(int nameSort = 0) + { + _sorting.Name = nameSort; + SortApply(); + } + + public void StatusSorting(int statusSort = 0) + { + _sorting.PlayerCount = statusSort; + SortApply(); + } + + public void Search(string searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + { + SetEntries(Mwvm.LdnModels); + SortApply(); + return; + } + + SetEntries(Mwvm.LdnModels.Where(x => + x.Title.Name.ContainsIgnoreCase(searchTerm) + || x.Title.Id.ContainsIgnoreCase(searchTerm))); + + SortApply(); + } + + private void SetEntries(IEnumerable entries) + { + _visibleEntries = entries.ToList(); + OnPropertyChanged(nameof(VisibleEntries)); + } + + private void SortApply() + { + try + { + _visibleEntries = (_sorting switch + { + (0, 0) => _visibleEntries.OrderBy(x => _sortKeySelector(x) ?? string.Empty), // A - Z + (0, 1) => _visibleEntries.OrderByDescending(x => _sortKeySelector(x) ?? string.Empty), // Z - A + (1, 0) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then A - Z + (1, 1) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then A - Z + (2, 0) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then Z - A + (2, 1) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then Z - A + _ => _visibleEntries.OrderBy(x => x.PlayerCount) + }).ToList(); + } + catch + { + + } + + OnPropertyChanged(nameof(VisibleEntries)); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 2e60ae3a3..d1b2d6916 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Common.UI; @@ -57,7 +58,6 @@ using Key = Ryujinx.Input.Key; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; -using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; namespace Ryujinx.Ava.UI.ViewModels { @@ -111,6 +111,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] private bool _isSubMenuOpen; [ObservableProperty] private ApplicationContextMenu _listAppContextMenu; [ObservableProperty] private ApplicationContextMenu _gridAppContextMenu; + [ObservableProperty] private bool _isRyuLdnEnabled; [ObservableProperty] private bool _updateAvailable; public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () => @@ -142,7 +143,25 @@ namespace Ryujinx.Ava.UI.ViewModels private ApplicationData _gridSelectedApplication; // Key is Title ID - public SafeDictionary LdnData = []; + /// + /// At any given time, this dictionary contains the filtered data from . + /// Filtered in this case meaning installed games only. + /// + public SafeDictionary UsableLdnData = []; + + private LdnGameModel[] _ldnModels; + + public LdnGameModel[] LdnModels + { + get => _ldnModels; + set + { + _ldnModels = value; + LocaleManager.Associate(LocaleKeys.LdnGameListTitle, value.Length); + LocaleManager.Associate(LocaleKeys.LdnGameListSearchBoxWatermark, value.Length); + OnPropertyChanged(); + } + } public MainWindow Window { get; init; } @@ -165,11 +184,28 @@ namespace Ryujinx.Ava.UI.ViewModels { LoadConfigurableHotKeys(); + IsRyuLdnEnabled = ConfigurationState.Instance.Multiplayer.Mode.Value is MultiplayerMode.LdnRyu; + ConfigurationState.Instance.Multiplayer.Mode.Event += OnLdnModeChanged; + Volume = ConfigurationState.Instance.System.AudioVolume; CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; } } + ~MainWindowViewModel() + { + if (Program.PreviewerDetached) + { + ConfigurationState.Instance.Multiplayer.Mode.Event -= OnLdnModeChanged; + } + } + + private void OnLdnModeChanged(object sender, ReactiveEventArgs e) => + Dispatcher.UIThread.Post(() => + { + IsRyuLdnEnabled = e.NewValue is MultiplayerMode.LdnRyu; + }); + public void Initialize( ContentManager contentManager, IStorageProvider storageProvider, @@ -313,11 +349,11 @@ namespace Ryujinx.Ava.UI.ViewModels if (ts.HasValue) { var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value); - LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime); + LocaleManager.Associate(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime); ShowTotalTimePlayed = formattedPlayTime != string.Empty; return; } - + ShowTotalTimePlayed = ts.HasValue; } diff --git a/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs index 894af8c6c..eb7fa0fef 100644 --- a/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs @@ -4,7 +4,6 @@ using DynamicData.Binding; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Models; using Ryujinx.HLE.HOS.Services.Account.Acc; -using System.Collections.Generic; using System.Collections.ObjectModel; namespace Ryujinx.Ava.UI.ViewModels diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index ac8046209..76bafe0c4 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -7,6 +7,7 @@ mc:Ignorable="d" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" + xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common" x:DataType="viewModels:MainWindowViewModel" x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> @@ -250,25 +251,30 @@ Name="CompatibilityListMenuItem" Header="{ext:Locale CompatibilityListOpen}" Icon="{ext:Icon fa-solid fa-database}"/> + diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 15a287495..d8dff3eb3 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -49,6 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Main XciTrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show); AboutWindowMenuItem.Command = Commands.Create(AboutView.Show); CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show()); + LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show()); UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand; diff --git a/src/Ryujinx/UI/Views/Misc/ApplicationListView.axaml b/src/Ryujinx/UI/Views/Misc/ApplicationListView.axaml index c9926301b..fe1391fee 100644 --- a/src/Ryujinx/UI/Views/Misc/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Views/Misc/ApplicationListView.axaml @@ -148,12 +148,27 @@ Text="{Binding FileExtension}" TextAlignment="Start" TextWrapping="Wrap" /> - + Background="{DynamicResource AppListBackgroundColor}" + Padding="0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml.cs b/src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml.cs new file mode 100644 index 000000000..dbff559bf --- /dev/null +++ b/src/Ryujinx/UI/Windows/LdnGamesListWindow.axaml.cs @@ -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)); + } + } + } +} diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 195b1470a..2a7bfa8ef 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -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;