diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs index 05bddc76f..9d5b53177 100644 --- a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs @@ -27,9 +27,19 @@ 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(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg b/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg new file mode 100644 index 000000000..8310994de Binary files /dev/null and b/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg differ diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 9c5d0c4c6..5ec79a57c 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -59,6 +59,7 @@ + diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs index f33c8d7aa..35ec69135 100644 --- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs +++ b/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 @@ -65,5 +66,10 @@ namespace Ryujinx.HLE.UI /// Takes a screenshot from the current renderer and saves it in the screenshots folder. /// void TakeScreenshot(); + + /// + /// Displays the player select dialog and returns the selected profile. + /// + UserProfile ShowPlayerSelectDialog(); } } diff --git a/src/Ryujinx/Headless/WindowBase.cs b/src/Ryujinx/Headless/WindowBase.cs index 77b7e5a81..49e465569 100644 --- a/src/Ryujinx/Headless/WindowBase.cs +++ b/src/Ryujinx/Headless/WindowBase.cs @@ -10,6 +10,7 @@ using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; 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.Loaders.Processes; using Ryujinx.HLE.UI; @@ -28,6 +29,7 @@ using static SDL.SDL3; 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 { @@ -590,5 +592,10 @@ namespace Ryujinx.Headless { throw new NotImplementedException(); } + + public UserProfile ShowPlayerSelectDialog() + { + return AccountSaveDataManager.GetLastUsedUser(); + } } } diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 1c04aa02e..4685c84a7 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -169,4 +169,10 @@ + + + UserSelectorDialog.axaml + Code + + diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 21bac3935..db984595c 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -1,17 +1,23 @@ 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.Windows; +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 Ryujinx.UI.Common.Configuration; using System; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading; namespace Ryujinx.Ava.UI.Applet @@ -256,11 +262,61 @@ namespace Ryujinx.Ava.UI.Applet return showDetails; } - public IDynamicTextInputHandler CreateDynamicTextInputHandler() - { - return new AvaloniaDynamicTextInputHandler(_parent); - } + 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 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)); + ProfileSelectorDialogViewModel viewModel = new() + { + Profiles = profiles, + SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId + }; + (selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel); + + 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; + } + public void TakeScreenshot() { _parent.ViewModel.AppHost.ScreenshotRequested = true; diff --git a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml new file mode 100644 index 000000000..369790053 --- /dev/null +++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs new file mode 100644 index 000000000..8d2c54088 --- /dev/null +++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs @@ -0,0 +1,118 @@ +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.Common.Logging; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.ObjectModel; +using System.ComponentModel; +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 ProfileSelectorDialog : UserControl + { + public ProfileSelectorDialogViewModel ViewModel { get; set; } + + public ProfileSelectorDialog(ProfileSelectorDialogViewModel viewModel) + { + DataContext = ViewModel = viewModel; + + InitializeComponent(); + } + + 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: {userProfile.UserId}", "ProfileSelector"); + + ObservableCollection newProfiles = []; + + foreach (BaseModel item in ViewModel.Profiles) + { + if (item is UserProfile originalItem) + { + UserProfileSft profile = new(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(ProfileSelectorDialogViewModel viewModel) + { + FAContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue], + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel], + Content = new ProfileSelectorDialog(viewModel), + Padding = new Thickness(0) + }; + + UserId result = UserId.Null; + bool input = false; + + contentDialog.Closed += Handler; + + await ContentDialogHelper.ShowAsync(contentDialog); + + return (result, input); + + void Handler(FAContentDialog sender, FAContentDialogClosedEventArgs eventArgs) + { + if (eventArgs.Result == FAContentDialogResult.Primary) + { + result = viewModel.SelectedUserId; + input = true; + } + else + { + result = UserId.Null; + input = false; + } + } + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs new file mode 100644 index 000000000..979e1616a --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.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 ProfileSelectorDialogViewModel : BaseModel + { + + [ObservableProperty] private UserId _selectedUserId; + + [ObservableProperty] private ObservableCollection _profiles = []; + } +}