aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/UI/Views/User
diff options
context:
space:
mode:
authorIsaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>2023-01-11 00:20:19 -0500
committerGitHub <noreply@github.com>2023-01-11 06:20:19 +0100
commit934b5a64e5638ae5228acb52faf48efadefdea8d (patch)
treecc65eab75c5a9a7c3438de3302ab1f6cbcec1599 /Ryujinx.Ava/UI/Views/User
parentcee667b491f87c48546f348bad8c6f16cdf6d628 (diff)
Ava GUI: User Profile Manager + Other Fixes (#4166)
* Fix redundancies * Add back elses * Loading Screen fixes * Redesign User Profile Manager - Backported long selection bar in Grid/List view not working - Backported UserSelector is jank * Fix SelectionIndicator * Fix DataType * Fix SaveManager bug * Remove debug log * Load saves on UIThread * Reduce UI thread blocking * Fix locale keys * Use block namespaces * Fix close button width * Make UserProfile ordering consistent * Alphabetical order * Adjust layout, remove green circle for blue selector * Fix some inconsistencies * Fix no inital selected profile * Adjust appearance of edit button * Adjust SaveManager * Remove redundant warning dialog * Make firmware avatar selector clearer * View redesign again :hero_depressed: * Consistency adjustments * Adjust margins * Make `UserProfileImageSelector` consistent * Make `UserFirmwareAvatarSelector` consistent * Fix long grid view selector * Switch case * Remove long selection bar Handled in #4178 * Consistency * Started dialog titles * Fixes * Remaining titles * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml Co-authored-by: Mary-nyan <thog@protonmail.com> * Fix build * Hide UserRecoverer if no LostProfiles are found * UserEditor Avatar Placeholder * Watermark + locale adjustment * Border radius * Remove unnecessary styles * Fix firmware avatar image order * Cleanup `ColorPickerButton` * Make `UserId` copy/paste able * Make `FirmwareAvatarSelector` 6 images wide * Make selection bar better * Unsaved changes dialogue * Fix indentation * Remove extra check * Address suggestions * Reorganise - Remove unused views - Rename views to match convention - Fix weird namespacing * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml Co-authored-by: Ac_K <Acoustik666@gmail.com> * UserRecovererView empty placeholder * Update Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Models/UserProfile.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Remove AddModel * Update Ryujinx.Ava/Assets/Locales/en_US.json Co-authored-by: Ac_K <Acoustik666@gmail.com> * Fix bug Co-authored-by: Mary-nyan <thog@protonmail.com> Co-authored-by: Ac_K <Acoustik666@gmail.com>
Diffstat (limited to 'Ryujinx.Ava/UI/Views/User')
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserEditorView.axaml123
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs165
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml114
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs88
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml63
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs124
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml83
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs51
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml199
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs148
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml165
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs128
12 files changed, 1451 insertions, 0 deletions
diff --git a/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml
new file mode 100644
index 00000000..7e55f25e
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml
@@ -0,0 +1,123 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Views.User.UserEditorView"
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ Margin="0"
+ MinWidth="500"
+ Padding="0"
+ mc:Ignorable="d"
+ Focusable="True"
+ x:CompileBindings="True"
+ x:DataType="models:TempProfile">
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ </UserControl.Resources>
+ <Grid Margin="0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <StackPanel
+ Grid.Row="0"
+ Grid.Column="0"
+ HorizontalAlignment="Stretch"
+ Orientation="Vertical"
+ Spacing="10">
+ <TextBlock Text="{locale:Locale UserProfilesName}" />
+ <TextBox
+ Name="NameBox"
+ Width="300"
+ HorizontalAlignment="Stretch"
+ MaxLength="{Binding MaxProfileNameLength}"
+ Watermark="{locale:Locale ProfileNameSelectionWatermark}"
+ Text="{Binding Name}" />
+ <TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
+ <TextBox
+ Name="IdLabel"
+ Width="300"
+ HorizontalAlignment="Stretch"
+ IsReadOnly="True"
+ Text="{Binding UserIdString}" />
+ </StackPanel>
+ <StackPanel
+ Grid.Row="0"
+ Grid.Column="1"
+ HorizontalAlignment="Right"
+ VerticalAlignment="Stretch"
+ Orientation="Vertical">
+ <Border
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ BorderThickness="1">
+ <Panel>
+ <ui:SymbolIcon
+ FontSize="60"
+ Width="96"
+ Height="96"
+ Margin="0"
+ Foreground="{DynamicResource AppListHoverBackgroundColor}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Symbol="Camera" />
+ <Image
+ Name="ProfileImage"
+ Width="96"
+ Height="96"
+ Margin="0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+ </Panel>
+ </Border>
+ </StackPanel>
+ <StackPanel
+ Grid.Row="1"
+ Grid.Column="0"
+ Grid.ColumnSpan="2"
+ HorizontalAlignment="Left"
+ Orientation="Horizontal"
+ Margin="0 24 0 0"
+ Spacing="10">
+ <Button
+ Width="50"
+ MinWidth="50"
+ Click="BackButton_Click">
+ <ui:SymbolIcon Symbol="Back" />
+ </Button>
+ </StackPanel>
+ <StackPanel
+ Grid.Row="1"
+ Grid.Column="0"
+ Grid.ColumnSpan="2"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal"
+ Margin="0 24 0 0"
+ Spacing="10">
+ <Button
+ Name="DeleteButton"
+ Click="DeleteButton_Click"
+ Content="{locale:Locale UserProfilesDelete}" />
+ <Button
+ Name="ChangePictureButton"
+ Click="ChangePictureButton_Click"
+ Content="{locale:Locale UserProfilesChangeProfileImage}" />
+ <Button
+ Name="AddPictureButton"
+ Click="ChangePictureButton_Click"
+ Content="{locale:Locale UserProfilesSetProfileImage}" />
+ <Button
+ Name="SaveButton"
+ Click="SaveButton_Click"
+ Content="{locale:Locale Save}" />
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs
new file mode 100644
index 00000000..fb33dcf8
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs
@@ -0,0 +1,165 @@
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserEditorView : UserControl
+ {
+ private NavigationDialogHost _parent;
+ private UserProfile _profile;
+ private bool _isNewUser;
+
+ public TempProfile TempProfile { get; set; }
+ public uint MaxProfileNameLength => 0x20;
+ public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
+
+ public UserEditorView()
+ {
+ InitializeComponent();
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ switch (arg.NavigationMode)
+ {
+ case NavigationMode.New:
+ var args = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
+ _isNewUser = args.isNewUser;
+ _profile = args.profile;
+ TempProfile = new TempProfile(_profile);
+
+ _parent = args.parent;
+ break;
+ }
+
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
+ $"{ (_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
+
+ DataContext = TempProfile;
+
+ AddPictureButton.IsVisible = _isNewUser;
+ ChangePictureButton.IsVisible = !_isNewUser;
+ IdLabel.IsVisible = _profile != null;
+ IdText.IsVisible = _profile != null;
+ if (!_isNewUser && IsDeletable)
+ {
+ DeleteButton.IsVisible = true;
+ }
+ else
+ {
+ DeleteButton.IsVisible = false;
+ }
+ }
+ }
+
+ private async void BackButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_isNewUser)
+ {
+ if (TempProfile.Name != String.Empty || TempProfile.Image != null)
+ {
+ if (await ContentDialogHelper.CreateChoiceDialog(
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
+ {
+ _parent?.GoBack();
+ }
+ }
+ else
+ {
+ _parent?.GoBack();
+ }
+ }
+ else
+ {
+ if (_profile.Name != TempProfile.Name || _profile.Image != TempProfile.Image)
+ {
+ if (await ContentDialogHelper.CreateChoiceDialog(
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
+ LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
+ {
+ _parent?.GoBack();
+ }
+ }
+ else
+ {
+ _parent?.GoBack();
+ }
+ }
+ }
+
+ private void DeleteButton_Click(object sender, RoutedEventArgs e)
+ {
+ _parent.DeleteUser(_profile);
+ }
+
+ private void SaveButton_Click(object sender, RoutedEventArgs e)
+ {
+ DataValidationErrors.ClearErrors(NameBox);
+
+ if (string.IsNullOrWhiteSpace(TempProfile.Name))
+ {
+ DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
+
+ return;
+ }
+
+ if (TempProfile.Image == null)
+ {
+ _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile));
+
+ return;
+ }
+
+ if (_profile != null && !_isNewUser)
+ {
+ _profile.Name = TempProfile.Name;
+ _profile.Image = TempProfile.Image;
+ _profile.UpdateState();
+ _parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
+ _parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
+ }
+ else if (_isNewUser)
+ {
+ _parent.AccountManager.AddUser(TempProfile.Name, TempProfile.Image, TempProfile.UserId);
+ }
+ else
+ {
+ return;
+ }
+
+ _parent?.GoBack();
+ }
+
+ public void SelectProfileImage()
+ {
+ _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile));
+ }
+
+ private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_profile != null || _isNewUser)
+ {
+ SelectProfileImage();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
new file mode 100644
index 00000000..d46fcefc
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
@@ -0,0 +1,114 @@
+<UserControl
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Width="528"
+ d:DesignWidth="578"
+ d:DesignHeight="350"
+ x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ x:CompileBindings="True"
+ x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"
+ Focusable="True">
+ <Design.DataContext>
+ <viewModels:UserFirmwareAvatarSelectorViewModel />
+ </Design.DataContext>
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ </UserControl.Resources>
+ <Grid
+ Margin="0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <ListBox
+ Grid.Row="1"
+ BorderThickness="0"
+ SelectedIndex="{Binding SelectedIndex}"
+ Height="400"
+ Items="{Binding Images}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <WrapPanel
+ Orientation="Horizontal"
+ Margin="0"
+ HorizontalAlignment="Center" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="CornerRadius" Value="4" />
+ <Setter Property="Width" Value="85" />
+ <Setter Property="MaxWidth" Value="85" />
+ <Setter Property="MinWidth" Value="85" />
+ </Style>
+ <Style Selector="ListBoxItem /template/ Border#SelectionIndicator">
+ <Setter Property="MinHeight" Value="70" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Panel
+ Background="{Binding BackgroundColor}"
+ Margin="5">
+ <Image Source="{Binding Data, Converter={StaticResource ByteImage}}" />
+ </Panel>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ <StackPanel
+ Grid.Row="3"
+ Orientation="Horizontal"
+ Spacing="10"
+ Margin="0 24 0 0"
+ HorizontalAlignment="Left">
+ <Button
+ Width="50"
+ MinWidth="50"
+ Height="35"
+ Click="GoBack">
+ <ui:SymbolIcon Symbol="Back" />
+ </Button>
+ </StackPanel>
+ <StackPanel
+ Grid.Row="3"
+ Orientation="Horizontal"
+ Spacing="10"
+ Margin="0 24 0 0"
+ HorizontalAlignment="Right">
+ <ui:ColorPickerButton
+ FlyoutPlacement="Top"
+ IsMoreButtonVisible="False"
+ UseColorPalette="False"
+ UseColorTriangle="False"
+ UseColorWheel="False"
+ ShowAcceptDismissButtons="False"
+ IsAlphaEnabled="False"
+ Color="{Binding BackgroundColor, Mode=TwoWay}"
+ Name="ColorButton">
+ <ui:ColorPickerButton.Styles>
+ <Style Selector="Grid#Root > DockPanel > Grid">
+ <Setter Property="IsVisible" Value="False" />
+ </Style>
+ </ui:ColorPickerButton.Styles>
+ </ui:ColorPickerButton>
+ <Button
+ Content="{locale:Locale AvatarChoose}"
+ Height="35"
+ Name="ChooseButton"
+ Click="ChooseButton_OnClick" />
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
new file mode 100644
index 00000000..7c9191ab
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
@@ -0,0 +1,88 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System.IO;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserFirmwareAvatarSelectorView : UserControl
+ {
+ private NavigationDialogHost _parent;
+ private TempProfile _profile;
+
+ public UserFirmwareAvatarSelectorView(ContentManager contentManager)
+ {
+ ContentManager = contentManager;
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+ }
+
+ public UserFirmwareAvatarSelectorView()
+ {
+ InitializeComponent();
+
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ if (arg.NavigationMode == NavigationMode.New)
+ {
+ (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
+ ContentManager = _parent.ContentManager;
+ if (Program.PreviewerDetached)
+ {
+ ViewModel = new UserFirmwareAvatarSelectorViewModel();
+ }
+
+ DataContext = ViewModel;
+ }
+ }
+ }
+
+ public ContentManager ContentManager { get; private set; }
+
+ internal UserFirmwareAvatarSelectorViewModel ViewModel { get; set; }
+
+ private void GoBack(object sender, RoutedEventArgs e)
+ {
+ _parent.GoBack();
+ }
+
+ private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (ViewModel.SelectedImage != null)
+ {
+ MemoryStream streamJpg = new();
+ SixLabors.ImageSharp.Image avatarImage = SixLabors.ImageSharp.Image.Load(ViewModel.SelectedImage, new PngDecoder());
+
+ avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(
+ ViewModel.BackgroundColor.R,
+ ViewModel.BackgroundColor.G,
+ ViewModel.BackgroundColor.B,
+ ViewModel.BackgroundColor.A)));
+ avatarImage.SaveAsJpeg(streamJpg);
+
+ _profile.Image = streamJpg.ToArray();
+
+ _parent.GoBack();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml
new file mode 100644
index 00000000..b9f51fdc
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml
@@ -0,0 +1,63 @@
+<UserControl
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ Focusable="True"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView"
+ x:CompileBindings="True"
+ x:DataType="viewModles:UserProfileImageSelectorViewModel"
+ Width="500"
+ d:DesignWidth="500">
+ <Design.DataContext>
+ <viewModles:UserProfileImageSelectorViewModel />
+ </Design.DataContext>
+ <Grid
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="70" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock
+ Grid.Row="0"
+ TextWrapping="Wrap"
+ HorizontalAlignment="Left"
+ TextAlignment="Left"
+ Text="{locale:Locale ProfileImageSelectionNote}" />
+ <StackPanel
+ Grid.Row="2"
+ Spacing="10"
+ HorizontalAlignment="Left"
+ Orientation="Horizontal">
+ <Button
+ Width="50"
+ MinWidth="50"
+ Click="GoBack">
+ <ui:SymbolIcon Symbol="Back" />
+ </Button>
+ </StackPanel>
+ <StackPanel
+ Grid.Row="2"
+ Spacing="10"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal">
+ <Button
+ Name="Import"
+ Click="Import_OnClick">
+ <TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
+ </Button>
+ <Button
+ Name="SelectFirmwareImage"
+ IsEnabled="{Binding FirmwareFound}"
+ Click="SelectFirmwareImage_OnClick">
+ <TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
+ </Button>
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs
new file mode 100644
index 00000000..18f76f80
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs
@@ -0,0 +1,124 @@
+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.Controls;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using System.IO;
+using Image = SixLabors.ImageSharp.Image;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserProfileImageSelectorView : UserControl
+ {
+ private ContentManager _contentManager;
+ private NavigationDialogHost _parent;
+ private TempProfile _profile;
+
+ internal UserProfileImageSelectorViewModel ViewModel { get; private set; }
+
+ public UserProfileImageSelectorView()
+ {
+ 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;
+
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}";
+
+ if (Program.PreviewerDetached)
+ {
+ DataContext = ViewModel = new UserProfileImageSelectorViewModel();
+ ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
+ }
+
+ break;
+ case NavigationMode.Back:
+ if (_profile.Image != null)
+ {
+ _parent.GoBack();
+ }
+ break;
+ }
+ }
+ }
+
+ private async void Import_OnClick(object sender, RoutedEventArgs e)
+ {
+ OpenFileDialog dialog = new();
+ dialog.Filters.Add(new FileDialogFilter
+ {
+ Name = LocaleManager.Instance[LocaleKeys.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));
+
+ if (_profile.Image != null)
+ {
+ _parent.GoBack();
+ }
+ }
+ }
+ }
+
+ private void GoBack(object sender, RoutedEventArgs e)
+ {
+ _parent.GoBack();
+ }
+
+ private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (ViewModel.FirmwareFound)
+ {
+ _parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_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/Views/User/UserRecovererView.axaml b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml
new file mode 100644
index 00000000..62b5e184
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml
@@ -0,0 +1,83 @@
+<UserControl
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ d:DesignWidth="550"
+ d:DesignHeight="450"
+ Width="500"
+ Height="400"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
+ x:CompileBindings="True"
+ x:DataType="viewModels:UserProfileViewModel"
+ Focusable="True">
+ <Design.DataContext>
+ <viewModels:UserProfileViewModel />
+ </Design.DataContext>
+ <Grid HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Border
+ CornerRadius="5"
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ BorderThickness="1"
+ Grid.Row="0">
+ <Panel>
+ <ListBox
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Items="{Binding LostProfiles}">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Border
+ Margin="2"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ClipToBounds="True"
+ CornerRadius="5">
+ <Grid Margin="0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding UserId}"
+ TextAlignment="Left"
+ TextWrapping="Wrap" />
+ <Button Grid.Column="1"
+ HorizontalAlignment="Right"
+ Click="Recover"
+ CommandParameter="{Binding}"
+ Content="{locale:Locale Recover}"/>
+ </Grid>
+ </Border>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ <TextBlock
+ IsVisible="{Binding IsEmpty}"
+ TextAlignment="Center"
+ Text="{locale:Locale UserProfilesRecoverEmptyList}"/>
+ </Panel>
+ </Border>
+ <StackPanel
+ Grid.Row="1"
+ Margin="0 24 0 0"
+ Orientation="Horizontal">
+ <Button
+ Width="50"
+ MinWidth="50"
+ Click="GoBack">
+ <ui:SymbolIcon Symbol="Back"/>
+ </Button>
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs
new file mode 100644
index 00000000..0c53e53d
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs
@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserRecovererView : UserControl
+ {
+ private NavigationDialogHost _parent;
+
+ public UserRecovererView()
+ {
+ InitializeComponent();
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ switch (arg.NavigationMode)
+ {
+ case NavigationMode.New:
+ var parent = (NavigationDialogHost)arg.Parameter;
+
+ _parent = parent;
+
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
+
+ break;
+ }
+ }
+ }
+
+ private void GoBack(object sender, RoutedEventArgs e)
+ {
+ _parent?.GoBack();
+ }
+
+ private void Recover(object sender, RoutedEventArgs e)
+ {
+ _parent?.RecoverLostAccounts();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml
new file mode 100644
index 00000000..cdf74d52
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml
@@ -0,0 +1,199 @@
+<UserControl
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ mc:Ignorable="d"
+ d:DesignWidth="600"
+ d:DesignHeight="500"
+ Height="450"
+ Width="550"
+ x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
+ x:CompileBindings="True"
+ x:DataType="viewModels:UserSaveManagerViewModel"
+ Focusable="True">
+ <Design.DataContext>
+ <viewModels:UserSaveManagerViewModel />
+ </Design.DataContext>
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid
+ Grid.Row="0"
+ HorizontalAlignment="Stretch">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <StackPanel
+ Spacing="10"
+ Orientation="Horizontal"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center">
+ <Label Content="{locale:Locale CommonSort}" VerticalAlignment="Center" />
+ <ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
+ <ComboBoxItem>
+ <Label
+ VerticalAlignment="Center"
+ HorizontalContentAlignment="Left"
+ Content="{locale:Locale Name}" />
+ </ComboBoxItem>
+ <ComboBoxItem>
+ <Label
+ VerticalAlignment="Center"
+ HorizontalContentAlignment="Left"
+ Content="{locale:Locale Size}" />
+ </ComboBoxItem>
+ </ComboBox>
+ <ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
+ <ComboBoxItem>
+ <Label
+ VerticalAlignment="Center"
+ HorizontalContentAlignment="Left"
+ Content="{locale:Locale OrderAscending}" />
+ </ComboBoxItem>
+ <ComboBoxItem>
+ <Label
+ VerticalAlignment="Center"
+ HorizontalContentAlignment="Left"
+ Content="{locale:Locale OrderDescending}" />
+ </ComboBoxItem>
+ </ComboBox>
+ </StackPanel>
+ <Grid
+ Grid.Column="1"
+ HorizontalAlignment="Stretch"
+ Margin="10,0, 0, 0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition/>
+ </Grid.ColumnDefinitions>
+ <Label Content="{locale:Locale Search}" VerticalAlignment="Center" />
+ <TextBox
+ Margin="5,0,0,0"
+ Grid.Column="1"
+ HorizontalAlignment="Stretch"
+ Text="{Binding Search}" />
+ </Grid>
+ </Grid>
+ <Border
+ Grid.Row="1"
+ Margin="0,5"
+ BorderThickness="1"
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ CornerRadius="5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <ListBox
+ Name="SaveList"
+ Items="{Binding Views}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="10" />
+ <Setter Property="Margin" Value="5" />
+ <Setter Property="CornerRadius" Value="4" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate x:DataType="models:SaveModel">
+ <Grid HorizontalAlignment="Stretch">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <StackPanel
+ Grid.Column="0"
+ Orientation="Horizontal"
+ Spacing="5">
+ <Border
+ Height="42"
+ Width="42"
+ Padding="10"
+ IsVisible="{Binding !InGameList}">
+ <ui:SymbolIcon
+ Symbol="Help"
+ FontSize="30"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Border>
+ <Image
+ IsVisible="{Binding InGameList}"
+ Width="42"
+ Height="42"
+ Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
+ <TextBlock
+ MaxLines="3"
+ Width="320"
+ Margin="5"
+ TextWrapping="Wrap"
+ Text="{Binding Title}"
+ VerticalAlignment="Center" />
+ </StackPanel>
+ <StackPanel
+ Grid.Column="1"
+ Spacing="10"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal">
+ <Label
+ Content="{Binding SizeString}"
+ IsVisible="{Binding SizeAvailable}"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right" />
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Name="OpenLocation"
+ Click="OpenLocation">
+ <ui:SymbolIcon
+ Symbol="OpenFolder"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Name="Delete"
+ Click="Delete">
+ <ui:SymbolIcon
+ Symbol="Delete"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ </StackPanel>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Border>
+ <StackPanel
+ Grid.Row="2"
+ Margin="0 24 0 0"
+ Orientation="Horizontal">
+ <Button
+ Width="50"
+ MinWidth="50"
+ Click="GoBack">
+ <ui:SymbolIcon Symbol="Back" />
+ </Button>
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
new file mode 100644
index 00000000..9d955326
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
@@ -0,0 +1,148 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+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.Controls;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using UserId = LibHac.Fs.UserId;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserSaveManagerView : UserControl
+ {
+ internal UserSaveManagerViewModel ViewModel { get; private set; }
+
+ private AccountManager _accountManager;
+ private HorizonClient _horizonClient;
+ private VirtualFileSystem _virtualFileSystem;
+ private NavigationDialogHost _parent;
+
+ public UserSaveManagerView()
+ {
+ InitializeComponent();
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ switch (arg.NavigationMode)
+ {
+ case NavigationMode.New:
+ var args = ((NavigationDialogHost parent, AccountManager accountManager, HorizonClient client, VirtualFileSystem virtualFileSystem))arg.Parameter;
+ _accountManager = args.accountManager;
+ _horizonClient = args.client;
+ _virtualFileSystem = args.virtualFileSystem;
+
+ _parent = args.parent;
+ break;
+ }
+
+ DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager);
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}";
+
+ Task.Run(LoadSaves);
+ }
+ }
+
+ public void LoadSaves()
+ {
+ ViewModel.Saves.Clear();
+ var saves = new ObservableCollection<SaveModel>();
+ var saveDataFilter = SaveDataFilter.Make(
+ programId: default,
+ saveType: SaveDataType.Account,
+ new UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low),
+ saveDataId: default,
+ index: default);
+
+ using var saveDataIterator = new UniqueRef<SaveDataIterator>();
+
+ _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
+
+ Span<SaveDataInfo> 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);
+ }
+ }
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ ViewModel.Saves = saves;
+ ViewModel.Sort();
+ });
+ }
+
+ private void GoBack(object sender, RoutedEventArgs e)
+ {
+ _parent?.GoBack();
+ }
+
+ private void OpenLocation(object sender, RoutedEventArgs e)
+ {
+ if (sender is Avalonia.Controls.Button button)
+ {
+ if (button.DataContext is SaveModel saveModel)
+ {
+ ApplicationHelper.OpenSaveDir(saveModel.SaveId);
+ }
+ }
+ }
+
+ private async void Delete(object sender, RoutedEventArgs e)
+ {
+ if (sender is Avalonia.Controls.Button button)
+ {
+ if (button.DataContext is SaveModel saveModel)
+ {
+ var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
+ LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
+ LocaleManager.Instance[LocaleKeys.InputDialogYes],
+ LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
+
+ if (result == UserResult.Yes)
+ {
+ _horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, saveModel.SaveId);
+ }
+
+ ViewModel.Saves.Remove(saveModel);
+ ViewModel.Views.Remove(saveModel);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml
new file mode 100644
index 00000000..9a6ba054
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml
@@ -0,0 +1,165 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Views.User.UserSelectorViews"
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ d:DesignHeight="450"
+ MinWidth="500"
+ d:DesignWidth="800"
+ mc:Ignorable="d"
+ Focusable="True"
+ x:CompileBindings="True"
+ x:DataType="viewModels:UserProfileViewModel">
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ </UserControl.Resources>
+ <Design.DataContext>
+ <viewModels:UserProfileViewModel />
+ </Design.DataContext>
+ <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Border
+ CornerRadius="5"
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ BorderThickness="1">
+ <ListBox
+ MaxHeight="300"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center"
+ SelectionChanged="ProfilesList_SelectionChanged"
+ Background="Transparent"
+ Items="{Binding Profiles}">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <flex:FlexPanel
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ AlignContent="FlexStart"
+ JustifyContent="FlexStart" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Margin" Value="5 5 0 5" />
+ <Setter Property="CornerRadius" Value="5" />
+ </Style>
+ <Style Selector="Border#SelectionIndicator">
+ <Setter Property="Opacity" Value="0" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.DataTemplates>
+ <DataTemplate
+ DataType="models:UserProfile">
+ <Grid
+ PointerEnter="Grid_PointerEntered"
+ PointerLeave="Grid_OnPointerExited">
+ <Border
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ClipToBounds="True"
+ CornerRadius="5"
+ Background="{Binding BackgroundColor}">
+ <StackPanel
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <Image
+ Width="96"
+ Height="96"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ MaxWidth="90"
+ Text="{Binding Name}"
+ TextAlignment="Center"
+ TextWrapping="Wrap"
+ TextTrimming="CharacterEllipsis"
+ MaxLines="2"
+ Margin="5" />
+ </StackPanel>
+ </Border>
+ <Border
+ Margin="2"
+ Height="24"
+ Width="24"
+ CornerRadius="12"
+ HorizontalAlignment="Right"
+ VerticalAlignment="Top"
+ Background="{DynamicResource ThemeContentBackgroundColor}"
+ IsVisible="{Binding IsPointerOver}">
+ <Button
+ MaxHeight="24"
+ MaxWidth="24"
+ MinHeight="24"
+ MinWidth="24"
+ CornerRadius="12"
+ Padding="0"
+ Click="EditUser">
+ <ui:SymbolIcon Symbol="Edit" />
+ </Button>
+ </Border>
+ </Grid>
+ </DataTemplate>
+ <DataTemplate
+ DataType="viewModels:BaseModel">
+ <Panel
+ Height="118"
+ Width="96">
+ <Button
+ MinWidth="50"
+ MinHeight="50"
+ MaxWidth="50"
+ MaxHeight="50"
+ CornerRadius="25"
+ Margin="10"
+ Padding="0"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ Click="AddUser">
+ <ui:SymbolIcon Symbol="Add" />
+ </Button>
+ <Panel.Styles>
+ <Style Selector="Panel">
+ <Setter Property="Background" Value="{DynamicResource ListBoxBackground}"/>
+ </Style>
+ </Panel.Styles>
+ </Panel>
+ </DataTemplate>
+ </ListBox.DataTemplates>
+ </ListBox>
+ </Border>
+ <StackPanel
+ Grid.Row="1"
+ Margin="0 24 0 0"
+ HorizontalAlignment="Left"
+ Orientation="Horizontal"
+ Spacing="10">
+ <Button
+ Click="ManageSaves"
+ Content="{locale:Locale UserProfilesManageSaves}" />
+ <Button
+ Click="RecoverLostAccounts"
+ Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
+ </StackPanel>
+ <StackPanel
+ Grid.Row="1"
+ Margin="0 24 0 0"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal">
+ <Button
+ Click="Close"
+ Content="{locale:Locale UserProfilesClose}" />
+ </StackPanel>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs
new file mode 100644
index 00000000..aa89fea9
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs
@@ -0,0 +1,128 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.ViewModels;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserSelectorViews : UserControl
+ {
+ private NavigationDialogHost _parent;
+
+ public UserProfileViewModel ViewModel { get; set; }
+
+ public UserSelectorViews()
+ {
+ InitializeComponent();
+
+ if (Program.PreviewerDetached)
+ {
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ if (arg.NavigationMode == NavigationMode.New)
+ {
+ _parent = (NavigationDialogHost)arg.Parameter;
+ ViewModel = _parent.ViewModel;
+ }
+
+ if (arg.NavigationMode == NavigationMode.Back)
+ {
+ ((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle];
+ }
+
+ DataContext = ViewModel;
+ }
+ }
+
+ private void Grid_PointerEntered(object sender, PointerEventArgs e)
+ {
+ if (sender is Grid grid)
+ {
+ if (grid.DataContext is UserProfile profile)
+ {
+ profile.IsPointerOver = true;
+ }
+ }
+ }
+
+ private void Grid_OnPointerExited(object sender, PointerEventArgs e)
+ {
+ if (sender is Grid grid)
+ {
+ if (grid.DataContext is UserProfile profile)
+ {
+ profile.IsPointerOver = false;
+ }
+ }
+ }
+
+ private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is ListBox listBox)
+ {
+ int selectedIndex = listBox.SelectedIndex;
+
+ if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
+ {
+ if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
+ {
+ _parent?.AccountManager?.OpenUser(userProfile.UserId);
+
+ foreach (BaseModel profile in ViewModel.Profiles)
+ {
+ if (profile is UserProfile uProfile)
+ {
+ uProfile.UpdateState();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void AddUser(object sender, RoutedEventArgs e)
+ {
+ _parent.AddUser();
+ }
+
+ private void EditUser(object sender, RoutedEventArgs e)
+ {
+ if (sender is Avalonia.Controls.Button button)
+ {
+ if (button.DataContext is UserProfile userProfile)
+ {
+ _parent.EditUser(userProfile);
+ }
+ }
+ }
+
+ private void ManageSaves(object sender, RoutedEventArgs e)
+ {
+ _parent.ManageSaves();
+ }
+
+ private void RecoverLostAccounts(object sender, RoutedEventArgs e)
+ {
+ _parent.RecoverLostAccounts();
+ }
+
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ ((ContentDialog)_parent.Parent).Hide();
+ }
+ }
+} \ No newline at end of file