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/AmiiboWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
new file mode 100644
index 00000000..206d0a7e
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
@@ -0,0 +1,59 @@
+using Avalonia.Interactivity;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ui.Common.Models.Amiibo;
+
+namespace Ryujinx.Ava.UI.Windows
+{
+ public partial class AmiiboWindow : StyleableWindow
+ {
+ public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
+ {
+ ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
+
+ ViewModel.ShowAllAmiibo = showAll;
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.Amiibo];
+ }
+
+ public AmiiboWindow()
+ {
+ ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+
+ if (Program.PreviewerDetached)
+ {
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.Amiibo];
+ }
+ }
+
+ public bool IsScanned { get; set; }
+ public AmiiboApi ScannedAmiibo { get; set; }
+ public AmiiboWindowViewModel ViewModel { get; set; }
+
+ private void ScanButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (ViewModel.AmiiboSelectedIndex > -1)
+ {
+ AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
+ ScannedAmiibo = amiibo;
+ IsScanned = true;
+ Close();
+ }
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ IsScanned = false;
+
+ Close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
new file mode 100644
index 00000000..3557ed69
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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/SettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
new file mode 100644
index 00000000..bdf7e94d
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
@@ -0,0 +1,103 @@
+using FluentAvalonia.Core;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using System;
+using System.ComponentModel;
+
+namespace Ryujinx.Ava.UI.Windows
+{
+ public partial class SettingsWindow : StyleableWindow
+ {
+ internal SettingsViewModel ViewModel { get; set; }
+
+ public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager)
+ {
+ Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.Settings]}";
+
+ ViewModel = new SettingsViewModel(virtualFileSystem, contentManager);
+ DataContext = ViewModel;
+
+ ViewModel.CloseWindow += Close;
+ ViewModel.SaveSettingsEvent += SaveSettings;
+
+ InitializeComponent();
+ Load();
+ }
+
+ public SettingsWindow()
+ {
+ ViewModel = new SettingsViewModel();
+ DataContext = ViewModel;
+
+ InitializeComponent();
+ Load();
+ }
+
+ public void SaveSettings()
+ {
+ InputPage.ControllerSettings?.SaveCurrentProfile();
+
+ if (Owner is MainWindow window && ViewModel.DirectoryChanged)
+ {
+ window.ViewModel.LoadApplications();
+ }
+ }
+
+ private void Load()
+ {
+ Pages.Children.Clear();
+ NavPanel.SelectionChanged += NavPanelOnSelectionChanged;
+ NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
+ }
+
+ private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
+ {
+ if (e.SelectedItem is NavigationViewItem navItem && navItem.Tag is not null)
+ {
+ switch (navItem.Tag.ToString())
+ {
+ case "UiPage":
+ UiPage.ViewModel = ViewModel;
+ NavPanel.Content = UiPage;
+ break;
+ case "InputPage":
+ NavPanel.Content = InputPage;
+ break;
+ case "HotkeysPage":
+ NavPanel.Content = HotkeysPage;
+ break;
+ case "SystemPage":
+ SystemPage.ViewModel = ViewModel;
+ NavPanel.Content = SystemPage;
+ break;
+ case "CpuPage":
+ NavPanel.Content = CpuPage;
+ break;
+ case "GraphicsPage":
+ NavPanel.Content = GraphicsPage;
+ break;
+ case "AudioPage":
+ NavPanel.Content = AudioPage;
+ break;
+ case "NetworkPage":
+ NavPanel.Content = NetworkPage;
+ break;
+ case "LoggingPage":
+ NavPanel.Content = LoggingPage;
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ HotkeysPage.Dispose();
+ InputPage.Dispose();
+ base.OnClosing(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs b/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs
new file mode 100644
index 00000000..a157f154
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs
@@ -0,0 +1,39 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace Ryujinx.Ava.UI.Windows
+{
+ public class StyleableWindow : Window
+ {
+ public IBitmap IconImage { get; set; }
+
+ public StyleableWindow()
+ {
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ TransparencyLevelHint = WindowTransparencyLevel.None;
+
+ using Stream stream = Assembly.GetAssembly(typeof(Ryujinx.Ui.Common.Configuration.ConfigurationState)).GetManifestResourceStream("Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ Icon = new WindowIcon(stream);
+ stream.Position = 0;
+ IconImage = new Bitmap(stream);
+ }
+
+ protected override void OnOpened(EventArgs e)
+ {
+ base.OnOpened(e);
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml
new file mode 100644
index 00000000..e9858038
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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