From 76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:24:05 +0000 Subject: Ava GUI: Restructure `Ryujinx.Ava` (#4165) * Restructure `Ryujinx.Ava` * Stylistic consistency * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Fix redundancies * Remove redunancies * Add back elses Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> --- Ryujinx.Ava/UI/Controls/GameGridView.axaml | 195 +++++++++++++++++ Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs | 83 ++++++++ Ryujinx.Ava/UI/Controls/GameListView.axaml | 234 +++++++++++++++++++++ Ryujinx.Ava/UI/Controls/GameListView.axaml.cs | 83 ++++++++ Ryujinx.Ava/UI/Controls/InputDialog.axaml | 32 +++ Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs | 57 +++++ Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml | 16 ++ .../UI/Controls/NavigationDialogHost.axaml.cs | 91 ++++++++ .../UI/Controls/ProfileImageSelectionDialog.axaml | 57 +++++ .../Controls/ProfileImageSelectionDialog.axaml.cs | 105 +++++++++ Ryujinx.Ava/UI/Controls/RendererHost.axaml | 11 + Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs | 127 +++++++++++ Ryujinx.Ava/UI/Controls/SaveManager.axaml | 175 +++++++++++++++ Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs | 160 ++++++++++++++ Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml | 42 ++++ Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs | 20 ++ Ryujinx.Ava/UI/Controls/UserEditor.axaml | 86 ++++++++ Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs | 118 +++++++++++ Ryujinx.Ava/UI/Controls/UserRecoverer.axaml | 72 +++++++ Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs | 44 ++++ Ryujinx.Ava/UI/Controls/UserSelector.axaml | 145 +++++++++++++ Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs | 77 +++++++ 22 files changed, 2030 insertions(+) create mode 100644 Ryujinx.Ava/UI/Controls/GameGridView.axaml create mode 100644 Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/GameListView.axaml create mode 100644 Ryujinx.Ava/UI/Controls/GameListView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/InputDialog.axaml create mode 100644 Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml create mode 100644 Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml create mode 100644 Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/RendererHost.axaml create mode 100644 Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/SaveManager.axaml create mode 100644 Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml create mode 100644 Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/UserEditor.axaml create mode 100644 Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/UserRecoverer.axaml create mode 100644 Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs create mode 100644 Ryujinx.Ava/UI/Controls/UserSelector.axaml create mode 100644 Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs (limited to 'Ryujinx.Ava/UI/Controls') diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml b/Ryujinx.Ava/UI/Controls/GameGridView.axaml new file mode 100644 index 00000000..1c4d7638 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs b/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs new file mode 100644 index 00000000..9965f750 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs @@ -0,0 +1,83 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using LibHac.Common; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class GameGridView : UserControl + { + private ApplicationData _selectedApplication; + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + var selected = listBox.SelectedItem as ApplicationData; + + _selectedApplication = selected; + } + } + + public ApplicationData SelectedApplication => _selectedApplication; + + public GameGridView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + + private void MenuBase_OnMenuOpened(object sender, EventArgs e) + { + var selection = SelectedApplication; + + if (selection != null) + { + if (sender is ContextMenu menu) + { + bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; + bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; + bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + + ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; + } + } + } + } +} diff --git a/Ryujinx.Ava/UI/Controls/GameListView.axaml b/Ryujinx.Ava/UI/Controls/GameListView.axaml new file mode 100644 index 00000000..d886ecbe --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/GameListView.axaml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs b/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs new file mode 100644 index 00000000..01e35990 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs @@ -0,0 +1,83 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using LibHac.Common; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class GameListView : UserControl + { + private ApplicationData _selectedApplication; + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + var selected = listBox.SelectedItem as ApplicationData; + + _selectedApplication = selected; + } + } + + public ApplicationData SelectedApplication => _selectedApplication; + + public GameListView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + + private void MenuBase_OnMenuOpened(object sender, EventArgs e) + { + var selection = SelectedApplication; + + if (selection != null) + { + if (sender is ContextMenu menu) + { + bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; + bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; + bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + + ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; + } + } + } + } +} diff --git a/Ryujinx.Ava/UI/Controls/InputDialog.axaml b/Ryujinx.Ava/UI/Controls/InputDialog.axaml new file mode 100644 index 00000000..ed1ceda3 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/InputDialog.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs b/Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs new file mode 100644 index 00000000..abaabd3b --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class InputDialog : UserControl + { + public string Message { get; set; } + public string Input { get; set; } + public string SubMessage { get; set; } + + public uint MaxLength { get; } + + public InputDialog(string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue) + { + Message = message; + Input = input; + SubMessage = subMessage; + MaxLength = maxLength; + + DataContext = this; + } + + public InputDialog() + { + InitializeComponent(); + } + + public static async Task<(UserResult Result, string Input)> ShowInputDialog(string title, string message, + string input = "", string subMessage = "", uint maxLength = int.MaxValue) + { + UserResult result = UserResult.Cancel; + + InputDialog content = new InputDialog(message, input, subMessage, maxLength); + ContentDialog contentDialog = new ContentDialog + { + Title = title, + PrimaryButtonText = LocaleManager.Instance["InputDialogOk"], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["InputDialogCancel"], + Content = content, + PrimaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Ok; + input = content.Input; + }) + }; + await contentDialog.ShowAsync(); + + return (result, input); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml new file mode 100644 index 00000000..90720478 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs new file mode 100644 index 00000000..98f9e9e3 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs @@ -0,0 +1,91 @@ +using Avalonia; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using LibHac; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class NavigationDialogHost : UserControl + { + public AccountManager AccountManager { get; } + public ContentManager ContentManager { get; } + public VirtualFileSystem VirtualFileSystem { get; } + public HorizonClient HorizonClient { get; } + public UserProfileViewModel ViewModel { get; set; } + + public NavigationDialogHost() + { + InitializeComponent(); + } + + public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, + VirtualFileSystem virtualFileSystem, HorizonClient horizonClient) + { + AccountManager = accountManager; + ContentManager = contentManager; + VirtualFileSystem = virtualFileSystem; + HorizonClient = horizonClient; + ViewModel = new UserProfileViewModel(this); + + + if (contentManager.GetCurrentFirmwareVersion() != null) + { + Task.Run(() => + { + AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem); + }); + } + InitializeComponent(); + } + + public void GoBack(object parameter = null) + { + if (ContentFrame.BackStack.Count > 0) + { + ContentFrame.GoBack(); + } + + ViewModel.LoadProfiles(); + } + + public void Navigate(Type sourcePageType, object parameter) + { + ContentFrame.Navigate(sourcePageType, parameter); + } + + public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) + { + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance["UserProfileWindowTitle"], + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["UserProfilesClose"], + Content = content, + Padding = new Thickness(0) + }; + + contentDialog.Closed += (sender, args) => + { + content.ViewModel.Dispose(); + }; + + await contentDialog.ShowAsync(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Navigate(typeof(UserSelector), this); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml new file mode 100644 index 00000000..56f8152a --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs new file mode 100644 index 00000000..00183b69 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs @@ -0,0 +1,105 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE.FileSystem; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System.IO; +using Image = SixLabors.ImageSharp.Image; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ProfileImageSelectionDialog : UserControl + { + private ContentManager _contentManager; + private NavigationDialogHost _parent; + private TempProfile _profile; + + public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null; + + public ProfileImageSelectionDialog() + { + InitializeComponent(); + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + case NavigationMode.New: + (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; + _contentManager = _parent.ContentManager; + break; + case NavigationMode.Back: + _parent.GoBack(); + break; + } + + DataContext = this; + } + } + + private async void Import_OnClick(object sender, RoutedEventArgs e) + { + OpenFileDialog dialog = new(); + dialog.Filters.Add(new FileDialogFilter + { + Name = LocaleManager.Instance["AllSupportedFormats"], + Extensions = { "jpg", "jpeg", "png", "bmp" } + }); + dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } }); + + dialog.AllowMultiple = false; + + string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); + + if (image != null) + { + if (image.Length > 0) + { + string imageFile = image[0]; + + _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); + } + + _parent.GoBack(); + } + } + + private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) + { + if (FirmwareFound) + { + _parent.Navigate(typeof(AvatarWindow), (_parent, _profile)); + } + } + + private static byte[] ProcessProfileImage(byte[] buffer) + { + using (Image image = Image.Load(buffer)) + { + image.Mutate(x => x.Resize(256, 256)); + + using (MemoryStream streamJpg = new()) + { + image.SaveAsJpeg(streamJpg); + + return streamJpg.ToArray(); + } + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/RendererHost.axaml b/Ryujinx.Ava/UI/Controls/RendererHost.axaml new file mode 100644 index 00000000..1cc557f0 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/RendererHost.axaml @@ -0,0 +1,11 @@ + + diff --git a/Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs b/Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs new file mode 100644 index 00000000..97058fa4 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs @@ -0,0 +1,127 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Common.Configuration; +using Silk.NET.Vulkan; +using SPB.Graphics.OpenGL; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class RendererHost : UserControl, IDisposable + { + private readonly GraphicsDebugLevel _graphicsDebugLevel; + private EmbeddedWindow _currentWindow; + + public bool IsVulkan { get; private set; } + + public RendererHost(GraphicsDebugLevel graphicsDebugLevel) + { + _graphicsDebugLevel = graphicsDebugLevel; + InitializeComponent(); + } + + public RendererHost() + { + InitializeComponent(); + } + + public void CreateOpenGL() + { + Dispose(); + + _currentWindow = new OpenGLEmbeddedWindow(3, 3, _graphicsDebugLevel); + Initialize(); + + IsVulkan = false; + } + + private void Initialize() + { + _currentWindow.WindowCreated += CurrentWindow_WindowCreated; + _currentWindow.SizeChanged += CurrentWindow_SizeChanged; + Content = _currentWindow; + } + + public void CreateVulkan() + { + Dispose(); + + _currentWindow = new VulkanEmbeddedWindow(); + Initialize(); + + IsVulkan = true; + } + + public OpenGLContextBase GetContext() + { + if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow) + { + return openGlEmbeddedWindow.Context; + } + + return null; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + Dispose(); + } + + private void CurrentWindow_SizeChanged(object sender, Size e) + { + SizeChanged?.Invoke(sender, e); + } + + private void CurrentWindow_WindowCreated(object sender, IntPtr e) + { + RendererInitialized?.Invoke(this, EventArgs.Empty); + } + + public void MakeCurrent() + { + if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow) + { + openGlEmbeddedWindow.MakeCurrent(); + } + } + + public void MakeCurrent(SwappableNativeWindowBase window) + { + if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow) + { + openGlEmbeddedWindow.MakeCurrent(window); + } + } + + public void SwapBuffers() + { + if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow) + { + openGlEmbeddedWindow.SwapBuffers(); + } + } + + public event EventHandler RendererInitialized; + public event Action SizeChanged; + public void Dispose() + { + if (_currentWindow != null) + { + _currentWindow.WindowCreated -= CurrentWindow_WindowCreated; + _currentWindow.SizeChanged -= CurrentWindow_SizeChanged; + } + } + + public SurfaceKHR CreateVulkanSurface(Instance instance, Vk api) + { + return (_currentWindow is VulkanEmbeddedWindow vulkanEmbeddedWindow) + ? vulkanEmbeddedWindow.CreateSurface(instance) + : default; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/SaveManager.axaml b/Ryujinx.Ava/UI/Controls/SaveManager.axaml new file mode 100644 index 00000000..b0dc4c6f --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/SaveManager.axaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs b/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs new file mode 100644 index 00000000..9910481c --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs @@ -0,0 +1,160 @@ +using Avalonia.Controls; +using DynamicData; +using DynamicData.Binding; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class SaveManager : UserControl + { + private readonly UserProfile _userProfile; + private readonly HorizonClient _horizonClient; + private readonly VirtualFileSystem _virtualFileSystem; + private int _sortIndex; + private int _orderIndex; + private ObservableCollection _view = new ObservableCollection(); + private string _search; + + public ObservableCollection Saves { get; set; } = new ObservableCollection(); + + public ObservableCollection View + { + get => _view; + set => _view = value; + } + + public int SortIndex + { + get => _sortIndex; + set + { + _sortIndex = value; + Sort(); + } + } + + public int OrderIndex + { + get => _orderIndex; + set + { + _orderIndex = value; + Sort(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + Sort(); + } + } + + public SaveManager() + { + InitializeComponent(); + } + + public SaveManager(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem) + { + _userProfile = userProfile; + _horizonClient = horizonClient; + _virtualFileSystem = virtualFileSystem; + InitializeComponent(); + + DataContext = this; + + Task.Run(LoadSaves); + } + + public void LoadSaves() + { + Saves.Clear(); + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, + new UserId((ulong)_userProfile.UserId.High, (ulong)_userProfile.UserId.Low), saveDataId: default, index: default); + + using var saveDataIterator = new UniqueRef(); + + _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span saveDataInfo = stackalloc SaveDataInfo[10]; + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + var save = saveDataInfo[i]; + if (save.ProgramId.Value != 0) + { + var saveModel = new SaveModel(save, _horizonClient, _virtualFileSystem); + Saves.Add(saveModel); + saveModel.DeleteAction = () => { Saves.Remove(saveModel); }; + } + + Sort(); + } + } + } + + private void Sort() + { + Saves.AsObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out var view).AsObservableList(); + + _view.Clear(); + _view.AddRange(view); + } + + private IComparer GetComparer() + { + switch (SortIndex) + { + case 0: + return OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Title) + : SortExpressionComparer.Descending(save => save.Title); + case 1: + return OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Size) + : SortExpressionComparer.Descending(save => save.Size); + default: + return null; + } + } + + private bool Filter(object arg) + { + if (arg is SaveModel save) + { + return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower()); + } + + return false; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml b/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml new file mode 100644 index 00000000..c5041230 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs b/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs new file mode 100644 index 00000000..9db7b5d4 --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.Windows; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class UpdateWaitWindow : StyleableWindow + { + public UpdateWaitWindow(string primaryText, string secondaryText) : this() + { + PrimaryText.Text = primaryText; + SecondaryText.Text = secondaryText; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + } + + public UpdateWaitWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UserEditor.axaml b/Ryujinx.Ava/UI/Controls/UserEditor.axaml new file mode 100644 index 00000000..155f1cfe --- /dev/null +++ b/Ryujinx.Ava/UI/Controls/UserEditor.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +