diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 9bda759a5..ec0f58b01 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -1034,16 +1034,16 @@ namespace Ryujinx.HLE.FileSystem switch (fileName) { case "prod.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; case "title.keys": - verified = verifyKeys(lines, titlePattern); + verified = VerifyKeys(lines, titlePattern); break; case "console.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; case "dev.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; default: throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported."); @@ -1056,20 +1056,22 @@ namespace Ryujinx.HLE.FileSystem { throw new FileNotFoundException($"Keys file not found at \"{filePath}\"."); } - } - private bool verifyKeys(string[] lines, string regex) - { - foreach (string line in lines) + return; + + bool VerifyKeys(string[] lines, string regex) { - if (!Regex.IsMatch(line, regex)) + foreach (string line in lines) { - return false; + if (!Regex.IsMatch(line, regex)) + { + return false; + } } + return true; } - return true; } - + public bool AreKeysAlredyPresent(string pathToCheck) { string[] fileNames = { "prod.keys", "title.keys", "console.keys", "dev.keys" }; diff --git a/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs b/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs new file mode 100644 index 000000000..644b7fe74 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs @@ -0,0 +1,64 @@ +using LibHac.Common; +using LibHac.Ncm; +using LibHac.Ns; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.HLE; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; + +namespace Ryujinx.UI.Common.Helper +{ + public readonly struct AppletMetadata + { + private readonly ContentManager _contentManager; + + public string Name { get; } + public ulong ProgramId { get; } + + public string Version { get; } + + public AppletMetadata(ContentManager contentManager, string name, ulong programId, string version = "1.0.0") + : this(name, programId, version) + { + _contentManager = contentManager; + } + + public AppletMetadata(string name, ulong programId, string version = "1.0.0") + { + Name = name; + ProgramId = programId; + Version = version; + } + + public string GetContentPath(ContentManager contentManager) + => (contentManager ?? _contentManager) + .GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program); + + public bool CanStart(ContentManager contentManager, out ApplicationData appData, out BlitStruct appControl) + { + contentManager ??= _contentManager; + if (contentManager == null) + { + appData = null; + appControl = new BlitStruct(0); + return false; + } + + appData = new() + { + Name = Name, + Id = ProgramId, + Path = GetContentPath(contentManager) + }; + + if (string.IsNullOrEmpty(appData.Path)) + { + appControl = new BlitStruct(0); + return false; + } + + appControl = StructHelpers.CreateCustomNacpData(Name, Version); + return true; + } + } +} diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png b/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png new file mode 100644 index 000000000..a00c7ce68 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Thiccjinx.png b/src/Ryujinx.UI.Common/Resources/Logo_Thiccjinx.png deleted file mode 100644 index be807a40a..000000000 Binary files a/src/Ryujinx.UI.Common/Resources/Logo_Thiccjinx.png and /dev/null differ diff --git a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj index c43f95e4a..1ee9a4aa0 100644 --- a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj +++ b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index f29efb15a..70b04ec95 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -1,7 +1,6 @@ using Gommon; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common; -using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.UI.Common.Configuration; using System; @@ -17,7 +16,6 @@ namespace Ryujinx.Ava.Common.Locale private const string DefaultLanguageCode = "en_US"; private readonly Dictionary _localeStrings; - private Dictionary _localeDefaultStrings; private readonly ConcurrentDictionary _dynamicValues; private string _localeLanguageCode; @@ -27,7 +25,6 @@ namespace Ryujinx.Ava.Common.Locale public LocaleManager() { _localeStrings = new Dictionary(); - _localeDefaultStrings = new Dictionary(); _dynamicValues = new ConcurrentDictionary(); Load(); @@ -37,9 +34,7 @@ namespace Ryujinx.Ava.Common.Locale { var localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ? ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_'); - - // Load en_US as default, if the target language translation is missing or incomplete. - LoadDefaultLanguage(); + LoadLanguage(localeLanguageCode); // Save whatever we ended up with. @@ -66,26 +61,14 @@ namespace Ryujinx.Ava.Common.Locale } catch { - // If formatting failed use the default text instead. - if (_localeDefaultStrings.TryGetValue(key, out value)) - try - { - return string.Format(value, dynamicValue); - } - catch - { - // If formatting the default text failed return the key. - return key.ToString(); - } + // If formatting the text failed, + // continue to the below line & return the text without formatting. } return value; } - - // If the locale doesn't contain the key return the default one. - return _localeDefaultStrings.TryGetValue(key, out string defaultValue) - ? defaultValue - : key.ToString(); // If the locale text doesn't exist return the key. + + return key.ToString(); // If the locale text doesn't exist return the key. } set { @@ -109,16 +92,11 @@ namespace Ryujinx.Ava.Common.Locale { _dynamicValues[key] = values; - OnPropertyChanged("Item"); + OnPropertyChanged("Translation"); return this[key]; } - private void LoadDefaultLanguage() - { - _localeDefaultStrings = LoadJsonLanguage(DefaultLanguageCode); - } - public void LoadLanguage(string languageCode) { var locale = LoadJsonLanguage(languageCode); @@ -126,7 +104,7 @@ namespace Ryujinx.Ava.Common.Locale if (locale == null) { _localeLanguageCode = DefaultLanguageCode; - locale = _localeDefaultStrings; + locale = LoadJsonLanguage(_localeLanguageCode); } else { @@ -138,16 +116,12 @@ namespace Ryujinx.Ava.Common.Locale _localeStrings[key] = val; } - OnPropertyChanged("Item"); + OnPropertyChanged("Translation"); LocaleChanged?.Invoke(); } - #nullable enable - private static LocalesJson? _localeData; - - #nullable disable private static Dictionary LoadJsonLanguage(string languageCode) { @@ -158,18 +132,29 @@ namespace Ryujinx.Ava.Common.Locale foreach (LocalesEntry locale in _localeData.Value.Locales) { - if (locale.Translations.Count != _localeData.Value.Languages.Count) + 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}!"); + } + + 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}!"); } if (!Enum.TryParse(locale.ID, out var localeKey)) continue; - localeStrings[localeKey] = - locale.Translations.TryGetValue(languageCode, out string val) && val != string.Empty - ? val - : locale.Translations[DefaultLanguageCode]; + var str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) + ? val + : locale.Translations[DefaultLanguageCode]; + + 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"); + } + + localeStrings[localeKey] = str; } return localeStrings; diff --git a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs index b1b7361a6..364c08c0b 100644 --- a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs +++ b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Common.Markup { internal abstract class BasicMarkupExtension : MarkupExtension { - public virtual string Name => "Item"; + public abstract string Name { get; } public virtual Action? Setter => null; protected abstract T? Value { get; } diff --git a/src/Ryujinx/Common/Markup/MarkupExtensions.cs b/src/Ryujinx/Common/Markup/MarkupExtensions.cs index cae6d8c2c..9e8ff00ef 100644 --- a/src/Ryujinx/Common/Markup/MarkupExtensions.cs +++ b/src/Ryujinx/Common/Markup/MarkupExtensions.cs @@ -6,16 +6,19 @@ namespace Ryujinx.Ava.Common.Markup { internal class IconExtension(string iconString) : BasicMarkupExtension { + public override string Name => "Icon"; protected override Icon Value => new() { Value = iconString }; } internal class SpinningIconExtension(string iconString) : BasicMarkupExtension { + public override string Name => "SIcon"; protected override Icon Value => new() { Value = iconString, Animation = IconAnimation.Spin }; } internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension { + public override string Name => "Translation"; protected override string Value => LocaleManager.Instance[key]; protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension) diff --git a/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx/UI/Controls/SliderScroll.cs similarity index 100% rename from src/Ryujinx/UI/Controls/SliderScroll.axaml.cs rename to src/Ryujinx/UI/Controls/SliderScroll.cs diff --git a/src/Ryujinx/UI/Controls/StatusBarSeparator.cs b/src/Ryujinx/UI/Controls/StatusBarSeparator.cs new file mode 100644 index 000000000..7888879f5 --- /dev/null +++ b/src/Ryujinx/UI/Controls/StatusBarSeparator.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace Ryujinx.Ava.UI.Controls +{ + public class MiniVerticalSeparator : Border + { + public MiniVerticalSeparator() + { + Width = 2; + Height = 12; + Margin = new Thickness(); + BorderBrush = Brushes.Gray; + Background = Brushes.Gray; + BorderThickness = new Thickness(1); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs index c6e814e90..14e8e178a 100644 --- a/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs +++ b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Data; using Avalonia.Data.Converters; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.UI.Common.Models; using System; @@ -32,11 +33,11 @@ namespace Ryujinx.Ava.UI.Helpers if (app.CurrentSavingsB < app.PotentialSavingsB) { - return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, (app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB); + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, ((app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB).CoerceAtLeast(0)); } else { - return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, app.CurrentSavingsB / _bytesPerMB); + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, (app.CurrentSavingsB / _bytesPerMB).CoerceAtLeast(0)); } } diff --git a/src/Ryujinx/UI/ViewModels/BaseModel.cs b/src/Ryujinx/UI/ViewModels/BaseModel.cs index 4db9cf812..d8f2e9096 100644 --- a/src/Ryujinx/UI/ViewModels/BaseModel.cs +++ b/src/Ryujinx/UI/ViewModels/BaseModel.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -11,5 +12,13 @@ namespace Ryujinx.Ava.UI.ViewModels { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + protected void OnPropertiesChanged(params ReadOnlySpan propertyNames) + { + foreach (var propertyName in propertyNames) + { + OnPropertyChanged(propertyName); + } + } } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 3ff05785a..04881b58d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -131,7 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels // For an example of this, download canary 1.2.95, then open the settings menu, and look at the icon in the top-left. // The border gets reduced to colored pixels in the 4 corners. public static readonly Bitmap IconBitmap = - new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Thiccjinx.png")!); + new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx_AntiAlias.png")!); public MainWindow Window { get; init; } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 85ba203f9..7504147b2 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -71,8 +71,7 @@ namespace Ryujinx.Ava.UI.ViewModels { _resolutionScale = value; - OnPropertyChanged(nameof(CustomResolutionScale)); - OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); + OnPropertiesChanged(nameof(CustomResolutionScale), nameof(IsCustomResolutionScaleActive)); } } @@ -181,8 +180,9 @@ namespace Ryujinx.Ava.UI.ViewModels int newInterval = (int)((value / 100f) * 60); _customVSyncInterval = newInterval; _customVSyncIntervalPercentageProxy = value; - OnPropertyChanged((nameof(CustomVSyncInterval))); - OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + OnPropertiesChanged( + nameof(CustomVSyncInterval), + nameof(CustomVSyncIntervalPercentageText)); } } @@ -190,7 +190,7 @@ namespace Ryujinx.Ava.UI.ViewModels { get { - string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + string text = CustomVSyncIntervalPercentageProxy + "%"; return text; } } @@ -221,8 +221,9 @@ namespace Ryujinx.Ava.UI.ViewModels _customVSyncInterval = value; int newPercent = (int)((value / 60f) * 100); _customVSyncIntervalPercentageProxy = newPercent; - OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); - OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertiesChanged( + nameof(CustomVSyncIntervalPercentageProxy), + nameof(CustomVSyncIntervalPercentageText)); OnPropertyChanged(); } } diff --git a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs index e1f9eead5..402b182af 100644 --- a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs @@ -91,39 +91,42 @@ namespace Ryujinx.Ava.UI.ViewModels private void SortingChanged() { - OnPropertyChanged(nameof(IsSortedByName)); - OnPropertyChanged(nameof(IsSortedBySaved)); - OnPropertyChanged(nameof(SortingAscending)); - OnPropertyChanged(nameof(SortingField)); - OnPropertyChanged(nameof(SortingFieldName)); + OnPropertiesChanged( + nameof(IsSortedByName), + nameof(IsSortedBySaved), + nameof(SortingAscending), + nameof(SortingField), + nameof(SortingFieldName)); + SortAndFilter(); } private void DisplayedChanged() { - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(DisplayedXCIFiles)); - OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); + OnPropertiesChanged(nameof(Status), nameof(DisplayedXCIFiles), nameof(SelectedDisplayedXCIFiles)); } private void ApplicationsChanged() { - OnPropertyChanged(nameof(AllXCIFiles)); - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(PotentialSavings)); - OnPropertyChanged(nameof(ActualSavings)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); + OnPropertiesChanged( + nameof(AllXCIFiles), + nameof(Status), + nameof(PotentialSavings), + nameof(ActualSavings), + nameof(CanTrim), + nameof(CanUntrim)); + DisplayedChanged(); SortAndFilter(); } private void SelectionChanged(bool displayedChanged = true) { - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); - OnPropertyChanged(nameof(SelectedXCIFiles)); + OnPropertiesChanged( + nameof(Status), + nameof(CanTrim), + nameof(CanUntrim), + nameof(SelectedXCIFiles)); if (displayedChanged) OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); @@ -131,11 +134,12 @@ namespace Ryujinx.Ava.UI.ViewModels private void ProcessingChanged() { - OnPropertyChanged(nameof(Processing)); - OnPropertyChanged(nameof(Cancel)); - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); + OnPropertiesChanged( + nameof(Processing), + nameof(Cancel), + nameof(Status), + nameof(CanTrim), + nameof(CanUntrim)); } private IEnumerable GetSelectedDisplayedXCIFiles() @@ -360,7 +364,7 @@ namespace Ryujinx.Ava.UI.ViewModels value = _processingApplication.Value with { PercentageProgress = null }; if (value.HasValue) - _displayedXCIFiles.ReplaceWith(value.Value); + _displayedXCIFiles.ReplaceWith(value); _processingApplication = value; OnPropertyChanged(); diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 7d8135dcf..8207f8e03 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -18,7 +18,7 @@ Height="25" Width="25" ToolTip.Tip="{Binding Title}" - Source="resm:Ryujinx.UI.Common.Resources.Logo_Thiccjinx.png?assembly=Ryujinx.UI.Common" /> + Source="resm:Ryujinx.UI.Common.Resources.Logo_Ryujinx_AntiAlias.png?assembly=Ryujinx.UI.Common" /> - + - + - + - + - + - + - + - + 0, - > 1 => 1, - _ => newValue, - }; + Window.ViewModel.Volume = Math.Clamp(newValue, 0, 1); e.Handled = true; } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index e621b42ec..660caa605 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -91,12 +91,14 @@ namespace Ryujinx.Ava.UI.Windows TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; TitleBar.TitleBarHitTestType = (ConfigurationState.Instance.ShowTitleBar) ? TitleBarHitTestType.Simple : TitleBarHitTestType.Complex; - // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) - TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); - // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. StatusBarHeight = StatusBarView.StatusBar.MinHeight; MenuBarHeight = MenuBar.MinHeight; + + TitleBar.Height = MenuBarHeight; + + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); ApplicationList.DataContext = DataContext; ApplicationGrid.DataContext = DataContext;