diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json
index 4561ad8be3..be3071cc92 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 074b16612e..fbd4d4b37c 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 8b09bafdda..8f7c2e73cb 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 47f4392e8b..7f76661429 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 9ba631ad74..ced8832866 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 0000000000..ce337c7b4c
--- /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 0000000000..499cd918e8
--- /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 90c5c1fec4..898527e6a0 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}" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml.cs b/Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml.cs
new file mode 100644
index 0000000000..f093686dd4
--- /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
index c06bce231f..bc6d8c09ca 100644
--- a/Ryujinx.Ava/Ui/Controls/UserSelector.axaml
+++ b/Ryujinx.Ava/Ui/Controls/UserSelector.axaml
@@ -10,6 +10,7 @@
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
d:DesignHeight="450"
+ MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d">
@@ -25,6 +26,7 @@
-
-
+ HorizontalAlignment="Center">
+
+
+
+
+
+
+
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/SaveModel.cs b/Ryujinx.Ava/Ui/Models/SaveModel.cs
new file mode 100644
index 0000000000..70478cea93
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/SaveModel.cs
@@ -0,0 +1,122 @@
+using LibHac;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
+using LibHac.Ncm;
+using Ryujinx.Ava.Common;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.Ui.App.Common;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+ public class SaveModel : BaseModel
+ {
+ private readonly HorizonClient _horizonClient;
+ private long _size;
+
+ public Action DeleteAction { get; set; }
+ public ulong SaveId { get; }
+ public ProgramId TitleId { get; }
+ public string TitleIdString => $"{TitleId.Value:X16}";
+ public UserId UserId { get; }
+ public bool InGameList { get; }
+ public string Title { get; }
+ public byte[] Icon { get; }
+
+ public long Size
+ {
+ get => _size; set
+ {
+ _size = value;
+ SizeAvailable = true;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(SizeString));
+ OnPropertyChanged(nameof(SizeAvailable));
+ }
+ }
+
+ public bool SizeAvailable { get; set; }
+
+ public string SizeString => $"{((float)_size * 0.000000954):0.###}MB";
+
+ public SaveModel(SaveDataInfo info, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
+ {
+ _horizonClient = horizonClient;
+ SaveId = info.SaveDataId;
+ TitleId = info.ProgramId;
+ UserId = info.UserId;
+
+ var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
+
+ InGameList = appData != null;
+
+ if (InGameList)
+ {
+ Icon = appData.Icon;
+ Title = appData.TitleName;
+ }
+ else
+ {
+ var appMetadata = MainWindow.MainWindowViewModel.ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
+ Title = appMetadata.Title ?? TitleIdString;
+ }
+
+ Task.Run(() =>
+ {
+ var saveRoot = System.IO.Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{info.SaveDataId:x16}");
+
+ long total_size = GetDirectorySize(saveRoot);
+ long GetDirectorySize(string path)
+ {
+ long size = 0;
+ if (Directory.Exists(path))
+ {
+ var directories = Directory.GetDirectories(path);
+ foreach (var directory in directories)
+ {
+ size += GetDirectorySize(directory);
+ }
+
+ var files = Directory.GetFiles(path);
+ foreach (var file in files)
+ {
+ size += new FileInfo(file).Length;
+ }
+ }
+
+ return size;
+ }
+
+ Size = total_size;
+ });
+
+ }
+
+ public void OpenLocation()
+ {
+ ApplicationHelper.OpenSaveDir(SaveId);
+ }
+
+ public async void Delete()
+ {
+ var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DeleteUserSave"],
+ LocaleManager.Instance["IrreversibleActionNote"],
+ LocaleManager.Instance["InputDialogYes"],
+ LocaleManager.Instance["InputDialogNo"], "");
+
+ if (result == UserResult.Yes)
+ {
+ _horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, SaveId);
+
+ DeleteAction?.Invoke();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/TempProfile.cs b/Ryujinx.Ava/Ui/Models/TempProfile.cs
index e943687a16..4e6d344625 100644
--- a/Ryujinx.Ava/Ui/Models/TempProfile.cs
+++ b/Ryujinx.Ava/Ui/Models/TempProfile.cs
@@ -45,9 +45,12 @@ namespace Ryujinx.Ava.Ui.Models
{
_profile = profile;
- Image = profile.Image;
- Name = profile.Name;
- UserId = profile.UserId;
+ if (_profile != null)
+ {
+ Image = profile.Image;
+ Name = profile.Name;
+ UserId = profile.UserId;
+ }
}
public TempProfile(){}
diff --git a/Ryujinx.Ava/Ui/Models/UserProfile.cs b/Ryujinx.Ava/Ui/Models/UserProfile.cs
index 351ada76f4..c0ea9451ac 100644
--- a/Ryujinx.Ava/Ui/Models/UserProfile.cs
+++ b/Ryujinx.Ava/Ui/Models/UserProfile.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
@@ -7,6 +8,7 @@ namespace Ryujinx.Ava.Ui.Models
public class UserProfile : BaseModel
{
private readonly Profile _profile;
+ private readonly NavigationDialogHost _owner;
private byte[] _image;
private string _name;
private UserId _userId;
@@ -41,9 +43,10 @@ namespace Ryujinx.Ava.Ui.Models
}
}
- public UserProfile(Profile profile)
+ public UserProfile(Profile profile, NavigationDialogHost owner)
{
_profile = profile;
+ _owner = owner;
Image = profile.Image;
Name = profile.Name;
@@ -57,5 +60,10 @@ namespace Ryujinx.Ava.Ui.Models
OnPropertyChanged(nameof(IsOpened));
OnPropertyChanged(nameof(Name));
}
+
+ public void Recover(UserProfile userProfile)
+ {
+ _owner.Navigate(typeof(UserEditor), (_owner, userProfile, true));
+ }
}
}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
index cd43701720..c7053eb188 100644
--- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
+++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
@@ -76,6 +76,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
private bool _showAll;
private string _lastScannedAmiiboId;
private ReadOnlyObservableCollection _appsObservableList;
+ public ApplicationLibrary ApplicationLibrary => _owner.ApplicationLibrary;
public string TitleName { get; internal set; }
@@ -103,8 +104,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
public void Initialize()
{
- _owner.ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
- _owner.ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
+ ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
+ ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
Ptc.PtcStateChanged -= ProgressHandler;
Ptc.PtcStateChanged += ProgressHandler;
@@ -817,7 +818,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
Thread thread = new(() =>
{
- _owner.ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
+ ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
_isLoading = false;
})
@@ -1005,7 +1006,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
public async void ManageProfiles()
{
- await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem);
+ await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem, _owner.LibHacHorizonManager.RyujinxClient);
}
public async void OpenAboutWindow()
@@ -1098,7 +1099,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
{
selection.Favorite = !selection.Favorite;
- _owner.ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
+ ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
{
appMetadata.Favorite = selection.Favorite;
});
diff --git a/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
index a48b06e61a..eb9f69d638 100644
--- a/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
+++ b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
@@ -1,8 +1,14 @@
+using Avalonia;
using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
@@ -19,6 +25,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
public UserProfileViewModel()
{
Profiles = new ObservableCollection();
+ LostProfiles = new ObservableCollection();
}
public UserProfileViewModel(NavigationDialogHost owner) : this()
@@ -30,6 +37,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
public ObservableCollection Profiles { get; set; }
+ public ObservableCollection LostProfiles { get; set; }
+
public UserProfile SelectedProfile
{
get => _selectedProfile;
@@ -65,12 +74,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
public void LoadProfiles()
{
Profiles.Clear();
+ LostProfiles.Clear();
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
foreach (var profile in profiles)
{
- Profiles.Add(new UserProfile(profile));
+ Profiles.Add(new UserProfile(profile, _owner));
}
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
@@ -84,6 +94,42 @@ namespace Ryujinx.Ava.Ui.ViewModels
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
}
}
+
+ var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
+ default, saveDataId: default, index: default);
+
+ using var saveDataIterator = new UniqueRef();
+
+ _owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
+
+ Span saveDataInfo = stackalloc SaveDataInfo[10];
+
+ HashSet lostAccounts = new HashSet();
+
+ 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];
+ var id = new HLE.HOS.Services.Account.Acc.UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
+ if (Profiles.FirstOrDefault( x=> x.UserId == id) == null)
+ {
+ lostAccounts.Add(id);
+ }
+ }
+ }
+
+ foreach(var account in lostAccounts)
+ {
+ LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner));
+ }
}
public void AddUser()
@@ -93,6 +139,25 @@ namespace Ryujinx.Ava.Ui.ViewModels
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
}
+ public async void ManageSaves()
+ {
+ UserProfile userProfile = _highlightedProfile ?? SelectedProfile;
+
+ SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem);
+
+ ContentDialog contentDialog = new ContentDialog
+ {
+ Title = string.Format(LocaleManager.Instance["SaveManagerHeading"], userProfile.Name),
+ PrimaryButtonText = "",
+ SecondaryButtonText = "",
+ CloseButtonText = LocaleManager.Instance["UserProfilesClose"],
+ Content = manager,
+ Padding = new Thickness(0)
+ };
+
+ await contentDialog.ShowAsync();
+ }
+
public void EditUser()
{
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
@@ -134,5 +199,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
LoadProfiles();
}
+
+ public void GoBack()
+ {
+ _owner.GoBack();
+ }
+
+ public void RecoverLostAccounts()
+ {
+ _owner.Navigate(typeof(UserRecoverer), (this._owner, this));
+ }
}
}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs
index 8b5a39a7d7..c0a94154c2 100644
--- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs
+++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs
@@ -36,6 +36,7 @@ namespace Ryujinx.Ava.Ui.Windows
{
public partial class MainWindow : StyleableWindow
{
+ internal static MainWindowViewModel MainWindowViewModel { get; private set; }
private bool _canUpdate;
private bool _isClosing;
private bool _isLoading;
@@ -81,6 +82,8 @@ namespace Ryujinx.Ava.Ui.Windows
{
ViewModel = new MainWindowViewModel(this);
+ MainWindowViewModel = ViewModel;
+
DataContext = ViewModel;
InitializeComponent();
diff --git a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 9a6c1997e6..6f25478517 100644
--- a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -444,7 +444,10 @@ namespace Ryujinx.Ui.App.Common
continue;
}
- ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId);
+ ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
+ {
+ appMetadata.Title = titleName;
+ });
if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _))
{
diff --git a/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
index 2631a8cf8e..e19f7483fc 100644
--- a/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
+++ b/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
@@ -2,6 +2,7 @@
{
public class ApplicationMetadata
{
+ public string Title { get; set; }
public bool Favorite { get; set; }
public double TimePlayed { get; set; }
public string LastPlayed { get; set; } = "Never";