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