From d9053bbe3745846dd758561e24dd060d76b3ad9d Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 2 Dec 2022 13:16:43 +0000 Subject: [PATCH] Avalonia - Save Manager (#3476) * Add save manager to account selector * add fallback to app metadata for titlename if app is not in gamelist * Allow recovering lost accounts --- Ryujinx.Ava/Assets/Locales/en_US.json | 15 +- Ryujinx.Ava/Assets/Styles/BaseDark.xaml | 3 + Ryujinx.Ava/Assets/Styles/Styles.xaml | 15 +- Ryujinx.Ava/Common/ApplicationHelper.cs | 5 + .../Ui/Controls/NavigationDialogHost.axaml.cs | 12 +- Ryujinx.Ava/Ui/Controls/SaveManager.axaml | 102 +++++++++++ Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs | 160 ++++++++++++++++++ Ryujinx.Ava/Ui/Controls/UserEditor.axaml | 3 +- Ryujinx.Ava/Ui/Controls/UserEditor.axaml.cs | 18 +- Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml | 70 ++++++++ .../Ui/Controls/UserRecoverer.axaml.cs | 44 +++++ Ryujinx.Ava/Ui/Controls/UserSelector.axaml | 51 +++++- Ryujinx.Ava/Ui/Models/SaveModel.cs | 122 +++++++++++++ Ryujinx.Ava/Ui/Models/TempProfile.cs | 9 +- Ryujinx.Ava/Ui/Models/UserProfile.cs | 10 +- .../Ui/ViewModels/MainWindowViewModel.cs | 11 +- .../Ui/ViewModels/UserProfileViewModel.cs | 77 ++++++++- Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs | 3 + Ryujinx.Ui.Common/App/ApplicationLibrary.cs | 5 +- Ryujinx.Ui.Common/App/ApplicationMetadata.cs | 1 + 20 files changed, 693 insertions(+), 43 deletions(-) create mode 100644 Ryujinx.Ava/Ui/Controls/SaveManager.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Models/SaveModel.cs diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index 4561ad8b..be3071cc 100644 --- a/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/Ryujinx.Ava/Assets/Locales/en_US.json @@ -596,7 +596,18 @@ "RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?", "SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:", "SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:", - "VolumeShort": "Vol", "SettingsEnableMacroHLE": "Enable Macro HLE", - "SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure." + "SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Manage Saves", + "DeleteUserSave": "Do you want to delete user save for this game?", + "IrreversibleActionNote": "This action is not reversible.", + "SaveManagerHeading": "Manage Saves for {0}", + "SaveManagerTitle": "Save Manager", + "Name": "Name", + "Size": "Size", + "Search": "Search", + "UserProfilesRecoverLostAccounts": "Recover Lost Accounts", + "Recover": "Recover", + "UserProfilesRecoverHeading" : "Saves were found for the following accounts" } diff --git a/Ryujinx.Ava/Assets/Styles/BaseDark.xaml b/Ryujinx.Ava/Assets/Styles/BaseDark.xaml index 074b1661..fbd4d4b3 100644 --- a/Ryujinx.Ava/Assets/Styles/BaseDark.xaml +++ b/Ryujinx.Ava/Assets/Styles/BaseDark.xaml @@ -41,6 +41,9 @@ + #008AA8 + + #FF00C3E3 #FF99b000 #FF006d7d diff --git a/Ryujinx.Ava/Assets/Styles/Styles.xaml b/Ryujinx.Ava/Assets/Styles/Styles.xaml index 8b09bafd..8f7c2e73 100644 --- a/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -1,7 +1,6 @@  @@ -269,13 +268,15 @@ #FF00FABB #FF2D2D2D #FF505050 - 15 - 8 - 10 - 12 - 15 - 13 + 15 + 8 + 10 + 12 + 15 + 13 26 28 + 600 + 756 \ No newline at end of file diff --git a/Ryujinx.Ava/Common/ApplicationHelper.cs b/Ryujinx.Ava/Common/ApplicationHelper.cs index 47f4392e..7f766614 100644 --- a/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Common return; } + OpenSaveDir(saveDataId); + } + + public static void OpenSaveDir(ulong saveDataId) + { string saveRootPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}"); if (!Directory.Exists(saveRootPath)) diff --git a/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs index 9ba631ad..ced88328 100644 --- a/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs +++ b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs @@ -1,6 +1,7 @@ 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; @@ -14,6 +15,8 @@ namespace Ryujinx.Ava.Ui.Controls { public AccountManager AccountManager { get; } public ContentManager ContentManager { get; } + public VirtualFileSystem VirtualFileSystem { get; } + public HorizonClient HorizonClient { get; } public UserProfileViewModel ViewModel { get; set; } public NavigationDialogHost() @@ -22,10 +25,12 @@ namespace Ryujinx.Ava.Ui.Controls } public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, - VirtualFileSystem virtualFileSystem) + VirtualFileSystem virtualFileSystem, HorizonClient horizonClient) { AccountManager = accountManager; ContentManager = contentManager; + VirtualFileSystem = virtualFileSystem; + HorizonClient = horizonClient; ViewModel = new UserProfileViewModel(this); @@ -54,9 +59,10 @@ namespace Ryujinx.Ava.Ui.Controls ContentFrame.Navigate(sourcePageType, parameter); } - public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, VirtualFileSystem ownerVirtualFileSystem) + public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) { - var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem); + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); ContentDialog contentDialog = new ContentDialog { Title = LocaleManager.Instance["UserProfileWindowTitle"], diff --git a/Ryujinx.Ava/Ui/Controls/SaveManager.axaml b/Ryujinx.Ava/Ui/Controls/SaveManager.axaml new file mode 100644 index 00000000..ce337c7b --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/SaveManager.axaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..499cd918 --- /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 _view = new ObservableCollection(); + private string _search; + + public ObservableCollection Saves { get; set; } = new ObservableCollection(); + + public ObservableCollection 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(); + + _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span 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 GetComparer() + { + switch (SortIndex) + { + case 0: + return OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Title) + : SortExpressionComparer.Descending(save => save.Title); + case 1: + return OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Size) + : SortExpressionComparer.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/UserEditor.axaml b/Ryujinx.Ava/Ui/Controls/UserEditor.axaml index 90c5c1fe..898527e6 100644 --- a/Ryujinx.Ava/Ui/Controls/UserEditor.axaml +++ b/Ryujinx.Ava/Ui/Controls/UserEditor.axaml @@ -10,6 +10,7 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels" Margin="0" + MinWidth="500" Padding="0" mc:Ignorable="d"> @@ -63,7 +64,7 @@ HorizontalAlignment="Stretch" MaxLength="{Binding MaxProfileNameLength}" Text="{Binding Name}" /> - + + + + + + + + + + + + + + + + + + + + + + +