aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/UI/Controls
diff options
context:
space:
mode:
authorIsaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>2022-12-29 14:24:05 +0000
committerGitHub <noreply@github.com>2022-12-29 15:24:05 +0100
commit76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d (patch)
tree05013214e4696a9254369d0706173f58877f6a83 /Ryujinx.Ava/UI/Controls
parent3d1a0bf3749afa14da5b5ba1e0666fdb78c99beb (diff)
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>
Diffstat (limited to 'Ryujinx.Ava/UI/Controls')
-rw-r--r--Ryujinx.Ava/UI/Controls/GameGridView.axaml195
-rw-r--r--Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs83
-rw-r--r--Ryujinx.Ava/UI/Controls/GameListView.axaml234
-rw-r--r--Ryujinx.Ava/UI/Controls/GameListView.axaml.cs83
-rw-r--r--Ryujinx.Ava/UI/Controls/InputDialog.axaml32
-rw-r--r--Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs57
-rw-r--r--Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml16
-rw-r--r--Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs91
-rw-r--r--Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml57
-rw-r--r--Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs105
-rw-r--r--Ryujinx.Ava/UI/Controls/RendererHost.axaml11
-rw-r--r--Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs127
-rw-r--r--Ryujinx.Ava/UI/Controls/SaveManager.axaml175
-rw-r--r--Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs160
-rw-r--r--Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml42
-rw-r--r--Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs20
-rw-r--r--Ryujinx.Ava/UI/Controls/UserEditor.axaml86
-rw-r--r--Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs118
-rw-r--r--Ryujinx.Ava/UI/Controls/UserRecoverer.axaml72
-rw-r--r--Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs44
-rw-r--r--Ryujinx.Ava/UI/Controls/UserSelector.axaml145
-rw-r--r--Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs77
22 files changed, 2030 insertions, 0 deletions
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 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Controls.GameGridView"
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
+ 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:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ d:DesignHeight="450"
+ d:DesignWidth="800"
+ mc:Ignorable="d"
+ Focusable="True">
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+ <MenuItem
+ Command="{Binding ToggleFavorite}"
+ Header="{locale:Locale GameListContextMenuToggleFavorite}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenUserSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDeviceSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenBcatSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenTitleUpdateManager}"
+ Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDownloadableContentManager}"
+ Header="{locale:Locale GameListContextMenuManageDlc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
+ <MenuItem
+ Command="{Binding OpenCheatManager}"
+ Header="{locale:Locale GameListContextMenuManageCheat}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
+ <MenuItem
+ Command="{Binding OpenModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenSdModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
+ <Separator />
+ <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
+ <MenuItem
+ Command="{Binding PurgePtcCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
+ <MenuItem
+ Command="{Binding PurgeShaderCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
+ <MenuItem
+ Command="{Binding OpenPtcDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenShaderCacheDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
+ </MenuItem>
+ <MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
+ <MenuItem
+ Command="{Binding ExtractExeFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractRomFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractLogo}"
+ Header="{locale:Locale GameListContextMenuExtractDataLogo}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
+ </MenuItem>
+ </MenuFlyout>
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*" />
+ </Grid.RowDefinitions>
+ <ListBox
+ Grid.Row="0"
+ Padding="8"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ContextFlyout="{StaticResource GameContextMenu}"
+ DoubleTapped="GameList_DoubleTapped"
+ Items="{Binding AppsObservableList}"
+ SelectionChanged="GameList_SelectionChanged">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <flex:FlexPanel
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ AlignContent="FlexStart"
+ JustifyContent="Center" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="0" />
+ <Setter Property="Margin" Value="5" />
+ <Setter Property="CornerRadius" Value="4" />
+ <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
+ <Style.Animations>
+ <Animation Duration="0:0:0.7">
+ <KeyFrame Cue="0%">
+ <Setter Property="MaxWidth" Value="0" />
+ <Setter Property="Opacity" Value="0.0" />
+ </KeyFrame>
+ <KeyFrame Cue="50%">
+ <Setter Property="MaxWidth" Value="1000" />
+ <Setter Property="Opacity" Value="0.3" />
+ </KeyFrame>
+ <KeyFrame Cue="100%">
+ <Setter Property="MaxWidth" Value="1000" />
+ <Setter Property="Opacity" Value="1.0" />
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ </Style>
+ <Style Selector="ListBoxItem:selected /template/ ContentPresenter">
+ <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
+ </Style>
+ <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
+ <Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Border
+ Margin="10"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ ClipToBounds="True"
+ CornerRadius="4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Image
+ Grid.Row="0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
+ <Panel
+ Grid.Row="1"
+ Height="50"
+ Margin="0 10 0 0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}">
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center"
+ Text="{Binding TitleName}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
+ </Panel>
+ </Grid>
+ </Border>
+ <ui:SymbolIcon
+ Margin="5,5,0,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Top"
+ FontSize="16"
+ Foreground="{DynamicResource SystemAccentColor}"
+ IsVisible="{Binding Favorite}"
+ Symbol="StarFilled" />
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+</UserControl> \ 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<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
+ RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
+
+ public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
+ ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
+ ((menu.Items as AvaloniaList<object>)[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 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Controls.GameListView"
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ 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:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ d:DesignHeight="450"
+ d:DesignWidth="800"
+ mc:Ignorable="d"
+ Focusable="True">
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+ <MenuItem
+ Command="{Binding ToggleFavorite}"
+ Header="{locale:Locale GameListContextMenuToggleFavorite}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenUserSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDeviceSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenBcatSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenTitleUpdateManager}"
+ Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDownloadableContentManager}"
+ Header="{locale:Locale GameListContextMenuManageDlc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
+ <MenuItem
+ Command="{Binding OpenCheatManager}"
+ Header="{locale:Locale GameListContextMenuManageCheat}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
+ <MenuItem
+ Command="{Binding OpenModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenSdModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
+ <Separator />
+ <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
+ <MenuItem
+ Command="{Binding PurgePtcCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
+ <MenuItem
+ Command="{Binding PurgeShaderCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
+ <MenuItem
+ Command="{Binding OpenPtcDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenShaderCacheDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
+ </MenuItem>
+ <MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
+ <MenuItem
+ Command="{Binding ExtractExeFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractRomFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractLogo}"
+ Header="{locale:Locale GameListContextMenuExtractDataLogo}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
+ </MenuItem>
+ </MenuFlyout>
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*" />
+ </Grid.RowDefinitions>
+ <ListBox
+ Name="GameListBox"
+ Grid.Row="0"
+ Padding="8"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ContextFlyout="{StaticResource GameContextMenu}"
+ DoubleTapped="GameList_DoubleTapped"
+ Items="{Binding AppsObservableList}"
+ SelectionChanged="GameList_SelectionChanged">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <StackPanel
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Orientation="Vertical"
+ Spacing="2" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="0" />
+ <Setter Property="Margin" Value="0" />
+ <Setter Property="CornerRadius" Value="5" />
+ <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
+ <Setter Property="BorderThickness" Value="2"/>
+ <Style.Animations>
+ <Animation Duration="0:0:0.7">
+ <KeyFrame Cue="0%">
+ <Setter Property="MaxHeight" Value="0" />
+ <Setter Property="Opacity" Value="0.0" />
+ </KeyFrame>
+ <KeyFrame Cue="50%">
+ <Setter Property="MaxHeight" Value="1000" />
+ <Setter Property="Opacity" Value="0.3" />
+ </KeyFrame>
+ <KeyFrame Cue="100%">
+ <Setter Property="MaxHeight" Value="1000" />
+ <Setter Property="Opacity" Value="1.0" />
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ </Style>
+ <Style Selector="ListBoxItem:selected /template/ ContentPresenter">
+ <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
+ </Style>
+ <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
+ <Setter Property="MinHeight" Value="100" />
+ </Style>
+ <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
+ <Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Border
+ Margin="0"
+ Padding="10"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ClipToBounds="True"
+ CornerRadius="5">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="10" />
+ <ColumnDefinition Width="*" />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <Image
+ Grid.RowSpan="3"
+ Grid.Column="0"
+ Margin="0"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
+ <StackPanel
+ Grid.Column="2"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Top"
+ Orientation="Vertical"
+ Spacing="5" >
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding TitleName}"
+ TextAlignment="Left"
+ TextWrapping="Wrap" />
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding Developer}"
+ TextAlignment="Left"
+ TextWrapping="Wrap" />
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding Version}"
+ TextAlignment="Left"
+ TextWrapping="Wrap" />
+ </StackPanel>
+ <StackPanel
+ Grid.Column="3"
+ HorizontalAlignment="Right"
+ VerticalAlignment="Top"
+ Orientation="Vertical"
+ Spacing="5">
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding TimePlayed}"
+ TextAlignment="Right"
+ TextWrapping="Wrap" />
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding LastPlayed}"
+ TextAlignment="Right"
+ TextWrapping="Wrap" />
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding FileSize}"
+ TextAlignment="Right"
+ TextWrapping="Wrap" />
+ </StackPanel>
+ <ui:SymbolIcon
+ Grid.Row="0"
+ Grid.Column="0"
+ Margin="-5,-5,0,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Top"
+ FontSize="16"
+ Foreground="{DynamicResource SystemAccentColor}"
+ IsVisible="{Binding Favorite}"
+ Symbol="StarFilled" />
+ </Grid>
+ </Border>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+</UserControl> \ 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<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
+ RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
+
+ public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
+ ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
+ ((menu.Items as AvaloniaList<object>)[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 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Controls.InputDialog"
+ 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"
+ Focusable="True">
+ <Grid
+ Margin="5,10,5,5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock HorizontalAlignment="Center" Text="{Binding Message}" />
+ <TextBox
+ Grid.Row="1"
+ Width="300"
+ Margin="10"
+ HorizontalAlignment="Center"
+ MaxLength="{Binding MaxLength}"
+ Text="{Binding Input, Mode=TwoWay}" />
+ <TextBlock
+ Grid.Row="2"
+ Margin="5,5,5,10"
+ HorizontalAlignment="Center"
+ Text="{Binding SubMessage}" />
+ </Grid>
+</UserControl> \ 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 @@
+<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"
+ mc:Ignorable="d"
+ d:DesignWidth="800"
+ d:DesignHeight="450"
+ x:Class="Ryujinx.Ava.UI.Controls.NavigationDialogHost"
+ Focusable="True">
+ <ui:Frame
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ x:Name="ContentFrame" />
+</UserControl> \ 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 @@
+<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.UI.Controls.ProfileImageSelectionDialog"
+ Focusable="True">
+ <Grid
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center"
+ Margin="5,10,5, 5">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="70" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock
+ FontWeight="Bold"
+ FontSize="18"
+ HorizontalAlignment="Center"
+ Grid.Row="1"
+ Text="{locale:Locale ProfileImageSelectionHeader}" />
+ <TextBlock
+ FontWeight="Bold"
+ Grid.Row="2"
+ Margin="10"
+ MaxWidth="400"
+ TextWrapping="Wrap"
+ HorizontalAlignment="Center"
+ TextAlignment="Center"
+ Text="{locale:Locale ProfileImageSelectionNote}" />
+ <StackPanel
+ Margin="5,0"
+ Spacing="10"
+ Grid.Row="4"
+ HorizontalAlignment="Center"
+ Orientation="Horizontal">
+ <Button
+ Name="Import"
+ Click="Import_OnClick"
+ Width="200">
+ <TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
+ </Button>
+ <Button
+ Name="SelectFirmwareImage"
+ IsEnabled="{Binding FirmwareFound}"
+ Click="SelectFirmwareImage_OnClick"
+ Width="200">
+ <TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
+ </Button>
+ </StackPanel>
+ </Grid>
+</UserControl> \ 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 @@
+<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="800"
+ d:DesignHeight="450"
+ x:Class="Ryujinx.Ava.UI.Controls.RendererHost"
+ Focusable="True">
+</UserControl>
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<EventArgs> RendererInitialized;
+ public event Action<object, Size> 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 @@
+<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:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ 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"
+ mc:Ignorable="d"
+ d:DesignWidth="800"
+ d:DesignHeight="450"
+ Height="400"
+ Width="550"
+ x:Class="Ryujinx.Ava.UI.Controls.SaveManager"
+ Focusable="True">
+ <UserControl.Resources>
+ <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition />
+ </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 Descending}" />
+ </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"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <ListBox
+ Name="SaveList"
+ Items="{Binding View}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <ListBox.ItemTemplate>
+ <DataTemplate x:DataType="models:SaveModel">
+ <Grid HorizontalAlignment="Stretch" Margin="0,5">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <StackPanel Grid.Column="0" Orientation="Horizontal">
+ <Border
+ Height="42"
+ Margin="2"
+ Width="42"
+ Padding="10"
+ IsVisible="{Binding !InGameList}">
+ <ui:SymbolIcon
+ Symbol="Help"
+ FontSize="30"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Border>
+ <Image
+ IsVisible="{Binding InGameList}"
+ Margin="2"
+ 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"
+ Command="{Binding OpenLocation}">
+ <ui:SymbolIcon
+ Symbol="OpenFolder"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Name="Delete"
+ Command="{Binding Delete}">
+ <ui:SymbolIcon
+ Symbol="Delete"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ </StackPanel>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Border>
+ </Grid>
+</UserControl> \ 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<SaveModel> _view = new ObservableCollection<SaveModel>();
+ private string _search;
+
+ public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>();
+
+ public ObservableCollection<SaveModel> 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<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);
+ 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<SaveModel> GetComparer()
+ {
+ switch (SortIndex)
+ {
+ case 0:
+ return OrderIndex == 0
+ ? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
+ : SortExpressionComparer<SaveModel>.Descending(save => save.Title);
+ case 1:
+ return OrderIndex == 0
+ ? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
+ : SortExpressionComparer<SaveModel>.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 @@
+<Window
+ x:Class="Ryujinx.Ava.UI.Controls.UpdateWaitWindow"
+ 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"
+ Title="Ryujinx - Waiting"
+ SizeToContent="WidthAndHeight"
+ WindowStartupLocation="CenterOwner"
+ mc:Ignorable="d"
+ Focusable="True">
+ <Grid
+ Margin="20"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Image
+ Grid.Row="1"
+ Height="70"
+ MinWidth="50"
+ Margin="5,10,20,10"
+ Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
+ <StackPanel
+ Grid.Row="1"
+ Grid.Column="1"
+ VerticalAlignment="Center"
+ Orientation="Vertical">
+ <TextBlock Name="PrimaryText" Margin="5" />
+ <TextBlock
+ Name="SecondaryText"
+ Margin="5"
+ VerticalAlignment="Center" />
+ </StackPanel>
+ </Grid>
+</Window> \ 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 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Controls.UserEditor"
+ 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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ Margin="0"
+ MinWidth="500"
+ Padding="0"
+ mc:Ignorable="d"
+ Focusable="True">
+ <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
+ HorizontalAlignment="Left"
+ VerticalAlignment="Stretch"
+ Orientation="Vertical">
+ <Image
+ Name="ProfileImage"
+ Width="96"
+ Height="96"
+ Margin="0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+ <Button
+ Name="ChangePictureButton"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ Click="ChangePictureButton_Click"
+ Content="{locale:Locale UserProfilesChangeProfileImage}" />
+ <Button
+ Name="AddPictureButton"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ Click="ChangePictureButton_Click"
+ Content="{locale:Locale UserProfilesSetProfileImage}" />
+ </StackPanel>
+ <StackPanel
+ Grid.Row="0"
+ Grid.Column="1"
+ Margin="5,10"
+ HorizontalAlignment="Stretch"
+ Orientation="Vertical"
+ Spacing="10">
+ <TextBlock Text="{locale:Locale UserProfilesName}" />
+ <TextBox
+ Name="NameBox"
+ Width="300"
+ HorizontalAlignment="Stretch"
+ MaxLength="{Binding MaxProfileNameLength}"
+ Text="{Binding Name}" />
+ <TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
+ <TextBlock Name="IdLabel" Text="{Binding UserId}" />
+ </StackPanel>
+ <StackPanel
+ Grid.Row="1"
+ Grid.Column="0"
+ Grid.ColumnSpan="2"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal"
+ Spacing="10">
+ <Button
+ Name="SaveButton"
+ Click="SaveButton_Click"
+ Content="{locale:Locale Save}" />
+ <Button
+ Name="CloseButton"
+ HorizontalAlignment="Right"
+ Click="CloseButton_Click"
+ Content="{locale:Locale Discard}" />
+ </StackPanel>
+ </Grid>
+</UserControl>
diff --git a/Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs b/Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
new file mode 100644
index 00000000..19fa29e5
--- /dev/null
+++ b/Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
@@ -0,0 +1,118 @@
+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.Helpers;
+using Ryujinx.Ava.UI.Models;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
+
+namespace Ryujinx.Ava.UI.Controls
+{
+ public partial class UserEditor : UserControl
+ {
+ private NavigationDialogHost _parent;
+ private UserProfile _profile;
+ private bool _isNewUser;
+
+ public TempProfile TempProfile { get; set; }
+ public uint MaxProfileNameLength => 0x20;
+
+ public UserEditor()
+ {
+ 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;
+ }
+
+ DataContext = TempProfile;
+
+ AddPictureButton.IsVisible = _isNewUser;
+ IdLabel.IsVisible = _profile != null;
+ IdText.IsVisible = _profile != null;
+ ChangePictureButton.IsVisible = !_isNewUser;
+ }
+ }
+
+ private void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ _parent?.GoBack();
+ }
+
+ private async void SaveButton_Click(object sender, RoutedEventArgs e)
+ {
+ DataValidationErrors.ClearErrors(NameBox);
+ bool isInvalid = false;
+
+ if (string.IsNullOrWhiteSpace(TempProfile.Name))
+ {
+ DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance["UserProfileEmptyNameError"]));
+
+ isInvalid = true;
+ }
+
+ if (TempProfile.Image == null)
+ {
+ await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
+
+ isInvalid = true;
+ }
+
+ if(isInvalid)
+ {
+ 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(ProfileImageSelectionDialog), (_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/Controls/UserRecoverer.axaml b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml
new file mode 100644
index 00000000..69f3d36a
--- /dev/null
+++ b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml
@@ -0,0 +1,72 @@
+<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ mc:Ignorable="d"
+ d:DesignWidth="800"
+ d:DesignHeight="450"
+ MinWidth="500"
+ MinHeight="400"
+ x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer"
+ Focusable="True">
+ <Design.DataContext>
+ <viewModels:UserProfileViewModel />
+ </Design.DataContext>
+ <Grid HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <Button Grid.Row="0"
+ Margin="5"
+ Height="30"
+ Width="50"
+ MinWidth="50"
+ HorizontalAlignment="Left"
+ Command="{Binding GoBack}">
+ <ui:SymbolIcon Symbol="Back"/>
+ </Button>
+ <TextBlock Grid.Row="1"
+ Text="{locale:Locale UserProfilesRecoverHeading}"/>
+ <ListBox
+ Margin="5"
+ Grid.Row="2"
+ 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"
+ Command="{Binding Recover}"
+ CommandParameter="{Binding}"
+ Content="{locale:Locale Recover}"/>
+ </Grid>
+ </Border>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+</UserControl>
diff --git a/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs
new file mode 100644
index 00000000..9f29fddb
--- /dev/null
+++ b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs
@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+
+namespace Ryujinx.Ava.UI.Controls
+{
+ public partial class UserRecoverer : UserControl
+ {
+ private UserProfileViewModel _viewModel;
+ private NavigationDialogHost _parent;
+
+ public UserRecoverer()
+ {
+ 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, UserProfileViewModel viewModel))arg.Parameter;
+
+ _viewModel = args.viewModel;
+ _parent = args.parent;
+ break;
+ }
+
+ DataContext = _viewModel;
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/UI/Controls/UserSelector.axaml b/Ryujinx.Ava/UI/Controls/UserSelector.axaml
new file mode 100644
index 00000000..002d27a0
--- /dev/null
+++ b/Ryujinx.Ava/UI/Controls/UserSelector.axaml
@@ -0,0 +1,145 @@
+<UserControl
+ x:Class="Ryujinx.Ava.UI.Controls.UserSelector"
+ 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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+ d:DesignHeight="450"
+ MinWidth="500"
+ d:DesignWidth="800"
+ mc:Ignorable="d"
+ Focusable="True">
+ <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>
+ <ListBox
+ Margin="5"
+ MaxHeight="300"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center"
+ DoubleTapped="ProfilesList_DoubleTapped"
+ Items="{Binding Profiles}"
+ SelectionChanged="SelectingItemsControl_SelectionChanged">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <flex:FlexPanel
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ AlignContent="FlexStart"
+ JustifyContent="Center" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Border
+ Margin="2"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ ClipToBounds="True"
+ CornerRadius="5">
+ <Grid Margin="0">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Image
+ Grid.Row="0"
+ Width="96"
+ Height="96"
+ Margin="0"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+ <StackPanel
+ Grid.Row="1"
+ Height="30"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+ <TextBlock
+ HorizontalAlignment="Stretch"
+ Text="{Binding Name}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
+ </StackPanel>
+ </Grid>
+ </Border>
+ <Border
+ Width="10"
+ Height="10"
+ Margin="5"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Top"
+ Background="LimeGreen"
+ CornerRadius="5"
+ IsVisible="{Binding IsOpened}" />
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ <Grid
+ Grid.Row="1"
+ HorizontalAlignment="Center">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <Button
+ HorizontalAlignment="Stretch"
+ Grid.Row="0"
+ Grid.Column="0"
+ Margin="2"
+ Command="{Binding AddUser}"
+ Content="{locale:Locale UserProfilesAddNewProfile}" />
+ <Button
+ HorizontalAlignment="Stretch"
+ Grid.Row="0"
+ Margin="2"
+ Grid.Column="1"
+ Command="{Binding EditUser}"
+ Content="{locale:Locale UserProfilesEditProfile}"
+ IsEnabled="{Binding IsSelectedProfiledEditable}" />
+ <Button
+ HorizontalAlignment="Stretch"
+ Grid.Row="1"
+ Grid.Column="0"
+ Margin="2"
+ Content="{locale:Locale UserProfilesManageSaves}"
+ Command="{Binding ManageSaves}" />
+ <Button
+ HorizontalAlignment="Stretch"
+ Grid.Row="1"
+ Grid.Column="1"
+ Margin="2"
+ Command="{Binding DeleteUser}"
+ Content="{locale:Locale UserProfilesDeleteSelectedProfile}"
+ IsEnabled="{Binding IsSelectedProfileDeletable}" />
+ <Button
+ HorizontalAlignment="Stretch"
+ Grid.Row="2"
+ Grid.ColumnSpan="2"
+ Grid.Column="0"
+ Margin="2"
+ Command="{Binding RecoverLostAccounts}"
+ Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
+ </Grid>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs b/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
new file mode 100644
index 00000000..bd8c561e
--- /dev/null
+++ b/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
@@ -0,0 +1,77 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.UI.ViewModels;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
+
+namespace Ryujinx.Ava.UI.Controls
+{
+ public partial class UserSelector : UserControl
+ {
+ private NavigationDialogHost _parent;
+ public UserProfileViewModel ViewModel { get; set; }
+
+ public UserSelector()
+ {
+ 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;
+ }
+
+ DataContext = ViewModel;
+ }
+ }
+
+ private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
+ {
+ if (sender is ListBox listBox)
+ {
+ int selectedIndex = listBox.SelectedIndex;
+
+ if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
+ {
+ ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
+
+ _parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.UserId);
+
+ ViewModel.LoadProfiles();
+
+ foreach (UserProfile profile in ViewModel.Profiles)
+ {
+ profile.UpdateState();
+ }
+ }
+ }
+ }
+
+ private void SelectingItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is ListBox listBox)
+ {
+ int selectedIndex = listBox.SelectedIndex;
+
+ if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
+ {
+ ViewModel.HighlightedProfile = ViewModel.Profiles[selectedIndex];
+ }
+ }
+ }
+ }
+} \ No newline at end of file