diff options
| author | Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> | 2022-12-29 14:24:05 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-12-29 15:24:05 +0100 |
| commit | 76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d (patch) | |
| tree | 05013214e4696a9254369d0706173f58877f6a83 /Ryujinx.Ava/UI/Controls | |
| parent | 3d1a0bf3749afa14da5b5ba1e0666fdb78c99beb (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')
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 |
