From cee712105850ac3385cd0091a923438167433f9f Mon Sep 17 00:00:00 2001 From: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 8 Apr 2023 01:22:00 +0200 Subject: Move solution and projects to src --- src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml | 247 +++++ src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs | 62 ++ src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml | 74 ++ src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs | 59 + src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml | 106 ++ src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs | 119 ++ .../UI/Windows/ContentDialogOverlayWindow.axaml | 25 + .../UI/Windows/ContentDialogOverlayWindow.axaml.cs | 25 + .../UI/Windows/ControllerSettingsWindow.axaml | 1169 ++++++++++++++++++++ .../UI/Windows/ControllerSettingsWindow.axaml.cs | 181 +++ .../Windows/DownloadableContentManagerWindow.axaml | 194 ++++ .../DownloadableContentManagerWindow.axaml.cs | 115 ++ src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs | 192 ++++ src/Ryujinx.Ava/UI/Windows/MainWindow.axaml | 206 ++++ src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs | 441 ++++++++ .../UI/Windows/MotionSettingsWindow.axaml | 141 +++ .../UI/Windows/MotionSettingsWindow.axaml.cs | 71 ++ .../UI/Windows/RumbleSettingsWindow.axaml | 57 + .../UI/Windows/RumbleSettingsWindow.axaml.cs | 57 + src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml | 128 +++ src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs | 103 ++ src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs | 39 + src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml | 135 +++ .../UI/Windows/TitleUpdateWindow.axaml.cs | 96 ++ 24 files changed, 4042 insertions(+) create mode 100644 src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/MainWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs (limited to 'src/Ryujinx.Ava/UI/Windows') diff --git a/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml b/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml new file mode 100644 index 00000000..7bd3e20d --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs new file mode 100644 index 00000000..36a28605 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs @@ -0,0 +1,62 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class AboutWindow : UserControl + { + public AboutWindow() + { + DataContext = new AboutWindowViewModel(); + + InitializeComponent(); + } + + public static async Task Show() + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], + Content = new AboutWindow() + }; + + Style closeButton = new(x => x.Name("CloseButton")); + closeButton.Setters.Add(new Setter(WidthProperty, 80d)); + + Style closeButtonParent = new(x => x.Name("CommandSpace")); + closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right)); + + contentDialog.Styles.Add(closeButton); + contentDialog.Styles.Add(closeButtonParent); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void Button_OnClick(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + OpenHelper.OpenUrl(button.Tag.ToString()); + } + } + + private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (sender is TextBlock) + { + OpenHelper.OpenUrl("https://amiiboapi.com"); + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml new file mode 100644 index 00000000..90d47b8e --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs new file mode 100644 index 00000000..cb939763 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia.Collections; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class CheatWindow : StyleableWindow + { + private readonly string _enabledCheatsPath; + public bool NoCheatsFound { get; } + + private AvaloniaList LoadedCheats { get; } + + public string Heading { get; } + + public CheatWindow() + { + DataContext = this; + + InitializeComponent(); + + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.CheatWindowTitle]; + } + + public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) + { + LoadedCheats = new AvaloniaList(); + + Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); + + InitializeComponent(); + + string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); + ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber); + + _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); + + string[] enabled = { }; + + if (File.Exists(_enabledCheatsPath)) + { + enabled = File.ReadAllLines(_enabledCheatsPath); + } + + int cheatAdded = 0; + + var mods = new ModLoader.ModCache(); + + ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue); + + string currentCheatFile = string.Empty; + string buildId = string.Empty; + string parentPath = string.Empty; + + CheatsList currentGroup = null; + + foreach (var cheat in mods.Cheats) + { + if (cheat.Path.FullName != currentCheatFile) + { + currentCheatFile = cheat.Path.FullName; + parentPath = currentCheatFile.Replace(titleModsPath, ""); + + buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); + currentGroup = new CheatsList(buildId, parentPath); + + LoadedCheats.Add(currentGroup); + } + + var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}")); + currentGroup?.Add(model); + + cheatAdded++; + } + + if (cheatAdded == 0) + { + NoCheatsFound = true; + } + + DataContext = this; + + Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.CheatWindowTitle]; + } + + public void Save() + { + if (NoCheatsFound) + { + return; + } + + List enabledCheats = new List(); + + foreach (var cheats in LoadedCheats) + { + foreach (var cheat in cheats) + { + if (cheat.IsEnabled) + { + enabledCheats.Add(cheat.BuildIdKey); + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath)); + + File.WriteAllLines(_enabledCheatsPath, enabledCheats); + + Close(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml new file mode 100644 index 00000000..8b52bade --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs new file mode 100644 index 00000000..3f77124d --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ContentDialogOverlayWindow : StyleableWindow + { + public ContentDialogOverlayWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + ExtendClientAreaToDecorationsHint = true; + TransparencyLevelHint = WindowTransparencyLevel.Transparent; + WindowStartupLocation = WindowStartupLocation.Manual; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaTitleBarHeightHint = 0; + Background = Brushes.Transparent; + CanResize = false; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml new file mode 100644 index 00000000..8a4d22ff --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml @@ -0,0 +1,1169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs new file mode 100644 index 00000000..2864b6da --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs @@ -0,0 +1,181 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ControllerSettingsWindow : UserControl + { + private bool _dialogOpen; + + private ButtonKeyAssigner _currentAssigner; + internal ControllerSettingsViewModel ViewModel { get; set; } + + public ControllerSettingsWindow() + { + DataContext = ViewModel = new ControllerSettingsViewModel(this); + + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button && !(visual is CheckBox)) + { + button.Checked += Button_Checked; + button.Unchecked += Button_Unchecked; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + { + _currentAssigner.Cancel(); + } + } + + private void Button_Checked(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null && (bool)button.IsChecked) + { + _currentAssigner = new ButtonKeyAssigner(button); + + FocusManager.Instance.Focus(this, NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.IsAssigned) + { + ViewModel.IsModified = true; + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + var device = ViewModel.Devices[ViewModel.Device]; + + if (device.Type == DeviceType.Keyboard) + { + assigner = new KeyboardKeyAssigner((IKeyboard)ViewModel.SelectedGamepad); + } + else if (device.Type == DeviceType.Controller) + { + assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick); + } + else + { + throw new Exception("Controller not supported"); + } + + return assigner; + } + + private void Button_Unchecked(object sender, RoutedEventArgs e) + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = false; + + if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed) + { + shouldUnbind = true; + } + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + ViewModel.IsModified = false; + + if (e.AddedItems.Count > 0) + { + var player = (PlayerModel)e.AddedItems[0]; + ViewModel.PlayerId = player.Id; + } + } + } + + public void Dispose() + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + ViewModel.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml new file mode 100644 index 00000000..fe446fb3 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs new file mode 100644 index 00000000..6dc99fb5 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -0,0 +1,115 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class DownloadableContentManagerWindow : UserControl + { + public DownloadableContentManagerViewModel ViewModel; + + public DownloadableContentManagerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName); + + InitializeComponent(); + } + + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName), + Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")) + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs) + { + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void RemoveDLC(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is DownloadableContentModel model) + { + ViewModel.Remove(model); + } + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is DownloadableContentModel model) + { + OpenHelper.LocateFile(model.ContainerPath); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is DownloadableContentModel model) + { + var index = ViewModel.DownloadableContents.IndexOf(model); + + if (index != -1) + { + ViewModel.DownloadableContents[index].Enabled = true; + } + } + } + + foreach (var content in e.RemovedItems) + { + if (content is DownloadableContentModel model) + { + var index = ViewModel.DownloadableContents.IndexOf(model); + + if (index != -1) + { + ViewModel.DownloadableContents[index].Enabled = false; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs b/src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs new file mode 100644 index 00000000..a4c6287f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs @@ -0,0 +1,192 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Windows +{ + static class IconColorPicker + { + private const int ColorsPerLine = 64; + private const int TotalColors = ColorsPerLine * ColorsPerLine; + + private const int UvQuantBits = 3; + private const int UvQuantShift = BitsPerComponent - UvQuantBits; + + private const int SatQuantBits = 5; + private const int SatQuantShift = BitsPerComponent - SatQuantBits; + + private const int BitsPerComponent = 8; + + private const int CutOffLuminosity = 64; + + private readonly struct PaletteColor + { + public int Qck { get; } + public byte R { get; } + public byte G { get; } + public byte B { get; } + + public PaletteColor(int qck, byte r, byte g, byte b) + { + Qck = qck; + R = r; + G = g; + B = b; + } + } + + public static Color GetFilteredColor(Image image) + { + var color = GetColor(image).ToPixel(); + + // We don't want colors that are too dark. + // If the color is too dark, make it brighter by reducing the range + // and adding a constant color. + int luminosity = GetColorApproximateLuminosity(color.R, color.G, color.B); + if (luminosity < CutOffLuminosity) + { + color = Color.FromRgb( + (byte)Math.Min(CutOffLuminosity + color.R, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.G, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.B, byte.MaxValue)); + } + + return color; + } + + public static Color GetColor(Image image) + { + var colors = new PaletteColor[TotalColors]; + + var dominantColorBin = new Dictionary(); + + var buffer = GetBuffer(image); + + int w = image.Width; + + int w8 = w << 8; + int h8 = image.Height << 8; + + int xStep = w8 / ColorsPerLine; + int yStep = h8 / ColorsPerLine; + + int i = 0; + int maxHitCount = 0; + + for (int y = 0; y < image.Height; y++) + { + int yOffset = y * image.Width; + + for (int x = 0; x < image.Width && i < TotalColors; x++) + { + int offset = x + yOffset; + + byte cb = buffer[offset].B; + byte cg = buffer[offset].G; + byte cr = buffer[offset].R; + + var qck = GetQuantizedColorKey(cr, cg, cb); + + if (dominantColorBin.TryGetValue(qck, out int hitCount)) + { + dominantColorBin[qck] = hitCount + 1; + + if (maxHitCount < hitCount) + { + maxHitCount = hitCount; + } + } + else + { + dominantColorBin.Add(qck, 1); + } + + colors[i++] = new PaletteColor(qck, cr, cg, cb); + } + } + + int highScore = -1; + PaletteColor bestCandidate = default; + + for (i = 0; i < TotalColors; i++) + { + var score = GetColorScore(dominantColorBin, maxHitCount, colors[i]); + + if (highScore < score) + { + highScore = score; + bestCandidate = colors[i]; + } + } + + return Color.FromRgb(bestCandidate.R, bestCandidate.G, bestCandidate.B); + } + + public static Bgra32[] GetBuffer(Image image) + { + return image.TryGetSinglePixelSpan(out var data) ? data.ToArray() : Array.Empty(); + } + + private static int GetColorScore(Dictionary dominantColorBin, int maxHitCount, PaletteColor color) + { + var hitCount = dominantColorBin[color.Qck]; + var balancedHitCount = BalanceHitCount(hitCount, maxHitCount); + var quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift; + var value = GetColorValue(color); + + // If the color is rarely used on the image, + // then chances are that theres a better candidate, even if the saturation value + // is high. By multiplying the saturation value with a weight, we can lower + // it if the color is almost never used (hit count is low). + var satWeighted = quantSat; + var satWeight = balancedHitCount << 5; + if (satWeight < 0x100) + { + satWeighted = (satWeighted * satWeight) >> 8; + } + + // Compute score from saturation and dominance of the color. + // We prefer more vivid colors over dominant ones, so give more weight to the saturation. + var score = ((satWeighted << 1) + balancedHitCount) * value; + + return score; + } + + private static int BalanceHitCount(int hitCount, int maxHitCount) + { + return (hitCount << 8) / maxHitCount; + } + + private static int GetColorApproximateLuminosity(byte r, byte g, byte b) + { + return (r + g + b) / 3; + } + + private static int GetColorSaturation(PaletteColor color) + { + int cMax = Math.Max(Math.Max(color.R, color.G), color.B); + + if (cMax == 0) + { + return 0; + } + + int cMin = Math.Min(Math.Min(color.R, color.G), color.B); + int delta = cMax - cMin; + return (delta << 8) / cMax; + } + + private static int GetColorValue(PaletteColor color) + { + return Math.Max(Math.Max(color.R, color.G), color.B); + } + + private static int GetQuantizedColorKey(byte r, byte g, byte b) + { + int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml new file mode 100644 index 00000000..08b99cf5 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs new file mode 100644 index 00000000..81e05506 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -0,0 +1,441 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Applet; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Gpu; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.Input.SDL2; +using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common; +using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.Common.Helper; +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using InputManager = Ryujinx.Input.HLE.InputManager; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MainWindow : StyleableWindow + { + internal static MainWindowViewModel MainWindowViewModel { get; private set; } + + private bool _isLoading; + + private UserChannelPersistence _userChannelPersistence; + private static bool _deferLoad; + private static string _launchPath; + private static bool _startFullscreen; + internal readonly AvaHostUiHandler UiHandler; + + public VirtualFileSystem VirtualFileSystem { get; private set; } + public ContentManager ContentManager { get; private set; } + public AccountManager AccountManager { get; private set; } + + public LibHacHorizonManager LibHacHorizonManager { get; private set; } + + public InputManager InputManager { get; private set; } + + internal MainWindowViewModel ViewModel { get; private set; } + public SettingsWindow SettingsWindow { get; set; } + + public static bool ShowKeyErrorOnLoad { get; set; } + public ApplicationLibrary ApplicationLibrary { get; set; } + + public MainWindow() + { + ViewModel = new MainWindowViewModel(); + + MainWindowViewModel = ViewModel; + + DataContext = ViewModel; + + InitializeComponent(); + Load(); + + UiHandler = new AvaHostUiHandler(this); + + ViewModel.Title = $"Ryujinx {Program.Version}"; + + // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. + double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight; + Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; + Width /= Program.WindowScaleFactor; + + if (Program.PreviewerDetached) + { + Initialize(); + + InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + + ViewModel.Initialize( + ContentManager, + ApplicationLibrary, + VirtualFileSystem, + AccountManager, + InputManager, + _userChannelPersistence, + LibHacHorizonManager, + UiHandler, + ShowLoading, + SwitchToGameControl, + SetMainContent, + this); + + ViewModel.RefreshFirmwareStatus(); + + LoadGameList(); + + this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged); + } + + ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; + ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + ViewModel.ReloadGameList += ReloadGameList; + + NotificationHelper.SetNotificationManager(this); + } + + private void IsActiveChanged(bool obj) + { + ViewModel.IsActive = obj; + } + + public void LoadGameList() + { + if (_isLoading) + { + return; + } + + _isLoading = true; + + LoadApplications(); + + _isLoading = false; + } + + protected override void HandleScalingChanged(double scale) + { + Program.DesktopScaleFactor = scale; + base.HandleScalingChanged(scale); + } + + public void AddApplication(ApplicationData applicationData) + { + Dispatcher.UIThread.InvokeAsync(() => + { + ViewModel.Applications.Add(applicationData); + }); + } + + private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) + { + AddApplication(e.AppData); + } + + private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); + + Dispatcher.UIThread.Post(() => + { + ViewModel.StatusBarProgressValue = e.NumAppsLoaded; + ViewModel.StatusBarProgressMaximum = e.NumAppsFound; + + if (e.NumAppsFound == 0) + { + StatusBarView.LoadProgressBar.IsVisible = false; + } + + if (e.NumAppsLoaded == e.NumAppsFound) + { + StatusBarView.LoadProgressBar.IsVisible = false; + } + }); + } + + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) + { + if (args.Application != null) + { + ViewModel.SelectedIcon = args.Application.Icon; + + string path = new FileInfo(args.Application.Path).FullName; + + ViewModel.LoadApplication(path); + } + + args.Handled = true; + } + + internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg) + { + _deferLoad = true; + _launchPath = launchPathArg; + _startFullscreen = startFullscreenArg; + } + + public void SwitchToGameControl(bool startFullscreen = false) + { + ViewModel.ShowLoadProgress = false; + ViewModel.ShowContent = true; + ViewModel.IsLoadingIndeterminate = false; + + Dispatcher.UIThread.InvokeAsync(() => + { + if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + }); + } + + public void ShowLoading(bool startFullscreen = false) + { + ViewModel.ShowContent = false; + ViewModel.ShowLoadProgress = true; + ViewModel.IsLoadingIndeterminate = true; + + Dispatcher.UIThread.InvokeAsync(() => + { + if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + }); + } + + protected override void HandleWindowStateChanged(WindowState state) + { + ViewModel.WindowState = state; + + if (state != WindowState.Minimized) + { + Renderer.Start(); + } + } + + private void Initialize() + { + _userChannelPersistence = new UserChannelPersistence(); + VirtualFileSystem = VirtualFileSystem.CreateInstance(); + LibHacHorizonManager = new LibHacHorizonManager(); + ContentManager = new ContentManager(VirtualFileSystem); + + LibHacHorizonManager.InitializeFsServer(VirtualFileSystem); + LibHacHorizonManager.InitializeArpServer(); + LibHacHorizonManager.InitializeBcatServer(); + LibHacHorizonManager.InitializeSystemClients(); + + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); + + // Save data created before we supported extra data in directory save data will not work properly if + // given empty extra data. Luckily some of that extra data can be created using the data from the + // save data indexer, which should be enough to check access permissions for user saves. + // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. + // Consider removing this at some point in the future when we don't need to worry about old saves. + VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient); + + AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile); + + VirtualFileSystem.ReloadKeySet(); + + ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this); + } + + protected void CheckLaunchState() + { + if (ShowKeyErrorOnLoad) + { + ShowKeyErrorOnLoad = false; + + Dispatcher.UIThread.Post(async () => await + UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, this)); + } + + if (_deferLoad) + { + _deferLoad = false; + + ViewModel.LoadApplication(_launchPath, _startFullscreen); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) + { + Updater.BeginParse(this, false).ContinueWith(task => + { + Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + } + + private void Load() + { + StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; + + GameGrid.ApplicationOpened += Application_Opened; + + GameGrid.DataContext = ViewModel; + + GameList.ApplicationOpened += Application_Opened; + + GameList.DataContext = ViewModel; + + LoadHotKeys(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + CheckLaunchState(); + } + + private void SetMainContent(Control content = null) + { + if (content == null) + { + content = GameLibrary; + } + + if (MainContent.Content != content) + { + MainContent.Content = content; + } + } + + public static void UpdateGraphicsConfig() + { + GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale; + GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; + GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; + GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; + GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; + GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; + } + + public void LoadHotKeys() + { + HotKeyManager.SetHotKey(FullscreenHotKey, new KeyGesture(Key.Enter, KeyModifiers.Alt)); + HotKeyManager.SetHotKey(FullscreenHotKey2, new KeyGesture(Key.F11)); + HotKeyManager.SetHotKey(FullscreenHotKeyMacOS, new KeyGesture(Key.F, KeyModifiers.Control | KeyModifiers.Meta)); + HotKeyManager.SetHotKey(DockToggleHotKey, new KeyGesture(Key.F9)); + HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape)); + } + + private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e) + { + var volumeSplitButton = sender as ToggleSplitButton; + if (ViewModel.IsGameRunning) + { + if (!volumeSplitButton.IsChecked) + { + ViewModel.AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); + } + else + { + ViewModel.AppHost.Device.SetVolume(0); + } + + ViewModel.Volume = ViewModel.AppHost.Device.GetVolume(); + } + } + + protected override void OnClosing(CancelEventArgs e) + { + if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit) + { + e.Cancel = true; + + ConfirmExit(); + + return; + } + + ViewModel.IsClosing = true; + + if (ViewModel.AppHost != null) + { + ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit; + ViewModel.AppHost.AppExit += (sender, e) => + { + ViewModel.AppHost = null; + + Dispatcher.UIThread.Post(() => + { + MainContent = null; + + Close(); + }); + }; + ViewModel.AppHost?.Stop(); + + e.Cancel = true; + + return; + } + + ApplicationLibrary.CancelLoading(); + InputManager.Dispose(); + Program.Exit(); + + base.OnClosing(e); + } + + private void ConfirmExit() + { + Dispatcher.UIThread.InvokeAsync(async () => + { + ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); + + if (ViewModel.IsClosing) + { + Close(); + } + }); + } + + public async void LoadApplications() + { + await Dispatcher.UIThread.InvokeAsync(() => + { + ViewModel.Applications.Clear(); + + StatusBarView.LoadProgressBar.IsVisible = true; + ViewModel.StatusBarProgressMaximum = 0; + ViewModel.StatusBarProgressValue = 0; + + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); + }); + + ReloadGameList(); + } + + private void ReloadGameList() + { + if (_isLoading) + { + return; + } + + _isLoading = true; + + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language); + + _isLoading = false; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml b/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml new file mode 100644 index 00000000..862998ac --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs new file mode 100644 index 00000000..c686e7c3 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs @@ -0,0 +1,71 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MotionSettingsWindow : UserControl + { + private readonly InputConfiguration _viewmodel; + + public MotionSettingsWindow() + { + InitializeComponent(); + DataContext = _viewmodel; + } + + public MotionSettingsWindow(ControllerSettingsViewModel viewmodel) + { + var config = viewmodel.Configuration as InputConfiguration; + + _viewmodel = new InputConfiguration() + { + Slot = config.Slot, + AltSlot = config.AltSlot, + DsuServerHost = config.DsuServerHost, + DsuServerPort = config.DsuServerPort, + MirrorInput = config.MirrorInput, + EnableMotion = config.EnableMotion, + Sensitivity = config.Sensitivity, + GyroDeadzone = config.GyroDeadzone, + EnableCemuHookMotion = config.EnableCemuHookMotion + }; + + InitializeComponent(); + DataContext = _viewmodel; + } + + public static async Task Show(ControllerSettingsViewModel viewmodel) + { + MotionSettingsWindow content = new MotionSettingsWindow(viewmodel); + + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance[LocaleKeys.ControllerMotionTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsClose], + Content = content + }; + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewmodel.Configuration as InputConfiguration; + config.Slot = content._viewmodel.Slot; + config.EnableMotion = content._viewmodel.EnableMotion; + config.Sensitivity = content._viewmodel.Sensitivity; + config.GyroDeadzone = content._viewmodel.GyroDeadzone; + config.AltSlot = content._viewmodel.AltSlot; + config.DsuServerHost = content._viewmodel.DsuServerHost; + config.DsuServerPort = content._viewmodel.DsuServerPort; + config.EnableCemuHookMotion = content._viewmodel.EnableCemuHookMotion; + config.MirrorInput = content._viewmodel.MirrorInput; + }; + + await contentDialog.ShowAsync(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml b/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml new file mode 100644 index 00000000..e47cc5bd --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs new file mode 100644 index 00000000..178109d6 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class RumbleSettingsWindow : UserControl + { + private readonly InputConfiguration _viewmodel; + + public RumbleSettingsWindow() + { + InitializeComponent(); + DataContext = _viewmodel; + } + + public RumbleSettingsWindow(ControllerSettingsViewModel viewmodel) + { + var config = viewmodel.Configuration as InputConfiguration; + + _viewmodel = new InputConfiguration() + { + StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble + }; + + InitializeComponent(); + DataContext = _viewmodel; + } + + public static async Task Show(ControllerSettingsViewModel viewmodel) + { + RumbleSettingsWindow content = new RumbleSettingsWindow(viewmodel); + + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance[LocaleKeys.ControllerRumbleTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsClose], + Content = content, + }; + + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewmodel.Configuration as InputConfiguration; + config.StrongRumble = content._viewmodel.StrongRumble; + config.WeakRumble = content._viewmodel.WeakRumble; + }; + + await contentDialog.ShowAsync(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml new file mode 100644 index 00000000..a44cbfe7 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs new file mode 100644 index 00000000..153ce95d --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -0,0 +1,96 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class TitleUpdateWindow : UserControl + { + public TitleUpdateViewModel ViewModel; + + public TitleUpdateWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId, titleName); + + InitializeComponent(); + } + + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new TitleUpdateWindow(virtualFileSystem, titleId, titleName), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")) + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + public void Save(object sender, RoutedEventArgs e) + { + ViewModel.Save(); + + if (VisualRoot is MainWindow window) + { + window.ViewModel.LoadApplications(); + } + + ((ContentDialog)Parent).Hide(); + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is TitleUpdateModel model) + { + OpenHelper.LocateFile(model.Path); + } + } + } + + private void RemoveUpdate(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + ViewModel.RemoveUpdate((TitleUpdateModel)button.DataContext); + } + } + + private void RemoveAll(object sender, RoutedEventArgs e) + { + ViewModel.TitleUpdates.Clear(); + + ViewModel.SortUpdates(); + } + } +} \ No newline at end of file -- cgit v1.2.3