Bladeren bron

Add the player select applet. (#537)

This introduces the somewhat completed version of the Player Select
Applet, allowing users to select either a user or a guest from the UI.
Note: Selecting the guest more then once currently does not work.

closes https://github.com/Ryubing/Ryujinx/issues/532
Jacob 1 jaar geleden
bovenliggende
commit
52269964b6

+ 36 - 8
src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs

@@ -26,10 +26,20 @@ namespace Ryujinx.HLE.HOS.Applets
         {
             _normalSession = normalSession;
             _interactiveSession = interactiveSession;
-
-            // TODO(jduncanator): Parse PlayerSelectConfig from input data
-            _normalSession.Push(BuildResponse());
-
+            
+            UserProfile selected = _system.Device.UIHandler.ShowPlayerSelectDialog();
+            if (selected == null)
+            {
+                _normalSession.Push(BuildResponse());
+            }
+            else if (selected.UserId == new UserId("00000000000000000000000000000080"))
+            {
+                _normalSession.Push(BuildGuestResponse());
+            }
+            else
+            {
+                _normalSession.Push(BuildResponse(selected));
+            }
             AppletStateChanged?.Invoke(this, null);
 
             _system.ReturnFocus();
@@ -37,16 +47,34 @@ namespace Ryujinx.HLE.HOS.Applets
             return ResultCode.Success;
         }
 
-        private byte[] BuildResponse()
+        private byte[] BuildResponse(UserProfile selectedUser)
         {
-            UserProfile currentUser = _system.AccountManager.LastOpenedUser;
-
             using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
             using BinaryWriter writer = new(stream);
 
             writer.Write((ulong)PlayerSelectResult.Success);
 
-            currentUser.UserId.Write(writer);
+            selectedUser.UserId.Write(writer);
+
+            return stream.ToArray();
+        }
+        
+        private byte[] BuildGuestResponse()
+        {
+            using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
+            using BinaryWriter writer = new(stream);
+            
+            writer.Write(new byte());
+
+            return stream.ToArray();
+        }
+        
+        private byte[] BuildResponse()
+        {
+            using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
+            using BinaryWriter writer = new(stream);
+            
+            writer.Write((ulong)PlayerSelectResult.Failure);
 
             return stream.ToArray();
         }

BIN
src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg


+ 1 - 0
src/Ryujinx.HLE/Ryujinx.HLE.csproj

@@ -48,6 +48,7 @@
     <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnB.png" />
     <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF6.png" />
     <EmbeddedResource Include="HOS\Services\Account\Acc\DefaultUserImage.jpg" />
+    <EmbeddedResource Include="HOS\Services\Account\Acc\GuestUserImage.jpg" />
   </ItemGroup>
 
 </Project>

+ 7 - 0
src/Ryujinx.HLE/UI/IHostUIHandler.cs

@@ -1,4 +1,5 @@
 using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
 using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
 
 namespace Ryujinx.HLE.UI
@@ -59,5 +60,11 @@ namespace Ryujinx.HLE.UI
         /// Gets fonts and colors used by the host.
         /// </summary>
         IHostUITheme HostUITheme { get; }
+        
+        
+        /// <summary>
+        /// Displays the player select dialog and returns the selected profile.
+        /// </summary>
+        UserProfile ShowPlayerSelectDialog();
     }
 }

+ 7 - 0
src/Ryujinx/Headless/Windows/WindowBase.cs

@@ -9,6 +9,7 @@ using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.GAL.Multithreading;
 using Ryujinx.Graphics.OpenGL;
 using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
 using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
 using Ryujinx.HLE.UI;
 using Ryujinx.Input;
@@ -26,6 +27,7 @@ using static SDL2.SDL;
 using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
 using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
 using Switch = Ryujinx.HLE.Switch;
+using UserProfile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
 
 namespace Ryujinx.Headless
 {
@@ -555,5 +557,10 @@ namespace Ryujinx.Headless
                 SDL2Driver.Instance.Dispose();
             }
         }
+        
+        public UserProfile ShowPlayerSelectDialog()
+        {
+            return AccountSaveDataManager.GetLastUsedUser();
+        }
     }
 }

+ 6 - 0
src/Ryujinx/Ryujinx.csproj

@@ -173,4 +173,10 @@
   <ItemGroup>
     <Folder Include="Assets\Fonts\Mono\" />
   </ItemGroup>
+  <ItemGroup>
+    <Compile Update="UI\Applet\UserSelectorDialog.axaml.cs">
+      <DependentUpon>UserSelectorDialog.axaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
 </Project>

+ 61 - 0
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs

@@ -1,17 +1,24 @@
 using Avalonia.Controls;
 using Avalonia.Threading;
 using FluentAvalonia.UI.Controls;
+using Gommon;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.UI.Controls;
 using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ava.UI.ViewModels.Input;
 using Ryujinx.Ava.UI.Windows;
 using Ryujinx.Ava.Utilities.Configuration;
+using Ryujinx.Common;
 using Ryujinx.HLE;
 using Ryujinx.HLE.HOS.Applets;
 using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
 using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
 using Ryujinx.HLE.UI;
 using System;
+using System.Collections.ObjectModel;
+using System.Linq;
 using System.Threading;
 
 namespace Ryujinx.Ava.UI.Applet
@@ -253,5 +260,59 @@ namespace Ryujinx.Ava.UI.Applet
         }
 
         public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent);
+        
+        public UserProfile ShowPlayerSelectDialog()
+        {
+            UserId selected = UserId.Null;
+            byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg");
+            UserProfile guest = new UserProfile(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage);
+    
+            ManualResetEvent dialogCloseEvent = new(false);
+    
+            Dispatcher.UIThread.InvokeAsync(async () =>
+            {
+                ObservableCollection<BaseModel> profiles = [];
+                NavigationDialogHost nav = new();
+                
+                _parent.AccountManager.GetAllUsers()
+                    .OrderBy(x => x.Name)
+                    .ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
+                
+                profiles.Add(new Models.UserProfile(guest, nav));
+                UserSelectorDialogViewModel viewModel = new();
+                viewModel.Profiles = profiles;
+                viewModel.SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId;
+                UserSelectorDialog content = new(viewModel);
+                (UserId id, _) = await UserSelectorDialog.ShowInputDialog(content);
+                
+                selected = id;
+        
+                dialogCloseEvent.Set();
+            });
+    
+            dialogCloseEvent.WaitOne();
+            
+            UserProfile profile = _parent.AccountManager.LastOpenedUser;
+            if (selected == guest.UserId)
+            {
+                profile = guest;
+            }
+            else if (selected == UserId.Null)
+            {
+                profile = null;
+            }
+            else
+            {
+                foreach (UserProfile p in _parent.AccountManager.GetAllUsers())
+                {
+                    if (p.UserId == selected)
+                    {
+                        profile = p;
+                        break;
+                    }
+                }
+            }
+            return profile;
+        }
     }
 }

+ 121 - 0
src/Ryujinx/UI/Applet/UserSelectorDialog.axaml

@@ -0,0 +1,121 @@
+<UserControl
+    x:Class="Ryujinx.Ava.UI.Applet.UserSelectorDialog"
+    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:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
+    xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+    xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+    d:DesignHeight="450"
+    MinWidth="500"
+    d:DesignWidth="800"
+    mc:Ignorable="d"
+    Focusable="True"
+    x:DataType="viewModels:UserSelectorDialogViewModel">
+    
+    <UserControl.Resources>
+        <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
+    </UserControl.Resources>
+    
+    <Design.DataContext>
+        <viewModels:UserSelectorDialogViewModel />
+    </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"
+                Background="Transparent"
+                ItemsSource="{Binding Profiles}"
+                SelectionChanged="ProfilesList_SelectionChanged">
+                
+                <ListBox.ItemsPanel>
+                    <ItemsPanelTemplate>
+                        <WrapPanel
+                            HorizontalAlignment="Left"
+                            VerticalAlignment="Center"
+                            Orientation="Horizontal" />
+                    </ItemsPanelTemplate>
+                </ListBox.ItemsPanel>
+                
+                <ListBox.Styles>
+                    <Style Selector="ListBoxItem">
+                        <Setter Property="Margin" Value="5 5 0 5" />
+                        <Setter Property="CornerRadius" Value="5" />
+                    </Style>
+                    <Style Selector="Rectangle#SelectionIndicator">
+                        <Setter Property="Opacity" Value="0" />
+                    </Style>
+                </ListBox.Styles>
+                
+                <ListBox.DataTemplates>
+                    <DataTemplate
+                        DataType="models:UserProfile">
+                        <Grid
+                            PointerEntered="Grid_PointerEntered"
+                            PointerExited="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>
+                        </Grid>
+                    </DataTemplate>
+                    <DataTemplate
+                        DataType="viewModels:BaseModel">
+                        <Panel
+                            Height="118"
+                            Width="96">
+                            <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">
+        </StackPanel>
+    </Grid>
+</UserControl>

+ 123 - 0
src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs

@@ -0,0 +1,123 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ava.UI.ViewModels.Input;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
+using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
+
+namespace Ryujinx.Ava.UI.Applet
+{
+    public partial class UserSelectorDialog : UserControl, INotifyPropertyChanged
+    {
+        public UserSelectorDialogViewModel ViewModel { get; set; }
+
+        public UserSelectorDialog(UserSelectorDialogViewModel viewModel)
+        {
+            InitializeComponent();
+            ViewModel = viewModel;
+            DataContext = ViewModel;
+        }
+        
+        private void Grid_PointerEntered(object sender, PointerEventArgs e)
+        {
+            if (sender is Grid { DataContext: UserProfile profile })
+            {
+                profile.IsPointerOver = true;
+            }
+        }
+
+        private void Grid_OnPointerExited(object sender, PointerEventArgs e)
+        {
+            if (sender is Grid { DataContext: 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)
+                    {
+                        ViewModel.SelectedUserId = userProfile.UserId;
+                        Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}");
+
+                        ObservableCollection<BaseModel> newProfiles = [];
+
+                        foreach (var item in ViewModel.Profiles)
+                        {
+                            if (item is UserProfile originalItem)
+                            {
+                                var profile = new UserProfileSft(originalItem.UserId, originalItem.Name, originalItem.Image);
+                                
+                                if (profile.UserId == ViewModel.SelectedUserId)
+                                {
+                                    profile.AccountState = AccountState.Open;
+                                }
+
+                                newProfiles.Add(new UserProfile(profile, new NavigationDialogHost()));
+                            }
+                        }
+
+                        ViewModel.Profiles = newProfiles;
+                    }
+                }
+            }
+        }
+
+        public static async Task<(UserId Id, bool Result)> ShowInputDialog(UserSelectorDialog content)
+        {
+            ContentDialog contentDialog = new()
+            {
+                Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
+                PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
+                SecondaryButtonText = string.Empty,
+                CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
+                Content = content,
+                Padding = new Thickness(0)
+            };
+
+            UserId result = UserId.Null;
+            bool input = false;
+
+            void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
+            {
+                if (eventArgs.Result == ContentDialogResult.Primary)
+                {
+                    if (contentDialog.Content is UserSelectorDialog view)
+                    {
+                        result = view.ViewModel.SelectedUserId;
+                        input = true;
+                    }
+                }
+                else
+                {
+                    result = UserId.Null;
+                    input = false;
+                }
+            }
+
+            contentDialog.Closed += Handler;
+
+            await ContentDialogHelper.ShowAsync(contentDialog);
+
+            return (result, input);
+        }
+    }
+}

+ 14 - 0
src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs

@@ -0,0 +1,14 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Collections.ObjectModel;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+    public partial class UserSelectorDialogViewModel : BaseModel
+    {
+
+        [ObservableProperty] private UserId _selectedUserId;
+
+        [ObservableProperty] private ObservableCollection<BaseModel> _profiles = [];
+    }
+}