From eeb2af9953f48479c3a902664f31634e6a2148be Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sun, 8 Jan 2023 12:46:25 -0500 Subject: [PATCH] Ava GUI: `MainWindow` Refactor (#4178) * Fix redundancies * Add back elses * `MainWindow` Refactor * Switch commands to `ReflectionBinding` Not required in Ava 11 * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/AppHost.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs Co-authored-by: Ac_K * Resolve issues * Remove Ava 11 Fix * Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs Co-authored-by: gdkchan * Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs Co-authored-by: gdkchan * Fix whitespace + other suggestions * Move Vsync colours to `Styles.xaml` * Remove catch all * Use `switch` instead of `if` * Update locale keys * Use block-scoped namespaces * Fix improper Ava api usage then * Static PTC * Fix `GridItemSelectorSize` with `ShowNames` * Update for new About Window * Add back search fix Co-authored-by: Ac_K Co-authored-by: gdkchan --- Ryujinx.Ava/AppHost.cs | 205 ++- Ryujinx.Ava/Assets/Styles/Styles.xaml | 2 + Ryujinx.Ava/Input/AvaloniaMouseDriver.cs | 10 +- Ryujinx.Ava/Modules/Updater/Updater.cs | 4 +- Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs | 4 +- .../Applet/AvaloniaDynamicTextInputHandler.cs | 2 +- Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs | 4 +- .../ViewModels/ControllerSettingsViewModel.cs | 8 +- .../UI/ViewModels/MainWindowViewModel.cs | 1594 +++++++++++------ .../UI/Views/Main/MainMenuBarView.axaml | 216 +++ .../UI/Views/Main/MainMenuBarView.axaml.cs | 146 ++ .../UI/Views/Main/MainStatusBarView.axaml | 232 +++ .../UI/Views/Main/MainStatusBarView.axaml.cs | 52 + .../UI/Views/Main/MainViewControls.axaml | 176 ++ .../UI/Views/Main/MainViewControls.axaml.cs | 54 + Ryujinx.Ava/UI/Windows/MainWindow.axaml | 606 +------ Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs | 535 ++---- 17 files changed, 2153 insertions(+), 1697 deletions(-) create mode 100644 Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml create mode 100644 Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml create mode 100644 Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml create mode 100644 Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs index 2cf53ef691..5a0910e870 100644 --- a/Ryujinx.Ava/AppHost.cs +++ b/Ryujinx.Ava/AppHost.cs @@ -1,4 +1,6 @@ using ARMeilleure.Translation; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Threading; using LibHac.Tools.FsSystem; @@ -13,6 +15,7 @@ using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -42,7 +45,7 @@ using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; - +using Image = SixLabors.ImageSharp.Image; using InputManager = Ryujinx.Input.HLE.InputManager; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; @@ -67,7 +70,7 @@ namespace Ryujinx.Ava private readonly AccountManager _accountManager; private readonly UserChannelPersistence _userChannelPersistence; private readonly InputManager _inputManager; - private readonly MainWindow _parent; + private readonly MainWindowViewModel _viewModel; private readonly IKeyboard _keyboardInterface; private readonly GraphicsDebugLevel _glLogLevel; @@ -110,6 +113,7 @@ namespace Ryujinx.Ava public bool ScreenshotRequested { get; set; } private object _lockObject = new(); + private TopLevel _topLevel; public AppHost( RendererHost renderer, @@ -119,9 +123,10 @@ namespace Ryujinx.Ava ContentManager contentManager, AccountManager accountManager, UserChannelPersistence userChannelPersistence, - MainWindow parent) + MainWindowViewModel viewmodel, + TopLevel topLevel) { - _parent = parent; + _viewModel = viewmodel; _inputManager = inputManager; _accountManager = accountManager; _userChannelPersistence = userChannelPersistence; @@ -129,7 +134,8 @@ namespace Ryujinx.Ava _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle; _lastCursorMoveTime = Stopwatch.GetTimestamp(); _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel; - _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_parent, renderer)); + _topLevel = topLevel; + _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer)); _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0"); NpadManager = _inputManager.CreateNpadManager(); @@ -144,15 +150,15 @@ namespace Ryujinx.Ava if (ApplicationPath.StartsWith("@SystemContent")) { - ApplicationPath = _parent.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath); + ApplicationPath = _viewModel.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath); _isFirmwareTitle = true; } ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed; - _parent.PointerLeave += Parent_PointerLeft; - _parent.PointerMoved += Parent_PointerMoved; + _topLevel.PointerLeave += TopLevel_PointerLeave; + _topLevel.PointerMoved += TopLevel_PointerMoved; ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; @@ -162,25 +168,27 @@ namespace Ryujinx.Ava _gpuCancellationTokenSource = new CancellationTokenSource(); } - private void Parent_PointerMoved(object sender, PointerEventArgs e) + private void TopLevel_PointerMoved(object sender, PointerEventArgs e) { - _lastCursorMoveTime = Stopwatch.GetTimestamp(); - var p = e.GetCurrentPoint(_parent).Position; - var r = _parent.InputHitTest(p); - _isMouseInRenderer = r == Renderer; + if (sender is Control visual) + { + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + var point = e.GetCurrentPoint(visual).Position; + _isMouseInRenderer = Equals(visual.InputHitTest(point), Renderer); + } } - private void Parent_PointerLeft(object sender, PointerEventArgs e) + private void TopLevel_PointerLeave(object sender, PointerEventArgs e) { _isMouseInRenderer = false; - _parent.Cursor = Cursor.Default; + _viewModel.Cursor = Cursor.Default; } private void SetRendererWindowSize(Size size) { if (_renderer != null) { - double scale = _parent.PlatformImpl.RenderScaling; + double scale = _topLevel.PlatformImpl.RenderScaling; _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale)); } } @@ -256,7 +264,7 @@ namespace Ryujinx.Ava NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); TouchScreenManager.Initialize(Device); - _parent.ViewModel.IsGameRunning = true; + _viewModel.IsGameRunning = true; string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty @@ -276,10 +284,10 @@ namespace Ryujinx.Ava Dispatcher.UIThread.InvokeAsync(() => { - _parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; + _viewModel.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; }); - _parent.ViewModel.SetUiProgressHandlers(Device); + _viewModel.SetUIProgressHandlers(Device); Renderer.SizeChanged += Window_SizeChanged; @@ -287,7 +295,7 @@ namespace Ryujinx.Ava _renderingThread.Start(); - _parent.ViewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value; + _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value; MainLoop(); @@ -321,7 +329,7 @@ namespace Ryujinx.Ava Dispatcher.UIThread.Post(() => { var value = e.NewValue; - _parent.ViewModel.Volume = e.NewValue; + _viewModel.Volume = e.NewValue; }); } @@ -369,7 +377,7 @@ namespace Ryujinx.Ava { if (Device.Application != null) { - _parent.UpdateGameMetadata(Device.Application.TitleIdText); + _viewModel.UpdateGameMetadata(Device.Application.TitleIdText); } ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; @@ -377,6 +385,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState; + _topLevel.PointerLeave -= TopLevel_PointerLeave; + _topLevel.PointerMoved -= TopLevel_PointerMoved; + _gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Dispose(); @@ -410,7 +421,7 @@ namespace Ryujinx.Ava } else { - _parent.Cursor = Cursor.Default; + _viewModel.Cursor = Cursor.Default; } }); } @@ -422,57 +433,65 @@ namespace Ryujinx.Ava SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); - if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError)) + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion)) + if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError)) { - if (userError == UserError.NoFirmware) { - UserResult result = await ContentDialogHelper.CreateConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage], - string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage], firmwareVersion.VersionString), - LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], - ""); - - if (result != UserResult.Yes) + if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion)) { - await UserErrorDialog.ShowUserErrorDialog(userError, _parent); + if (userError == UserError.NoFirmware) + { + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage], + string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage], + firmwareVersion.VersionString), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + ""); + + if (result != UserResult.Yes) + { + await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow)); + Device.Dispose(); + + return false; + } + } + + if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _)) + { + await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow)); + Device.Dispose(); + + return false; + } + + // Tell the user that we installed a firmware for them. + if (userError == UserError.NoFirmware) + { + firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); + + _viewModel.RefreshFirmwareStatus(); + + await ContentDialogHelper.CreateInfoDialog( + string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage], + firmwareVersion.VersionString), + string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage], + firmwareVersion.VersionString), + LocaleManager.Instance[LocaleKeys.InputDialogOk], + "", + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + } + } + else + { + await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow)); Device.Dispose(); return false; } } - - if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _)) - { - await UserErrorDialog.ShowUserErrorDialog(userError, _parent); - Device.Dispose(); - - return false; - } - - // Tell the user that we installed a firmware for them. - if (userError == UserError.NoFirmware) - { - firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); - - _parent.RefreshFirmwareStatus(); - - await ContentDialogHelper.CreateInfoDialog( - string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage], firmwareVersion.VersionString), - string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage], firmwareVersion.VersionString), - LocaleManager.Instance[LocaleKeys.InputDialogOk], - "", - LocaleManager.Instance[LocaleKeys.RyujinxInfo]); - } - } - else - { - await UserErrorDialog.ShowUserErrorDialog(userError, _parent); - Device.Dispose(); - - return false; } } @@ -567,7 +586,7 @@ namespace Ryujinx.Ava DiscordIntegrationModule.SwitchToPlayingState(Device.Application.TitleIdText, Device.Application.TitleName); - _parent.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata => + _viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata => { appMetadata.LastPlayed = DateTime.UtcNow.ToString(); }); @@ -578,13 +597,13 @@ namespace Ryujinx.Ava internal void Resume() { Device?.System.TogglePauseEmulation(false); - _parent.ViewModel.IsPaused = false; + _viewModel.IsPaused = false; } internal void Pause() { Device?.System.TogglePauseEmulation(true); - _parent.ViewModel.IsPaused = true; + _viewModel.IsPaused = true; } private void InitializeSwitchInstance() @@ -632,7 +651,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new OpenALHardwareDeviceDriver(); } @@ -645,7 +664,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new SoundIoHardwareDeviceDriver(); } @@ -671,7 +690,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new SDL2HardwareDeviceDriver(); } @@ -684,7 +703,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new OpenALHardwareDeviceDriver(); } @@ -710,7 +729,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new SDL2HardwareDeviceDriver(); } @@ -723,7 +742,7 @@ namespace Ryujinx.Ava Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; - MainWindow.SaveConfig(); + MainWindowViewModel.SaveConfig(); deviceDriver = new SoundIoHardwareDeviceDriver(); } @@ -740,14 +759,14 @@ namespace Ryujinx.Ava IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(VirtualFileSystem, - _parent.LibHacHorizonManager, + _viewModel.LibHacHorizonManager, ContentManager, _accountManager, _userChannelPersistence, renderer, deviceDriver, memoryConfiguration, - _parent.UiHandler, + _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, ConfigurationState.Instance.Graphics.EnableVsync, @@ -788,14 +807,14 @@ namespace Ryujinx.Ava { Dispatcher.UIThread.InvokeAsync(() => { - if (_parent.ViewModel.StartGamesInFullscreen) + if (_viewModel.StartGamesInFullscreen) { - _parent.WindowState = WindowState.FullScreen; + _viewModel.WindowState = WindowState.FullScreen; } - if (_parent.WindowState == WindowState.FullScreen) + if (_viewModel.WindowState == WindowState.FullScreen) { - _parent.ViewModel.ShowMenuAndStatusBar = false; + _viewModel.ShowMenuAndStatusBar = false; } }); @@ -819,7 +838,7 @@ namespace Ryujinx.Ava Width = (int)Renderer.Bounds.Width; Height = (int)Renderer.Bounds.Height; - _renderer.Window.SetSize((int)(Width * _parent.PlatformImpl.RenderScaling), (int)(Height * _parent.PlatformImpl.RenderScaling)); + _renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling)); _chrono.Start(); @@ -847,7 +866,7 @@ namespace Ryujinx.Ava if (!_renderingStarted) { _renderingStarted = true; - _parent.SwitchToGameControl(); + _viewModel.SwitchToRenderer(false); } Device.PresentFrame(() => Renderer?.SwapBuffers()); @@ -914,7 +933,7 @@ namespace Ryujinx.Ava { Dispatcher.UIThread.Post(() => { - _parent.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default; + _viewModel.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default; }); } else @@ -925,7 +944,7 @@ namespace Ryujinx.Ava Dispatcher.UIThread.Post(() => { - _parent.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default; + _viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default; }); } } @@ -938,13 +957,13 @@ namespace Ryujinx.Ava return false; } - if (_parent.IsActive) + if (_viewModel.IsActive) { Dispatcher.UIThread.Post(() => { HandleScreenState(); - if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _parent.WindowState != WindowState.FullScreen) + if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen) { Device.Application.DiskCacheLoadState?.Cancel(); } @@ -953,7 +972,7 @@ namespace Ryujinx.Ava NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); - if (_parent.IsActive) + if (_viewModel.IsActive) { KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); @@ -969,10 +988,10 @@ namespace Ryujinx.Ava ScreenshotRequested = true; break; case KeyboardHotkeyState.ShowUi: - _parent.ViewModel.ShowMenuAndStatusBar = true; + _viewModel.ShowMenuAndStatusBar = true; break; case KeyboardHotkeyState.Pause: - if (_parent.ViewModel.IsPaused) + if (_viewModel.IsPaused) { Resume(); } @@ -991,7 +1010,7 @@ namespace Ryujinx.Ava Device.SetVolume(0); } - _parent.ViewModel.Volume = Device.GetVolume(); + _viewModel.Volume = Device.GetVolume(); break; case KeyboardHotkeyState.ResScaleUp: GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1; @@ -1004,13 +1023,13 @@ namespace Ryujinx.Ava _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2); Device.SetVolume(_newVolume); - _parent.ViewModel.Volume = Device.GetVolume(); + _viewModel.Volume = Device.GetVolume(); break; case KeyboardHotkeyState.VolumeDown: _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2); Device.SetVolume(_newVolume); - _parent.ViewModel.Volume = Device.GetVolume(); + _viewModel.Volume = Device.GetVolume(); break; case KeyboardHotkeyState.None: (_keyboardInterface as AvaloniaKeyboard).Clear(); @@ -1030,7 +1049,7 @@ namespace Ryujinx.Ava // Touchscreen bool hasTouch = false; - if (_parent.IsActive && !ConfigurationState.Instance.Hid.EnableMouse) + if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse) { hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); } diff --git a/Ryujinx.Ava/Assets/Styles/Styles.xaml b/Ryujinx.Ava/Assets/Styles/Styles.xaml index de965a2a0b..c5e760e81d 100644 --- a/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -269,6 +269,8 @@ #FF00FABB #FF2D2D2D #FF505050 + #FF2EEAC9 + #FFFF4554 15 8 10 diff --git a/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs b/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs index eb58752cee..b0b6cdf05a 100644 --- a/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs +++ b/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs @@ -11,10 +11,10 @@ namespace Ryujinx.Ava.Input { internal class AvaloniaMouseDriver : IGamepadDriver { - private Control _widget; - private bool _isDisposed; - private Size _size; - private readonly Window _window; + private Control _widget; + private bool _isDisposed; + private Size _size; + private readonly TopLevel _window; public bool[] PressedButtons { get; } public Vector2 CurrentPosition { get; private set; } @@ -23,7 +23,7 @@ namespace Ryujinx.Ava.Input public string DriverName => "AvaloniaMouseDriver"; public ReadOnlySpan GamepadsIds => new[] { "0" }; - public AvaloniaMouseDriver(Window window, Control parent) + public AvaloniaMouseDriver(TopLevel window, Control parent) { _widget = parent; _window = window; diff --git a/Ryujinx.Ava/Modules/Updater/Updater.cs b/Ryujinx.Ava/Modules/Updater/Updater.cs index bc4760bae8..a7399407fa 100644 --- a/Ryujinx.Ava/Modules/Updater/Updater.cs +++ b/Ryujinx.Ava/Modules/Updater/Updater.cs @@ -56,7 +56,7 @@ namespace Ryujinx.Modules } Running = true; - mainWindow.CanUpdate = false; + mainWindow.ViewModel.CanUpdate = false; // Detect current platform if (OperatingSystem.IsMacOS()) @@ -182,7 +182,7 @@ namespace Ryujinx.Modules } Running = false; - mainWindow.CanUpdate = true; + mainWindow.ViewModel.CanUpdate = true; return; } diff --git a/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs b/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs index f4d9bc8066..0809cb4f8d 100644 --- a/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs +++ b/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs @@ -144,9 +144,9 @@ namespace Ryujinx.Ava.UI.Applet public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value) { device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value); - if (_parent.AppHost != null) + if (_parent.ViewModel.AppHost != null) { - _parent.AppHost.Stop(); + _parent.ViewModel.AppHost.Stop(); } } diff --git a/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs index 314746e76e..bae4762ebe 100644 --- a/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs @@ -136,7 +136,7 @@ namespace Ryujinx.Ava.UI.Applet Dispatcher.UIThread.Post(() => { _hiddenTextBox.Clear(); - _parent.RendererControl.Focus(); + _parent.ViewModel.RendererControl.Focus(); _parent = null; }); diff --git a/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs b/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs index fe5e272101..77c7a2d2cc 100644 --- a/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs +++ b/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs @@ -13,8 +13,8 @@ namespace Ryujinx.Ava.UI.Applet DefaultBackgroundColor = BrushToThemeColor(parent.Background); DefaultForegroundColor = BrushToThemeColor(parent.Foreground); DefaultBorderColor = BrushToThemeColor(parent.BorderBrush); - SelectionBackgroundColor = BrushToThemeColor(parent.SearchBox.SelectionBrush); - SelectionForegroundColor = BrushToThemeColor(parent.SearchBox.SelectionForegroundBrush); + SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush); + SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush); } public string FontFamily { get; } diff --git a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs index 6a2afad1fc..82a75788de 100644 --- a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs @@ -244,9 +244,9 @@ namespace Ryujinx.Ava.UI.ViewModels _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; - if (_mainWindow.AppHost != null) + if (_mainWindow.ViewModel.AppHost != null) { - _mainWindow.AppHost.NpadManager.BlockInputUpdates(); + _mainWindow.ViewModel.AppHost.NpadManager.BlockInputUpdates(); } _isLoaded = false; @@ -862,7 +862,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - _mainWindow.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + _mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); // Atomically replace and signal input change. // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. @@ -891,7 +891,7 @@ namespace Ryujinx.Ava.UI.ViewModels _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; - _mainWindow.AppHost?.NpadManager.UnblockInputUpdates(); + _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); SelectedGamepad?.Dispose(); diff --git a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 514a8bb358..9571af477b 100644 --- a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,5 @@ -using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Media; using Avalonia.Threading; @@ -7,12 +7,12 @@ using DynamicData; using DynamicData.Binding; using LibHac.Fs; using LibHac.FsSystem; -using LibHac.Ncm; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -21,11 +21,13 @@ using Ryujinx.Cpu; using Ryujinx.HLE; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; -using Ryujinx.Modules; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.Ui; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -35,14 +37,14 @@ using System.Threading; using System.Threading.Tasks; using Path = System.IO.Path; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; +using UserId = LibHac.Fs.UserId; namespace Ryujinx.Ava.UI.ViewModels { - internal class MainWindowViewModel : BaseModel + public class MainWindowViewModel : BaseModel { private const int HotKeyPressDelayMs = 500; - private readonly MainWindow _owner; private ObservableCollection _applications; private string _aspectStatusText; @@ -57,7 +59,7 @@ namespace Ryujinx.Ava.UI.ViewModels private string _gpuStatusText; private bool _isAmiiboRequested; private bool _isGameRunning; - private bool _isLoading; + private bool _isFullScreen; private int _progressMaximum; private int _progressValue; private long _lastFullscreenToggle = Environment.TickCount64; @@ -76,15 +78,29 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _isLoadingIndeterminate = true; private bool _showAll; private string _lastScannedAmiiboId; + private bool _statusBarVisible; private ReadOnlyObservableCollection _appsObservableList; - public ApplicationLibrary ApplicationLibrary => _owner.ApplicationLibrary; - public string TitleName { get; internal set; } + private string _showUiKey = "F4"; + private string _pauseKey = "F5"; + private string _screenshotKey = "F8"; + private float _volume; + private string _backendText; - public MainWindowViewModel(MainWindow owner) : this() - { - _owner = owner; - } + public ApplicationData ListSelectedApplication; + public ApplicationData GridSelectedApplication; + private bool _canUpdate; + private Cursor _cursor; + private string _title; + private string _currentEmulatedGamePath; + private AutoResetEvent _rendererWaitEvent; + private WindowState _windowState; + private bool _isActive; + + public event Action ReloadGameList; + + private string TitleName { get; set; } + internal AppHost AppHost { get; set; } public MainWindowViewModel() { @@ -95,6 +111,8 @@ namespace Ryujinx.Ava.UI.ViewModels .Sort(GetComparer()) .Bind(out _appsObservableList).AsObservableList(); + _rendererWaitEvent = new AutoResetEvent(false); + if (Program.PreviewerDetached) { LoadConfigurableHotKeys(); @@ -103,12 +121,37 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public void Initialize() + public void Initialize( + ContentManager contentManager, + ApplicationLibrary applicationLibrary, + VirtualFileSystem virtualFileSystem, + AccountManager accountManager, + Ryujinx.Input.HLE.InputManager inputManager, + UserChannelPersistence userChannelPersistence, + LibHacHorizonManager libHacHorizonManager, + IHostUiHandler uiHandler, + Action showLoading, + Action switchToGameControl, + Action setMainContent, + TopLevel topLevel) { - ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; - ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + ContentManager = contentManager; + ApplicationLibrary = applicationLibrary; + VirtualFileSystem = virtualFileSystem; + AccountManager = accountManager; + InputManager = inputManager; + UserChannelPersistence = userChannelPersistence; + LibHacHorizonManager = libHacHorizonManager; + UiHandler = uiHandler; + + ShowLoading = showLoading; + SwitchToGameControl = switchToGameControl; + SetMainContent = setMainContent; + TopLevel = topLevel; } +#region Properties + public string SearchText { get => _searchText; @@ -130,6 +173,27 @@ namespace Ryujinx.Ava.UI.ViewModels _searchTimer = null; } + public bool CanUpdate + { + get => _canUpdate; + set + { + _canUpdate = value; + + OnPropertyChanged(); + } + } + + public Cursor Cursor + { + get => _cursor; + set + { + _cursor = value; + OnPropertyChanged(); + } + } + public ReadOnlyObservableCollection AppsObservableList { get => _appsObservableList; @@ -152,6 +216,28 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public long LastFullscreenToggle + { + get => _lastFullscreenToggle; + set + { + _lastFullscreenToggle = value; + + OnPropertyChanged(); + } + } + + public bool StatusBarVisible + { + get => _statusBarVisible && EnableNonGameRunningControls; + set + { + _statusBarVisible = value; + + OnPropertyChanged(); + } + } + public bool EnableNonGameRunningControls => !IsGameRunning; public bool ShowFirmwareStatus => !ShowLoadProgress; @@ -170,6 +256,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); OnPropertyChanged(nameof(EnableNonGameRunningControls)); + OnPropertyChanged(nameof(StatusBarVisible)); OnPropertyChanged(nameof(ShowFirmwareStatus)); } } @@ -208,11 +295,38 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private string _showUikey = "F4"; - private string _pauseKey = "F5"; - private string _screenshotkey = "F8"; - private float _volume; - private string _backendText; + public bool IsFullScreen + { + get => _isFullScreen; + set + { + _isFullScreen = value; + + OnPropertyChanged(); + } + } + + public bool ShowAll + { + get => _showAll; + set + { + _showAll = value; + + OnPropertyChanged(); + } + } + + public string LastScannedAmiiboId + { + get => _lastScannedAmiiboId; + set + { + _lastScannedAmiiboId = value; + + OnPropertyChanged(); + } + } public ApplicationData SelectedApplication { @@ -220,9 +334,9 @@ namespace Ryujinx.Ava.UI.ViewModels { return Glyph switch { - Glyph.List => _owner.GameList.SelectedApplication, - Glyph.Grid => _owner.GameGrid.SelectedApplication, - _ => null, + Glyph.List => ListSelectedApplication, + Glyph.Grid => GridSelectedApplication, + _ => null, }; } } @@ -414,7 +528,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (_isGameRunning) { - _owner.AppHost.Device.SetVolume(_volume); + AppHost.Device.SetVolume(_volume); } OnPropertyChanged(nameof(VolumeStatusText)); @@ -456,6 +570,18 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool IsActive + { + get => _isActive; + set + { + _isActive = value; + + OnPropertyChanged(); + } + } + + public bool ShowContent { get => _showContent; @@ -478,6 +604,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public WindowState WindowState + { + get => _windowState; + internal set + { + _windowState = value; + + OnPropertyChanged(); + } + } + public bool IsGrid => Glyph == Glyph.Grid; public bool IsList => Glyph == Glyph.List; @@ -495,44 +632,6 @@ namespace Ryujinx.Ava.UI.ViewModels RefreshView(); } - private IComparer GetComparer() - { - return SortMode switch - { - ApplicationSort.LastPlayed => new Models.Generic.LastPlayedSortComparer(IsAscending), - ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes) - : SortExpressionComparer.Descending(app => app.FileSizeBytes), - ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum) - : SortExpressionComparer.Descending(app => app.TimePlayedNum), - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) - : SortExpressionComparer.Descending(app => app.TitleName), - ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite) - : SortExpressionComparer.Descending(app => app.Favorite), - ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) - : SortExpressionComparer.Descending(app => app.Developer), - ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) - : SortExpressionComparer.Descending(app => app.FileExtension), - ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) - : SortExpressionComparer.Descending(app => app.Path), - _ => null, - }; - } - - private void RefreshView() - { - RefreshGrid(); - } - - private void RefreshGrid() - { - Applications.ToObservableChangeSet() - .Filter(Filter) - .Sort(GetComparer()) - .Bind(out _appsObservableList).AsObservableList(); - - OnPropertyChanged(nameof(AppsObservableList)); - } - public bool StartGamesInFullscreen { get => ConfigurationState.Instance.Ui.StartFullscreen; @@ -559,6 +658,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public string Title + { + get => _title; + set + { + _title = value; + + OnPropertyChanged(); + } + } + public bool ShowConsoleVisible { get => ConsoleHelper.SetConsoleWindowStateSupported; @@ -598,6 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); OnPropertyChanged(nameof(GridSizeScale)); + OnPropertyChanged(nameof(GridItemSelectorSize)); ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); } @@ -617,14 +728,70 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; - public bool IsSortedByTitle => SortMode == ApplicationSort.Title; - public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; - public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; - public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; - public bool IsSortedByType => SortMode == ApplicationSort.FileType; - public bool IsSortedBySize => SortMode == ApplicationSort.FileSize; - public bool IsSortedByPath => SortMode == ApplicationSort.Path; + public int ListItemSelectorSize + { + get + { + switch (ConfigurationState.Instance.Ui.GridSize) + { + case 1: + return 78; + case 2: + return 100; + case 3: + return 120; + case 4: + return 140; + default: + return 16; + } + } + } + + public int GridItemSelectorSize + { + get + { + switch (ConfigurationState.Instance.Ui.GridSize) + { + case 1: + return 120; + case 2: + return ShowNames ? 210 : 150; + case 3: + return ShowNames ? 240 : 180; + case 4: + return ShowNames ? 280 : 220; + default: + return 16; + } + } + } + + public int GridSizeScale + { + get => ConfigurationState.Instance.Ui.GridSize; + set + { + ConfigurationState.Instance.Ui.GridSize.Value = value; + + if (value < 2) + { + ShowNames = false; + } + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsGridSmall)); + OnPropertyChanged(nameof(IsGridMedium)); + OnPropertyChanged(nameof(IsGridLarge)); + OnPropertyChanged(nameof(IsGridHuge)); + OnPropertyChanged(nameof(ListItemSelectorSize)); + OnPropertyChanged(nameof(GridItemSelectorSize)); + OnPropertyChanged(nameof(ShowNames)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } public string SortName { @@ -632,15 +799,15 @@ namespace Ryujinx.Ava.UI.ViewModels { return SortMode switch { - ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], - ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], - ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], - ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], - ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], - ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], - ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], - _ => string.Empty, + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], + ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], + _ => string.Empty, }; } } @@ -662,9 +829,10 @@ namespace Ryujinx.Ava.UI.ViewModels public KeyGesture ShowUiKey { - get => KeyGesture.Parse(_showUikey); set + get => KeyGesture.Parse(_showUiKey); + set { - _showUikey = value.ToString(); + _showUiKey = value.ToString(); OnPropertyChanged(); } @@ -672,9 +840,10 @@ namespace Ryujinx.Ava.UI.ViewModels public KeyGesture ScreenshotKey { - get => KeyGesture.Parse(_screenshotkey); set + get => KeyGesture.Parse(_screenshotKey); + set { - _screenshotkey = value.ToString(); + _screenshotKey = value.ToString(); OnPropertyChanged(); } @@ -690,68 +859,73 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1; + public ContentManager ContentManager { get; private set; } + public ApplicationLibrary ApplicationLibrary { get; private set; } + public VirtualFileSystem VirtualFileSystem { get; private set; } + public AccountManager AccountManager { get; private set; } + public Ryujinx.Input.HLE.InputManager InputManager { get; private set; } + public UserChannelPersistence UserChannelPersistence { get; private set; } + public Action ShowLoading { get; private set; } + public Action SwitchToGameControl { get; private set; } + public Action SetMainContent { get; private set; } + public TopLevel TopLevel { get; private set; } + public RendererHost RendererControl { get; private set; } + public bool IsClosing { get; set; } + public LibHacHorizonManager LibHacHorizonManager { get; internal set; } + public IHostUiHandler UiHandler { get; internal set; } + public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; + public bool IsSortedByTitle => SortMode == ApplicationSort.Title; + public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; + public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; + public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; + public bool IsSortedByType => SortMode == ApplicationSort.FileType; + public bool IsSortedBySize => SortMode == ApplicationSort.FileSize; + public bool IsSortedByPath => SortMode == ApplicationSort.Path; + public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1; public bool IsGridMedium => ConfigurationState.Instance.Ui.GridSize == 2; - public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3; - public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4; + public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3; + public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4; - public int GridSizeScale +#endregion + +#region PrivateMethods + + private IComparer GetComparer() { - get => ConfigurationState.Instance.Ui.GridSize; - set + return SortMode switch { - ConfigurationState.Instance.Ui.GridSize.Value = value; - - if (value < 2) - { - ShowNames = false; - } - - OnPropertyChanged(); - OnPropertyChanged(nameof(IsGridSmall)); - OnPropertyChanged(nameof(IsGridMedium)); - OnPropertyChanged(nameof(IsGridLarge)); - OnPropertyChanged(nameof(IsGridHuge)); - OnPropertyChanged(nameof(ShowNames)); - - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - } + ApplicationSort.LastPlayed => new Models.Generic.LastPlayedSortComparer(IsAscending), + ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes) + : SortExpressionComparer.Descending(app => app.FileSizeBytes), + ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum) + : SortExpressionComparer.Descending(app => app.TimePlayedNum), + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) + : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite) + : SortExpressionComparer.Descending(app => app.Favorite), + ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) + : SortExpressionComparer.Descending(app => app.Developer), + ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) + : SortExpressionComparer.Descending(app => app.FileExtension), + ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) + : SortExpressionComparer.Descending(app => app.Path), + _ => null, + }; } - public async void OpenAmiiboWindow() + private void RefreshView() { - if (!_isAmiiboRequested) - { - return; - } - - if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId)) - { - string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper(); - AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId); - - await window.ShowDialog(_owner); - - if (window.IsScanned) - { - _showAll = window.ViewModel.ShowAllAmiibo; - _lastScannedAmiiboId = window.ScannedAmiibo.GetId(); - - _owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid); - } - } + RefreshGrid(); } - public void SetUiProgressHandlers(Switch emulationContext) + private void RefreshGrid() { - if (emulationContext.Application.DiskCacheLoadState != null) - { - emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; - emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; - } + Applications.ToObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out _appsObservableList).AsObservableList(); - emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; - emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; + OnPropertyChanged(nameof(AppsObservableList)); } private bool Filter(object arg) @@ -764,148 +938,314 @@ namespace Ryujinx.Ava.UI.ViewModels return false; } - private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) + private async Task HandleFirmwareInstallation(string filename) { - AddApplication(e.AppData); - } - - private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) - { - StatusBarProgressValue = e.NumAppsLoaded; - StatusBarProgressMaximum = e.NumAppsFound; - - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, StatusBarProgressValue, StatusBarProgressMaximum); - - Dispatcher.UIThread.Post(() => + try { - if (e.NumAppsFound == 0) + SystemVersion firmwareVersion = ContentManager.VerifyFirmwarePackage(filename); + + if (firmwareVersion == null) { - _owner.LoadProgressBar.IsVisible = false; + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage], filename)); + + return; } - if (e.NumAppsLoaded == e.NumAppsFound) + string dialogTitle = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle], firmwareVersion.VersionString); + + SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion(); + + string dialogMessage = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage], firmwareVersion.VersionString); + + if (currentVersion != null) { - _owner.LoadProgressBar.IsVisible = false; + dialogMessage += string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage], currentVersion.VersionString); } - }); - } - public void AddApplication(ApplicationData applicationData) - { - Dispatcher.UIThread.InvokeAsync(() => - { - Applications.Add(applicationData); - }); - } + dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage]; - public async void LoadApplications() - { - await Dispatcher.UIThread.InvokeAsync(() => - { - Applications.Clear(); + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + dialogTitle, + dialogMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - _owner.LoadProgressBar.IsVisible = true; - StatusBarProgressMaximum = 0; - StatusBarProgressValue = 0; + UpdateWaitWindow waitingDialog = ContentDialogHelper.CreateWaitingDialog(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]); - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); - }); + if (result == UserResult.Yes) + { + Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); - ReloadGameList(); - } + Thread thread = new(() => + { + Dispatcher.UIThread.InvokeAsync(delegate + { + waitingDialog.Show(); + }); - private void ReloadGameList() - { - if (_isLoading) - { - return; + try + { + ContentManager.InstallFirmware(filename); + + Dispatcher.UIThread.InvokeAsync(async delegate + { + waitingDialog.Close(); + + string message = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage], firmwareVersion.VersionString); + + await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + Logger.Info?.Print(LogClass.Application, message); + + // Purge Applet Cache. + + DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); + + if (miiEditorCacheFolder.Exists) + { + miiEditorCacheFolder.Delete(true); + } + }); + } + catch (Exception ex) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(ex.Message); + }); + } + finally + { + RefreshFirmwareStatus(); + } + }) { Name = "GUI.FirmwareInstallerThread" }; + + thread.Start(); + } } - - _isLoading = true; - - Thread thread = new(() => + catch (LibHac.Common.Keys.MissingKeyException ex) { - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language); - - _isLoading = false; - }) - { Name = "GUI.AppListLoadThread", Priority = ThreadPriority.AboveNormal }; - - thread.Start(); - } - - public async void OpenFile() - { - OpenFileDialog dialog = new() - { - Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle] - }; - - dialog.Filters.Add(new FileDialogFilter - { - Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], - Extensions = + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - "nsp", - "pfs0", - "xci", - "nca", - "nro", - "nso" + Logger.Error?.Print(LogClass.Application, ex.ToString()); + + async void Action() => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, (desktop.MainWindow as MainWindow)); + + Dispatcher.UIThread.Post(Action); } - }); - - dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "PFS0", Extensions = { "pfs0" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } }); - - string[] files = await dialog.ShowAsync(_owner); - - if (files != null && files.Length > 0) + } + catch (Exception ex) { - _owner.LoadApplication(files[0]); + await ContentDialogHelper.CreateErrorDialog(ex.Message); } } - public async void OpenFolder() + private void ProgressHandler(T state, int current, int total) where T : Enum { - OpenFolderDialog dialog = new() + Dispatcher.UIThread.Post((() => { - Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle] - }; + ProgressMaximum = total; + ProgressValue = current; - string folder = await dialog.ShowAsync(_owner); + switch (state) + { + case LoadState ptcState: + CacheLoadStatus = $"{current} / {total}"; + switch (ptcState) + { + case LoadState.Unloaded: + case LoadState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingPPTC]; + IsLoadingIndeterminate = false; + break; + case LoadState.Loaded: + LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); + IsLoadingIndeterminate = true; + CacheLoadStatus = ""; + break; + } + break; + case ShaderCacheLoadingState shaderCacheState: + CacheLoadStatus = $"{current} / {total}"; + switch (shaderCacheState) + { + case ShaderCacheLoadingState.Start: + case ShaderCacheLoadingState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingShaders]; + IsLoadingIndeterminate = false; + break; + case ShaderCacheLoadingState.Loaded: + LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); + IsLoadingIndeterminate = true; + CacheLoadStatus = ""; + break; + } + break; + default: + throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); + } + })); + } - if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) + private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId) + { + ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName); + } + + private async void ExtractLogo() + { + var selection = SelectedApplication; + if (selection != null) { - _owner.LoadApplication(folder); + await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); } } + private async void ExtractRomFs() + { + var selection = SelectedApplication; + if (selection != null) + { + await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); + } + } + + private async void ExtractExeFs() + { + var selection = SelectedApplication; + if (selection != null) + { + await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path); + } + } + + private void PrepareLoadScreen() + { + using MemoryStream stream = new(SelectedIcon); + using var gameIconBmp = SixLabors.ImageSharp.Image.Load(stream); + + var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel(); + + const float colorMultiple = 0.5f; + + Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B); + Color progressBgColor = Color.FromRgb( + (byte)(dominantColor.R * colorMultiple), + (byte)(dominantColor.G * colorMultiple), + (byte)(dominantColor.B * colorMultiple)); + + ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); + ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); + } + + private void InitializeGame() + { + RendererControl.RendererInitialized += GlRenderer_Created; + + AppHost.StatusUpdatedEvent += Update_StatusBar; + AppHost.AppExit += AppHost_AppExit; + + _rendererWaitEvent.WaitOne(); + + AppHost?.Start(); + + AppHost?.DisposeContext(); + } + + private void HandleRelaunch() + { + if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart) + { + UserChannelPersistence.ShouldRestart = false; + + Dispatcher.UIThread.Post(() => + { + LoadApplication(_currentEmulatedGamePath); + }); + } + else + { + // Otherwise, clear state. + UserChannelPersistence = new UserChannelPersistence(); + _currentEmulatedGamePath = null; + } + } + + private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) + { + if (ShowMenuAndStatusBar && !ShowLoadProgress) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Avalonia.Application.Current.Styles.TryGetResource(args.VSyncEnabled + ? "VsyncEnabled" + : "VsyncDisabled", out object color); + + if (color is not null) + { + VsyncColor = new SolidColorBrush((Color)color); + } + + DockedStatusText = args.DockedMode; + AspectRatioStatusText = args.AspectRatio; + GameStatusText = args.GameStatus; + VolumeStatusText = args.VolumeStatus; + FifoStatusText = args.FifoStatus; + GpuNameText = args.GpuName; + BackendText = args.GpuBackend; + + ShowStatusSeparator = true; + }); + } + } + + private void GlRenderer_Created(object sender, EventArgs e) + { + ShowLoading(false); + + _rendererWaitEvent.Set(); + } + +#endregion + +#region PublicMethods + + public void SetUIProgressHandlers(Switch emulationContext) + { + if (emulationContext.Application.DiskCacheLoadState != null) + { + emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; + emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; + } + + emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; + emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; + } + public void LoadConfigurableHotKeys() { if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi, out var showUiKey)) { - ShowUiKey = new KeyGesture(showUiKey, KeyModifiers.None); + ShowUiKey = new KeyGesture(showUiKey); } if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot, out var screenshotKey)) { - ScreenshotKey = new KeyGesture(screenshotKey, KeyModifiers.None); + ScreenshotKey = new KeyGesture(screenshotKey); } if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause, out var pauseKey)) { - PauseKey = new KeyGesture(pauseKey, KeyModifiers.None); + PauseKey = new KeyGesture(pauseKey); } } public void TakeScreenshot() { - _owner.AppHost.ScreenshotRequested = true; + AppHost.ScreenshotRequested = true; } public void HideUi() @@ -923,13 +1263,36 @@ namespace Ryujinx.Ava.UI.ViewModels Glyph = Glyph.Grid; } - public void OpenMiiApplet() + public async void InstallFirmwareFromFile() { - string contentPath = _owner.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - - if (!string.IsNullOrWhiteSpace(contentPath)) + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - _owner.LoadApplication(contentPath, false, "Mii Applet"); + OpenFileDialog dialog = new() { AllowMultiple = false }; + dialog.Filters.Add(new FileDialogFilter { Name = LocaleManager.Instance[LocaleKeys.FileDialogAllTypes], Extensions = { "xci", "zip" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } }); + + string[] file = await dialog.ShowAsync(desktop.MainWindow); + + if (file != null && file.Length > 0) + { + await HandleFirmwareInstallation(file[0]); + } + } + } + + public async void InstallFirmwareFromFolder() + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + OpenFolderDialog dialog = new(); + + string folder = await dialog.ShowAsync(desktop.MainWindow); + + if (!string.IsNullOrEmpty(folder)) + { + await HandleFirmwareInstallation(folder); + } } } @@ -947,39 +1310,6 @@ namespace Ryujinx.Ava.UI.ViewModels OpenHelper.OpenFolder(logPath); } - public void ToggleFullscreen() - { - if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs) - { - return; - } - - _lastFullscreenToggle = Environment.TickCount64; - - if (_owner.WindowState == WindowState.FullScreen) - { - _owner.WindowState = WindowState.Normal; - - if (IsGameRunning) - { - ShowMenuAndStatusBar = true; - } - } - else - { - _owner.WindowState = WindowState.FullScreen; - - if (IsGameRunning) - { - ShowMenuAndStatusBar = false; - } - } - - OnPropertyChanged(nameof(IsFullScreen)); - } - - public bool IsFullScreen => _owner.WindowState == WindowState.FullScreen; - public void ToggleDockMode() { if (IsGameRunning) @@ -990,7 +1320,7 @@ namespace Ryujinx.Ava.UI.ViewModels public async void ExitCurrentState() { - if (_owner.WindowState == WindowState.FullScreen) + if (WindowState == WindowState.FullScreen) { ToggleFullscreen(); } @@ -998,146 +1328,19 @@ namespace Ryujinx.Ava.UI.ViewModels { await Task.Delay(100); - _owner.AppHost?.ShowExitPrompt(); + AppHost?.ShowExitPrompt(); } } - public async void OpenSettings() - { - _owner.SettingsWindow = new(_owner.VirtualFileSystem, _owner.ContentManager); - - await _owner.SettingsWindow.ShowDialog(_owner); - - LoadConfigurableHotKeys(); - } - - public async void ManageProfiles() - { - await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem, _owner.LibHacHorizonManager.RyujinxClient); - } - - public async void OpenAboutWindow() - { - await AboutWindow.Show(); - } - public void ChangeLanguage(object obj) { LocaleManager.Instance.LoadDefaultLanguage(); LocaleManager.Instance.LoadLanguage((string)obj); } - private void ProgressHandler(T state, int current, int total) where T : Enum + public async void ManageProfiles() { - try - { - ProgressMaximum = total; - ProgressValue = current; - - switch (state) - { - case LoadState ptcState: - CacheLoadStatus = $"{current} / {total}"; - switch (ptcState) - { - case LoadState.Unloaded: - case LoadState.Loading: - LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingPPTC]; - IsLoadingIndeterminate = false; - break; - case LoadState.Loaded: - LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); - IsLoadingIndeterminate = true; - CacheLoadStatus = ""; - break; - } - break; - case ShaderCacheLoadingState shaderCacheState: - CacheLoadStatus = $"{current} / {total}"; - switch (shaderCacheState) - { - case ShaderCacheLoadingState.Start: - case ShaderCacheLoadingState.Loading: - LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingShaders]; - IsLoadingIndeterminate = false; - break; - case ShaderCacheLoadingState.Loaded: - LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); - IsLoadingIndeterminate = true; - CacheLoadStatus = ""; - break; - } - break; - default: - throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); - } - } - catch (Exception) { } - } - - public void OpenUserSaveDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - Task.Run(() => - { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); - - return; - } - - UserId userId = new((ulong)_owner.AccountManager.LastOpenedUser.UserId.High, (ulong)_owner.AccountManager.LastOpenedUser.UserId.Low); - SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); - } - } - - public void ToggleFavorite() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - selection.Favorite = !selection.Favorite; - - ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata => - { - appMetadata.Favorite = selection.Favorite; - }); - - RefreshView(); - } - } - - public void OpenModsDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - string modsBasePath = _owner.VirtualFileSystem.ModLoader.GetModsBasePath(); - string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); - - OpenHelper.OpenFolder(titleModsPath); - } - } - - public void OpenSdModsDirectory() - { - ApplicationData selection = SelectedApplication; - - if (selection != null) - { - string sdModsBasePath = _owner.VirtualFileSystem.ModLoader.GetSdModsBasePath(); - string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); - - OpenHelper.OpenFolder(titleModsPath); - } + await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient); } public void OpenPtcDirectory() @@ -1145,8 +1348,8 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationData selection = SelectedApplication; if (selection != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu"); - string mainPath = Path.Combine(ptcDir, "0"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu"); + string mainPath = Path.Combine(ptcDir, "0"); string backupPath = Path.Combine(ptcDir, "1"); if (!Directory.Exists(ptcDir)) @@ -1165,7 +1368,7 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationData selection = SelectedApplication; if (selection != null) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0")); DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "1")); // FIXME: Found a way to reproduce the bold effect on the title name (fork?). @@ -1222,7 +1425,7 @@ namespace Ryujinx.Ava.UI.ViewModels public void SimulateWakeUpMessage() { - _owner.AppHost.Device.System.SimulateWakeUpMessage(); + AppHost.Device.System.SimulateWakeUpMessage(); } public async void PurgeShaderCache() @@ -1240,7 +1443,7 @@ namespace Ryujinx.Ava.UI.ViewModels LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); List oldCacheDirectories = new(); - List newCacheFiles = new(); + List newCacheFiles = new(); if (shaderCacheDir.Exists) { @@ -1278,57 +1481,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void CheckForUpdates() - { - if (Updater.CanUpdate(true, _owner)) - { - await Updater.BeginParse(_owner, true); - } - } - - public async void OpenTitleUpdateManager() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - await new TitleUpdateWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner); - } - } - - public async void OpenDownloadableContentManager() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - await new DownloadableContentManagerWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner); - } - } - - public async void OpenCheatManager() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - await new CheatWindow(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(_owner); - } - } - - public async void OpenCheatManagerForCurrentApp() - { - if (!IsGameRunning) - { - return; - } - - ApplicationLoader application = _owner.AppHost.Device.Application; - if (application != null) - { - await new CheatWindow(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(_owner); - - _owner.AppHost.Device.EnableCheats(); - } - } - public void OpenDeviceSaveDirectory() { ApplicationData selection = SelectedApplication; @@ -1338,10 +1490,12 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { - Dispatcher.UIThread.Post(async () => + async void Action() { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + } + + Dispatcher.UIThread.Post(Action); return; } @@ -1361,10 +1515,12 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { - Dispatcher.UIThread.Post(async () => + async void Action() { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + } + + Dispatcher.UIThread.Post(Action); return; } @@ -1375,169 +1531,421 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId) + public void ToggleFavorite() { - ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName); - } - - private async void ExtractLogo() - { - var selection = SelectedApplication; + ApplicationData selection = SelectedApplication; if (selection != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); - } - } + selection.Favorite = !selection.Favorite; - private async void ExtractRomFs() - { - var selection = SelectedApplication; - if (selection != null) - { - await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); - } - } - - private async void ExtractExeFs() - { - var selection = SelectedApplication; - if (selection != null) - { - await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path); - } - } - - public void CloseWindow() - { - _owner.Close(); - } - - private async Task HandleFirmwareInstallation(string filename) - { - try - { - SystemVersion firmwareVersion = _owner.ContentManager.VerifyFirmwarePackage(filename); - - if (firmwareVersion == null) + ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata => { - await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage], filename)); + appMetadata.Favorite = selection.Favorite; + }); + + RefreshView(); + } + } + + public void OpenUserSaveDirectory() + { + ApplicationData selection = SelectedApplication; + if (selection != null) + { + Task.Run(() => + { + if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + { + async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + UserId userId = new((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low); + SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default); + OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); + }); + } + } + + public void OpenModsDirectory() + { + ApplicationData selection = SelectedApplication; + if (selection != null) + { + string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); + + OpenHelper.OpenFolder(titleModsPath); + } + } + + public void OpenSdModsDirectory() + { + ApplicationData selection = SelectedApplication; + + if (selection != null) + { + string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath(); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); + + OpenHelper.OpenFolder(titleModsPath); + } + } + + public async void OpenTitleUpdateManager() + { + ApplicationData selection = SelectedApplication; + if (selection != null) + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); + } + } + } + + public async void OpenDownloadableContentManager() + { + ApplicationData selection = SelectedApplication; + if (selection != null) + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); + } + } + } + + public async void OpenCheatManager() + { + ApplicationData selection = SelectedApplication; + if (selection != null) + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new CheatWindow(VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(desktop.MainWindow); + } + } + } + + public async void LoadApplications() + { + await Dispatcher.UIThread.InvokeAsync(() => + { + Applications.Clear(); + + StatusBarVisible = true; + StatusBarProgressMaximum = 0; + StatusBarProgressValue = 0; + + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); + }); + + ReloadGameList?.Invoke(); + } + + public async void OpenFile() + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + OpenFileDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle] + }; + + dialog.Filters.Add(new FileDialogFilter + { + Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], + Extensions = + { + "nsp", + "pfs0", + "xci", + "nca", + "nro", + "nso" + } + }); + + dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "PFS0", Extensions = { "pfs0" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } }); + + string[] files = await dialog.ShowAsync(desktop.MainWindow); + + if (files != null && files.Length > 0) + { + LoadApplication(files[0]); + } + } + } + + public async void OpenFolder() + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + OpenFolderDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle] + }; + + string folder = await dialog.ShowAsync(desktop.MainWindow); + + if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) + { + LoadApplication(folder); + } + } + } + + public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "") + { + if (AppHost != null) + { + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage], + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogOk], + "", + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + return; + } + +#if RELEASE + await PerformanceCheck(); +#endif + + Logger.RestartTime(); + + if (SelectedIcon == null) + { + SelectedIcon = ApplicationLibrary.GetApplicationIcon(path); + } + + PrepareLoadScreen(); + + RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel); + if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) + { + RendererControl.CreateOpenGL(); + } + else + { + RendererControl.CreateVulkan(); + } + + AppHost = new AppHost( + RendererControl, + InputManager, + path, + VirtualFileSystem, + ContentManager, + AccountManager, + UserChannelPersistence, + this, + TopLevel); + + async void Action() + { + if (!await AppHost.LoadGuestApplication()) + { + AppHost.DisposeContext(); + AppHost = null; return; } - string dialogTitle = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle], firmwareVersion.VersionString); + CanUpdate = false; + LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName; + TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName; - SystemVersion currentVersion = _owner.ContentManager.GetCurrentFirmwareVersion(); + SwitchToRenderer(startFullscreen); - string dialogMessage = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage], firmwareVersion.VersionString); + _currentEmulatedGamePath = path; - if (currentVersion != null) + Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; + gameThread.Start(); + } + + Dispatcher.UIThread.Post(Action); + } + + public void SwitchToRenderer(bool startFullscreen) + { + Dispatcher.UIThread.Post(() => + { + SwitchToGameControl(startFullscreen); + + SetMainContent(RendererControl); + + RendererControl.Focus(); + }); + } + + public void UpdateGameMetadata(string titleId) + { + ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => + { + if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime)) { - dialogMessage += string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage], currentVersion.VersionString); - } + double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; - dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage]; + appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); + } + }); + } + + public void RefreshFirmwareStatus() + { + SystemVersion version = null; + try + { + version = ContentManager.GetCurrentFirmwareVersion(); + } + catch (Exception) { } + + bool hasApplet = false; + + if (version != null) + { + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, + version.VersionString); + + hasApplet = version.Major > 3; + } + else + { + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0"); + } + + IsAppletMenuActive = hasApplet; + } + + public void AppHost_AppExit(object sender, EventArgs e) + { + if (IsClosing) + { + return; + } + + IsGameRunning = false; + + Dispatcher.UIThread.InvokeAsync(() => + { + ShowMenuAndStatusBar = true; + ShowContent = true; + ShowLoadProgress = false; + IsLoadingIndeterminate = false; + CanUpdate = true; + Cursor = Cursor.Default; + + SetMainContent(null); + + AppHost = null; + + HandleRelaunch(); + }); + + RendererControl.RendererInitialized -= GlRenderer_Created; + RendererControl = null; + + SelectedIcon = null; + + Dispatcher.UIThread.InvokeAsync(() => + { + Title = $"Ryujinx {Program.Version}"; + }); + } + + public void ToggleFullscreen() + { + if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) + { + return; + } + + LastFullscreenToggle = Environment.TickCount64; + + if (WindowState == WindowState.FullScreen) + { + WindowState = WindowState.Normal; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = true; + } + } + else + { + WindowState = WindowState.FullScreen; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = false; + } + } + + IsFullScreen = WindowState == WindowState.FullScreen; + } + + public static void SaveConfig() + { + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public static async Task PerformanceCheck() + { + if (ConfigurationState.Instance.Logger.EnableTrace.Value) + { + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage]; UserResult result = await ContentDialogHelper.CreateConfirmationDialog( - dialogTitle, - dialogMessage, + mainMessage, + secondaryMessage, LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - UpdateWaitWindow waitingDialog = ContentDialogHelper.CreateWaitingDialog(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]); - - if (result == UserResult.Yes) + if (result != UserResult.Yes) { - Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + ConfigurationState.Instance.Logger.EnableTrace.Value = false; - Thread thread = new(() => - { - Dispatcher.UIThread.InvokeAsync(delegate - { - waitingDialog.Show(); - }); - - try - { - _owner.ContentManager.InstallFirmware(filename); - - Dispatcher.UIThread.InvokeAsync(async delegate - { - waitingDialog.Close(); - - string message = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage], firmwareVersion.VersionString); - - await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]); - - Logger.Info?.Print(LogClass.Application, message); - - // Purge Applet Cache. - - DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); - - if (miiEditorCacheFolder.Exists) - { - miiEditorCacheFolder.Delete(true); - } - }); - } - catch (Exception ex) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - waitingDialog.Close(); - - await ContentDialogHelper.CreateErrorDialog(ex.Message); - }); - } - finally - { - _owner.RefreshFirmwareStatus(); - } - }); - - thread.Name = "GUI.FirmwareInstallerThread"; - thread.Start(); + SaveConfig(); } } - catch (LibHac.Common.Keys.MissingKeyException ex) - { - Logger.Error?.Print(LogClass.Application, ex.ToString()); - Dispatcher.UIThread.Post(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, _owner)); - } - catch (Exception ex) + if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) { - await ContentDialogHelper.CreateErrorDialog(ex.Message); + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + mainMessage, + secondaryMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result != UserResult.Yes) + { + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; + + SaveConfig(); + } } } - public async void InstallFirmwareFromFile() - { - OpenFileDialog dialog = new() { AllowMultiple = false }; - dialog.Filters.Add(new FileDialogFilter { Name = LocaleManager.Instance[LocaleKeys.FileDialogAllTypes], Extensions = { "xci", "zip" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } }); - - string[] file = await dialog.ShowAsync(_owner); - - if (file != null && file.Length > 0) - { - await HandleFirmwareInstallation(file[0]); - } - } - - public async void InstallFirmwareFromFolder() - { - OpenFolderDialog dialog = new(); - - string folder = await dialog.ShowAsync(_owner); - - if (!string.IsNullOrWhiteSpace(folder)) - { - await HandleFirmwareInstallation(folder); - } - } +#endregion } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml new file mode 100644 index 0000000000..0d0ae11937 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs new file mode 100644 index 0000000000..8c28abffe6 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -0,0 +1,146 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using System.Threading.Tasks; +using LibHac.FsSystem; +using LibHac.Ncm; +using Ryujinx.HLE.HOS; +using Ryujinx.Modules; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainMenuBarView : UserControl + { + public MainWindow Window { get; private set; } + public MainWindowViewModel ViewModel { get; private set; } + + public MainMenuBarView() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.VisualRoot is MainWindow window) + { + Window = window; + } + + ViewModel = Window.ViewModel; + DataContext = ViewModel; + } + + private async void StopEmulation_Click(object sender, RoutedEventArgs e) + { + await Task.Run(() => + { + Window.ViewModel.AppHost?.ShowExitPrompt(); + }); + } + + private async void PauseEmulation_Click(object sender, RoutedEventArgs e) + { + await Task.Run(() => + { + Window.ViewModel.AppHost?.Pause(); + }); + } + + private async void ResumeEmulation_Click(object sender, RoutedEventArgs e) + { + await Task.Run(() => + { + Window.ViewModel.AppHost?.Resume(); + }); + } + + public async void OpenSettings(object sender, RoutedEventArgs e) + { + Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager); + + await Window.SettingsWindow.ShowDialog(Window); + + ViewModel.LoadConfigurableHotKeys(); + } + + public void OpenMiiApplet(object sender, RoutedEventArgs e) + { + string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); + + if (!string.IsNullOrEmpty(contentPath)) + { + ViewModel.LoadApplication(contentPath, false, "Mii Applet"); + } + } + + public async void OpenAmiiboWindow(object sender, RoutedEventArgs e) + { + if (!ViewModel.IsAmiiboRequested) + { + return; + } + + if (ViewModel.AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + { + string titleId = ViewModel.AppHost.Device.Application.TitleIdText.ToUpper(); + AmiiboWindow window = new(ViewModel.ShowAll, ViewModel.LastScannedAmiiboId, titleId); + + await window.ShowDialog(Window); + + if (window.IsScanned) + { + ViewModel.ShowAll = window.ViewModel.ShowAllAmiibo; + ViewModel.LastScannedAmiiboId = window.ScannedAmiibo.GetId(); + + ViewModel.AppHost.Device.System.ScanAmiibo(deviceId, ViewModel.LastScannedAmiiboId, window.ViewModel.UseRandomUuid); + } + } + } + + public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e) + { + if (!ViewModel.IsGameRunning) + { + return; + } + + ApplicationLoader application = ViewModel.AppHost.Device.Application; + if (application != null) + { + await new CheatWindow(Window.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(Window); + + ViewModel.AppHost.Device.EnableCheats(); + } + } + + private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + if (sender is MenuItem) + { + ViewModel.IsAmiiboRequested = Window.ViewModel.AppHost.Device.System.SearchingForAmiibo(out _); + } + } + + public async void CheckForUpdates(object sender, RoutedEventArgs e) + { + if (Updater.CanUpdate(true, Window)) + { + await Updater.BeginParse(Window, true); + } + } + + public async void OpenAboutWindow(object sender, RoutedEventArgs e) + { + await AboutWindow.Show(); + } + + public void CloseWindow(object sender, RoutedEventArgs e) + { + Window.Close(); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml new file mode 100644 index 0000000000..1670569542 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs b/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs new file mode 100644 index 0000000000..d1050dddf7 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Ui.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainStatusBarView : UserControl + { + public MainWindow Window; + + public MainStatusBarView() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.VisualRoot is MainWindow window) + { + Window = window; + } + + DataContext = Window.ViewModel; + } + + private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + Window.ViewModel.AppHost.Device.EnableDeviceVsync = !Window.ViewModel.AppHost.Device.EnableDeviceVsync; + + Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {Window.ViewModel.AppHost.Device.EnableDeviceVsync}"); + } + + private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; + } + + private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; + + ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml new file mode 100644 index 0000000000..e83a650462 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs b/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs new file mode 100644 index 0000000000..841d59dec9 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using System; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainViewControls : UserControl + { + public MainWindowViewModel ViewModel; + + public MainViewControls() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.VisualRoot is MainWindow window) + { + ViewModel = window.ViewModel; + } + + DataContext = ViewModel; + } + + public void Sort_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton button) + { + ViewModel.Sort(Enum.Parse(button.Tag.ToString())); + } + } + + public void Order_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton button) + { + ViewModel.Sort(button.Tag.ToString() != "Descending"); + } + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + ViewModel.SearchText = SearchBox.Text; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/MainWindow.axaml b/Ryujinx.Ava/UI/Windows/MainWindow.axaml index 63d939a8e2..c3d34e75df 100644 --- a/Ryujinx.Ava/UI/Windows/MainWindow.axaml +++ b/Ryujinx.Ava/UI/Windows/MainWindow.axaml @@ -2,15 +2,16 @@ x:Class="Ryujinx.Ava.UI.Windows.MainWindow" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" - Title="Ryujinx" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" + xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main" + Cursor="{Binding Cursor}" + Title="{Binding Title}" + WindowState="{Binding WindowState}" Width="1280" Height="777" MinWidth="1092" @@ -66,206 +67,8 @@ VerticalAlignment="Stretch" IsVisible="{Binding ShowMenuAndStatusBar}" Orientation="Vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + TextWrapping="Wrap" + MaxWidth="500" /> + TextAlignment="Left" + MaxWidth="500" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index b2d822c3d3..3c4534c541 100644 --- a/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -1,19 +1,14 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; 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.Controls; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; @@ -25,11 +20,9 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; -using SixLabors.ImageSharp.PixelFormats; using System; using System.ComponentModel; using System.IO; -using System.Threading; using System.Threading.Tasks; using InputManager = Ryujinx.Input.HLE.InputManager; @@ -38,19 +31,14 @@ namespace Ryujinx.Ava.UI.Windows public partial class MainWindow : StyleableWindow { internal static MainWindowViewModel MainWindowViewModel { get; private set; } - private bool _canUpdate; - private bool _isClosing; - private bool _isLoading; - private Control _mainViewContent; + private bool _isLoading; private UserChannelPersistence _userChannelPersistence; private static bool _deferLoad; private static string _launchPath; private static bool _startFullscreen; - private string _currentEmulatedGamePath; internal readonly AvaHostUiHandler UiHandler; - private AutoResetEvent _rendererWaitEvent; public VirtualFileSystem VirtualFileSystem { get; private set; } public ContentManager ContentManager { get; private set; } @@ -58,30 +46,17 @@ namespace Ryujinx.Ava.UI.Windows public LibHacHorizonManager LibHacHorizonManager { get; private set; } - internal AppHost AppHost { get; private set; } public InputManager InputManager { get; private set; } - internal RendererHost RendererControl { get; private set; } internal MainWindowViewModel ViewModel { get; private set; } public SettingsWindow SettingsWindow { get; set; } - public bool CanUpdate - { - get => _canUpdate; - set - { - _canUpdate = value; - - Dispatcher.UIThread.InvokeAsync(() => UpdateMenuItem.IsEnabled = _canUpdate); - } - } - public static bool ShowKeyErrorOnLoad { get; set; } public ApplicationLibrary ApplicationLibrary { get; set; } public MainWindow() { - ViewModel = new MainWindowViewModel(this); + ViewModel = new MainWindowViewModel(); MainWindowViewModel = ViewModel; @@ -92,10 +67,10 @@ namespace Ryujinx.Ava.UI.Windows UiHandler = new AvaHostUiHandler(this); - Title = $"Ryujinx {Program.Version}"; + 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 + StatusBar.MinHeight; + double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight; Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; Width /= Program.WindowScaleFactor; @@ -103,14 +78,37 @@ namespace Ryujinx.Ava.UI.Windows { Initialize(); - ViewModel.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); } - _rendererWaitEvent = new AutoResetEvent(false); + ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; + ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + ViewModel.ReloadGameList += ReloadGameList; + } + + private void IsActiveChanged(bool obj) + { + ViewModel.IsActive = obj; } public void LoadGameList() @@ -122,45 +120,51 @@ namespace Ryujinx.Ava.UI.Windows _isLoading = true; - ViewModel.LoadApplications(); + LoadApplications(); _isLoading = false; } - private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) - { - if (ViewModel.ShowMenuAndStatusBar && !ViewModel.ShowLoadProgress) - { - Dispatcher.UIThread.InvokeAsync(() => - { - if (args.VSyncEnabled) - { - ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ff2eeac9")); - } - else - { - ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ffff4554")); - } - - ViewModel.DockedStatusText = args.DockedMode; - ViewModel.AspectRatioStatusText = args.AspectRatio; - ViewModel.GameStatusText = args.GameStatus; - ViewModel.VolumeStatusText = args.VolumeStatus; - ViewModel.FifoStatusText = args.FifoStatus; - ViewModel.GpuNameText = args.GpuName; - ViewModel.BackendText = args.GpuBackend; - - ViewModel.ShowStatusSeparator = true; - }); - } - } - 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.UpdateDynamicValue(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) @@ -169,50 +173,12 @@ namespace Ryujinx.Ava.UI.Windows string path = new FileInfo(args.Application.Path).FullName; - LoadApplication(path); + ViewModel.LoadApplication(path); } args.Handled = true; } - public async Task PerformanceCheck() - { - if (ConfigurationState.Instance.Logger.EnableTrace.Value) - { - string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage]; - string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage]; - - UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage, - LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - - if (result != UserResult.Yes) - { - ConfigurationState.Instance.Logger.EnableTrace.Value = false; - - SaveConfig(); - } - } - - if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) - { - string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage]; - string secondaryMessage = - LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage]; - - UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage, - LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - - if (result != UserResult.Yes) - { - ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; - - SaveConfig(); - } - } - } - internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg) { _deferLoad = true; @@ -220,109 +186,6 @@ namespace Ryujinx.Ava.UI.Windows _startFullscreen = startFullscreenArg; } -#pragma warning disable CS1998 - public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "") -#pragma warning restore CS1998 - { - if (AppHost != null) - { - await ContentDialogHelper.CreateInfoDialog( - LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage], - LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage], - LocaleManager.Instance[LocaleKeys.InputDialogOk], - "", - LocaleManager.Instance[LocaleKeys.RyujinxInfo]); - - return; - } - -#if RELEASE - await PerformanceCheck(); -#endif - - Logger.RestartTime(); - - if (ViewModel.SelectedIcon == null) - { - ViewModel.SelectedIcon = ApplicationLibrary.GetApplicationIcon(path); - } - - PrepareLoadScreen(); - - _mainViewContent = MainContent.Content as Control; - - RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel); - if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) - { - RendererControl.CreateOpenGL(); - } - else - { - RendererControl.CreateVulkan(); - } - - AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); - - Dispatcher.UIThread.Post(async () => - { - if (!await AppHost.LoadGuestApplication()) - { - AppHost.DisposeContext(); - AppHost = null; - - return; - } - - CanUpdate = false; - ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName; - ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName; - - SwitchToGameControl(startFullscreen); - - _currentEmulatedGamePath = path; - - Thread gameThread = new(InitializeGame) - { - Name = "GUI.WindowThread" - }; - gameThread.Start(); - }); - } - - private void InitializeGame() - { - RendererControl.RendererInitialized += GlRenderer_Created; - - AppHost.StatusUpdatedEvent += Update_StatusBar; - AppHost.AppExit += AppHost_AppExit; - - _rendererWaitEvent.WaitOne(); - - AppHost?.Start(); - - AppHost.DisposeContext(); - } - - - private void HandleRelaunch() - { - if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart) - { - _userChannelPersistence.ShouldRestart = false; - - Dispatcher.UIThread.Post(() => - { - LoadApplication(_currentEmulatedGamePath); - }); - } - else - { - // otherwise, clear state. - _userChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; - } - } - public void SwitchToGameControl(bool startFullscreen = false) { ViewModel.ShowLoadProgress = false; @@ -331,14 +194,10 @@ namespace Ryujinx.Ava.UI.Windows Dispatcher.UIThread.InvokeAsync(() => { - MainContent.Content = RendererControl; - - if (startFullscreen && WindowState != WindowState.FullScreen) + if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } - - RendererControl.Focus(); }); } @@ -350,71 +209,16 @@ namespace Ryujinx.Ava.UI.Windows Dispatcher.UIThread.InvokeAsync(() => { - if (startFullscreen && WindowState != WindowState.FullScreen) + if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } }); } - private void GlRenderer_Created(object sender, EventArgs e) - { - ShowLoading(); - - _rendererWaitEvent.Set(); - } - - private void AppHost_AppExit(object sender, EventArgs e) - { - if (_isClosing) - { - return; - } - - ViewModel.IsGameRunning = false; - - Dispatcher.UIThread.InvokeAsync(() => - { - ViewModel.ShowMenuAndStatusBar = true; - ViewModel.ShowContent = true; - ViewModel.ShowLoadProgress = false; - ViewModel.IsLoadingIndeterminate = false; - CanUpdate = true; - Cursor = Cursor.Default; - - if (MainContent.Content != _mainViewContent) - { - MainContent.Content = _mainViewContent; - } - - AppHost = null; - - HandleRelaunch(); - }); - - RendererControl.RendererInitialized -= GlRenderer_Created; - RendererControl = null; - - ViewModel.SelectedIcon = null; - - Dispatcher.UIThread.InvokeAsync(() => - { - Title = $"Ryujinx {Program.Version}"; - }); - } - - public void Sort_Checked(object sender, RoutedEventArgs args) - { - if (sender is RadioButton button) - { - var sort = Enum.Parse(button.Tag.ToString()); - ViewModel.Sort(sort); - } - } - protected override void HandleWindowStateChanged(WindowState state) { - WindowState = state; + ViewModel.WindowState = state; if (state != WindowState.Minimized) { @@ -422,15 +226,6 @@ namespace Ryujinx.Ava.UI.Windows } } - public void Order_Checked(object sender, RoutedEventArgs args) - { - if (sender is RadioButton button) - { - var tag = button.Tag.ToString(); - ViewModel.Sort(tag != "Descending"); - } - } - private void Initialize() { _userChannelPersistence = new UserChannelPersistence(); @@ -457,8 +252,6 @@ namespace Ryujinx.Ava.UI.Windows VirtualFileSystem.ReloadKeySet(); ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this); - - RefreshFirmwareStatus(); } protected void CheckLaunchState() @@ -475,7 +268,7 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - LoadApplication(_launchPath, _startFullscreen); + ViewModel.LoadApplication(_launchPath, _startFullscreen); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this)) @@ -487,35 +280,9 @@ namespace Ryujinx.Ava.UI.Windows } } - public void RefreshFirmwareStatus() - { - SystemVersion version = null; - try - { - version = ContentManager.GetCurrentFirmwareVersion(); - } - catch (Exception) { } - - bool hasApplet = false; - - if (version != null) - { - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, - version.VersionString); - - hasApplet = version.Major > 3; - } - else - { - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0"); - } - - ViewModel.IsAppletMenuActive = hasApplet; - } - private void Load() { - VolumeStatus.Click += VolumeStatus_CheckedChanged; + StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; GameGrid.ApplicationOpened += Application_Opened; @@ -535,6 +302,19 @@ namespace Ryujinx.Ava.UI.Windows 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; @@ -553,99 +333,6 @@ namespace Ryujinx.Ava.UI.Windows HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape)); } - public static void SaveConfig() - { - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - } - - public void UpdateGameMetadata(string titleId) - { - ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => - { - if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime)) - { - double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; - - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); - } - }); - } - - private void PrepareLoadScreen() - { - using MemoryStream stream = new MemoryStream(ViewModel.SelectedIcon); - using var gameIconBmp = SixLabors.ImageSharp.Image.Load(stream); - - var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel(); - - const int ColorDivisor = 4; - - Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B); - Color progressBgColor = Color.FromRgb( - (byte)(dominantColor.R / ColorDivisor), - (byte)(dominantColor.G / ColorDivisor), - (byte)(dominantColor.B / ColorDivisor)); - - ViewModel.ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); - ViewModel.ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); - } - - private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) - { - ViewModel.SearchText = SearchBox.Text; - } - - private async void StopEmulation_Click(object sender, RoutedEventArgs e) - { - if (AppHost != null) - { - await AppHost.ShowExitPrompt(); - } - } - - private async void PauseEmulation_Click(object sender, RoutedEventArgs e) - { - await Task.Run(() => - { - AppHost?.Pause(); - }); - } - - private async void ResumeEmulation_Click(object sender, RoutedEventArgs e) - { - await Task.Run(() => - { - AppHost?.Resume(); - }); - } - - private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - if (sender is MenuItem) - { - ViewModel.IsAmiiboRequested = AppHost.Device.System.SearchingForAmiibo(out _); - } - } - - private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e) - { - AppHost.Device.EnableDeviceVsync = !AppHost.Device.EnableDeviceVsync; - - Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {AppHost.Device.EnableDeviceVsync}"); - } - - private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e) - { - ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; - } - - private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e) - { - AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; - - ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; - } - private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e) { var volumeSplitButton = sender as ToggleSplitButton; @@ -653,20 +340,20 @@ namespace Ryujinx.Ava.UI.Windows { if (!volumeSplitButton.IsChecked) { - AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); + ViewModel.AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); } else { - AppHost.Device.SetVolume(0); + ViewModel.AppHost.Device.SetVolume(0); } - ViewModel.Volume = AppHost.Device.GetVolume(); + ViewModel.Volume = ViewModel.AppHost.Device.GetVolume(); } } protected override void OnClosing(CancelEventArgs e) { - if (!_isClosing && AppHost != null && ConfigurationState.Instance.ShowConfirmExit) + if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit) { e.Cancel = true; @@ -675,14 +362,14 @@ namespace Ryujinx.Ava.UI.Windows return; } - _isClosing = true; + ViewModel.IsClosing = true; - if (AppHost != null) + if (ViewModel.AppHost != null) { - AppHost.AppExit -= AppHost_AppExit; - AppHost.AppExit += (sender, e) => + ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit; + ViewModel.AppHost.AppExit += (sender, e) => { - AppHost = null; + ViewModel.AppHost = null; Dispatcher.UIThread.Post(() => { @@ -691,7 +378,7 @@ namespace Ryujinx.Ava.UI.Windows Close(); }); }; - AppHost?.Stop(); + ViewModel.AppHost?.Stop(); e.Cancel = true; @@ -709,13 +396,43 @@ namespace Ryujinx.Ava.UI.Windows { Dispatcher.UIThread.InvokeAsync(async () => { - _isClosing = await ContentDialogHelper.CreateExitDialog(); + ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); - if (_isClosing) + 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.UpdateDynamicValue(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