diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json index 671f369e7..4d656bc99 100644 --- a/Ryujinx.Ava/Assets/Locales/de_DE.json +++ b/Ryujinx.Ava/Assets/Locales/de_DE.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Profilbild ändern", "UserProfilesAvailableUserProfiles": "Verfügbare Profile:", "UserProfilesAddNewProfile": "Neues Profil", - "UserProfilesDeleteSelectedProfile": "Profil löschen", + "UserProfilesDelete": "Löschen", "UserProfilesClose": "Schließen", "ProfileImageSelectionTitle": "Auswahl des Profilbildes", "ProfileImageSelectionHeader": "Wähle ein Profilbild aus", diff --git a/Ryujinx.Ava/Assets/Locales/el_GR.json b/Ryujinx.Ava/Assets/Locales/el_GR.json index 5cd7a5540..ca3be8b9a 100644 --- a/Ryujinx.Ava/Assets/Locales/el_GR.json +++ b/Ryujinx.Ava/Assets/Locales/el_GR.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ", "UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:", "UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ", - "UserProfilesDeleteSelectedProfile": "Διαγραφή Επιλεγμένου Προφίλ", + "UserProfilesDelete": "Διαγράφω", "UserProfilesClose": "Κλείσιμο", "ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ", "ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ", diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index 46203463a..0c767871b 100644 --- a/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/Ryujinx.Ava/Assets/Locales/en_US.json @@ -260,8 +260,9 @@ "UserProfilesChangeProfileImage": "Change Profile Image", "UserProfilesAvailableUserProfiles": "Available User Profiles:", "UserProfilesAddNewProfile": "Create Profile", - "UserProfilesDeleteSelectedProfile": "Delete Selected", + "UserProfilesDelete": "Delete", "UserProfilesClose": "Close", + "ProfileNameSelectionWatermark": "Choose a nickname", "ProfileImageSelectionTitle": "Profile Image Selection", "ProfileImageSelectionHeader": "Choose a profile Image", "ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware", @@ -273,7 +274,7 @@ "InputDialogAddNewProfileTitle": "Choose the Profile Name", "InputDialogAddNewProfileHeader": "Please Enter a Profile Name", "InputDialogAddNewProfileSubtext": "(Max Length: {0})", - "AvatarChoose": "Choose", + "AvatarChoose": "Choose Avatar", "AvatarSetBackgroundColor": "Set Background Color", "AvatarClose": "Close", "ControllerSettingsLoadProfileToolTip": "Load Profile", @@ -368,6 +369,9 @@ "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.", "DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted", "DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile", + "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes", + "DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.", + "DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?", "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", "DialogDlcLoadNcaErrorMessage": "{0}. Errored File: {1}", @@ -584,7 +588,7 @@ "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", "UserProfilesName": "Name:", - "UserProfilesUserId": "User Id:", + "UserProfilesUserId": "User ID:", "SettingsTabGraphicsBackend": "Graphics Backend", "SettingsTabGraphicsBackendTooltip": "Graphics Backend to use", "SettingsEnableTextureRecompression": "Enable Texture Recompression", @@ -603,13 +607,15 @@ "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}", + "SaveManagerHeading": "Manage Saves for {0} ({1})", "SaveManagerTitle": "Save Manager", "Name": "Name", "Size": "Size", "Search": "Search", "UserProfilesRecoverLostAccounts": "Recover Lost Accounts", "Recover": "Recover", - "UserProfilesRecoverHeading" : "Saves were found for the following accounts" + "UserProfilesRecoverHeading" : "Saves were found for the following accounts", + "UserProfilesRecoverEmptyList": "No profiles to recover", + "UserEditorTitle" : "Edit User", + "UserEditorTitleCreate" : "Create User" } - diff --git a/Ryujinx.Ava/Assets/Locales/es_ES.json b/Ryujinx.Ava/Assets/Locales/es_ES.json index 1922318d0..660d62a1e 100644 --- a/Ryujinx.Ava/Assets/Locales/es_ES.json +++ b/Ryujinx.Ava/Assets/Locales/es_ES.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Cambiar imagen de perfil", "UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:", "UserProfilesAddNewProfile": "Añadir nuevo perfil", - "UserProfilesDeleteSelectedProfile": "Eliminar perfil seleccionado", + "UserProfilesDelete": "Eliminar", "UserProfilesClose": "Cerrar", "ProfileImageSelectionTitle": "Selección de imagen de perfil", "ProfileImageSelectionHeader": "Elige una imagen de perfil", diff --git a/Ryujinx.Ava/Assets/Locales/fr_FR.json b/Ryujinx.Ava/Assets/Locales/fr_FR.json index 938d0cc77..71f32c6ee 100644 --- a/Ryujinx.Ava/Assets/Locales/fr_FR.json +++ b/Ryujinx.Ava/Assets/Locales/fr_FR.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Changer l'image du profil", "UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:", "UserProfilesAddNewProfile": "Ajouter un nouveau profil", - "UserProfilesDeleteSelectedProfile": "Supprimer le profil sélectionné", + "UserProfilesDelete": "Supprimer", "UserProfilesClose": "Fermer", "ProfileImageSelectionTitle": "Sélection de l'image du profil", "ProfileImageSelectionHeader": "Choisir l'image du profil", diff --git a/Ryujinx.Ava/Assets/Locales/ja_JP.json b/Ryujinx.Ava/Assets/Locales/ja_JP.json index c88477f96..b1e0a43bf 100644 --- a/Ryujinx.Ava/Assets/Locales/ja_JP.json +++ b/Ryujinx.Ava/Assets/Locales/ja_JP.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "プロファイル画像を変更", "UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:", "UserProfilesAddNewProfile": "プロファイルを作成", - "UserProfilesDeleteSelectedProfile": "削除", + "UserProfilesDelete": "削除", "UserProfilesClose": "閉じる", "ProfileImageSelectionTitle": "プロファイル画像選択", "ProfileImageSelectionHeader": "プロファイル画像を選択", diff --git a/Ryujinx.Ava/Assets/Locales/pl_PL.json b/Ryujinx.Ava/Assets/Locales/pl_PL.json index 3c1b541ed..0cc0b4f91 100644 --- a/Ryujinx.Ava/Assets/Locales/pl_PL.json +++ b/Ryujinx.Ava/Assets/Locales/pl_PL.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Zmień Obraz Profilu", "UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:", "UserProfilesAddNewProfile": "Utwórz Profil", - "UserProfilesDeleteSelectedProfile": "Usuń Zaznaczone", + "UserProfilesDelete": "Usuwać", "UserProfilesClose": "Zamknij", "ProfileImageSelectionTitle": "Wybór Obrazu Profilu", "ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe", diff --git a/Ryujinx.Ava/Assets/Locales/pt_BR.json b/Ryujinx.Ava/Assets/Locales/pt_BR.json index 036b0a4bf..ded6cf95f 100644 --- a/Ryujinx.Ava/Assets/Locales/pt_BR.json +++ b/Ryujinx.Ava/Assets/Locales/pt_BR.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Mudar imagem de perfil", "UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:", "UserProfilesAddNewProfile": "Adicionar novo perfil", - "UserProfilesDeleteSelectedProfile": "Apagar perfil selecionado", + "UserProfilesDelete": "Apagar", "UserProfilesClose": "Fechar", "ProfileImageSelectionTitle": "Seleção da imagem de perfil", "ProfileImageSelectionHeader": "Escolha uma imagem de perfil", diff --git a/Ryujinx.Ava/Assets/Locales/ru_RU.json b/Ryujinx.Ava/Assets/Locales/ru_RU.json index b3ad82be7..7b25f4554 100644 --- a/Ryujinx.Ava/Assets/Locales/ru_RU.json +++ b/Ryujinx.Ava/Assets/Locales/ru_RU.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Изменить изображение профиля", "UserProfilesAvailableUserProfiles": "Доступные профили пользователей:", "UserProfilesAddNewProfile": "Добавить новый профиль", - "UserProfilesDeleteSelectedProfile": "Удалить выбранный профиль", + "UserProfilesDelete": "Удалить", "UserProfilesClose": "Закрыть", "ProfileImageSelectionTitle": "Выбор изображения профиля", "ProfileImageSelectionHeader": "Выберите изображение профиля", diff --git a/Ryujinx.Ava/Assets/Locales/tr_TR.json b/Ryujinx.Ava/Assets/Locales/tr_TR.json index ae14cdaf3..f277713ba 100644 --- a/Ryujinx.Ava/Assets/Locales/tr_TR.json +++ b/Ryujinx.Ava/Assets/Locales/tr_TR.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "Profil Resmini Değiştir", "UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:", "UserProfilesAddNewProfile": "Yeni Profil Ekle", - "UserProfilesDeleteSelectedProfile": "Seçili Profili Sil", + "UserProfilesDelete": "Sil", "UserProfilesClose": "Kapat", "ProfileImageSelectionTitle": "Profil Resmi Seçimi", "ProfileImageSelectionHeader": "Profil Resmi Seç", diff --git a/Ryujinx.Ava/Assets/Locales/zh_TW.json b/Ryujinx.Ava/Assets/Locales/zh_TW.json index 963c0a834..e68329957 100644 --- a/Ryujinx.Ava/Assets/Locales/zh_TW.json +++ b/Ryujinx.Ava/Assets/Locales/zh_TW.json @@ -260,7 +260,7 @@ "UserProfilesChangeProfileImage": "更換頭貼", "UserProfilesAvailableUserProfiles": "現有的帳號:", "UserProfilesAddNewProfile": "建立帳號", - "UserProfilesDeleteSelectedProfile": "刪除選擇的帳號", + "UserProfilesDelete": "刪除", "UserProfilesClose": "關閉", "ProfileImageSelectionTitle": "頭貼選擇", "ProfileImageSelectionHeader": "選擇合適的頭貼圖片", diff --git a/Ryujinx.Ava/Assets/Styles/Styles.xaml b/Ryujinx.Ava/Assets/Styles/Styles.xaml index c5e760e81..fc4e9ddd6 100644 --- a/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -179,6 +179,9 @@ + @@ -234,6 +237,35 @@ + + + diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 010aff514..46e135a9b 100644 --- a/Ryujinx.Ava/Program.cs +++ b/Ryujinx.Ava/Program.cs @@ -1,6 +1,6 @@ using Avalonia; using Avalonia.Threading; -using Ryujinx.Ava.UI.Helper; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; diff --git a/Ryujinx.Ava/Ryujinx.Ava.csproj b/Ryujinx.Ava/Ryujinx.Ava.csproj index 996817b9d..88b60d0ba 100644 --- a/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -130,6 +130,18 @@ GameListView.axaml Code + + UserEditor.axaml + Code + + + UserRecoverer.axaml + Code + + + UserSelector.axaml + Code + diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml b/Ryujinx.Ava/UI/Controls/GameGridView.axaml index c757f066c..862bc6d30 100644 --- a/Ryujinx.Ava/UI/Controls/GameGridView.axaml +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml @@ -112,32 +112,8 @@ - - - - diff --git a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml index 90720478d..bf34b303a 100644 --- a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml +++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml @@ -12,5 +12,6 @@ + x:Name="ContentFrame"> + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs index 0c3002675..6911a4d4c 100644 --- a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs +++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs @@ -1,13 +1,25 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Threading; +using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; using System; using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; namespace Ryujinx.Ava.UI.Controls { @@ -31,14 +43,14 @@ namespace Ryujinx.Ava.UI.Controls ContentManager = contentManager; VirtualFileSystem = virtualFileSystem; HorizonClient = horizonClient; - ViewModel = new UserProfileViewModel(this); - + ViewModel = new UserProfileViewModel(); + LoadProfiles(); if (contentManager.GetCurrentFirmwareVersion() != null) { Task.Run(() => { - AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem); + UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem); }); } InitializeComponent(); @@ -51,7 +63,7 @@ namespace Ryujinx.Ava.UI.Controls ContentFrame.GoBack(); } - ViewModel.LoadProfiles(); + LoadProfiles(); } public void Navigate(Type sourcePageType, object parameter) @@ -68,7 +80,7 @@ namespace Ryujinx.Ava.UI.Controls Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], PrimaryButtonText = "", SecondaryButtonText = "", - CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], + CloseButtonText = "", Content = content, Padding = new Thickness(0) }; @@ -78,6 +90,11 @@ namespace Ryujinx.Ava.UI.Controls content.ViewModel.Dispose(); }; + Style footer = new(x => x.Name("DialogSpace").Child().OfType()); + footer.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(footer); + await contentDialog.ShowAsync(); } @@ -85,7 +102,117 @@ namespace Ryujinx.Ava.UI.Controls { base.OnAttachedToVisualTree(e); - Navigate(typeof(UserSelector), this); + Navigate(typeof(UserSelectorViews), this); + } + + public void LoadProfiles() + { + ViewModel.Profiles.Clear(); + ViewModel.LostProfiles.Clear(); + + var profiles = AccountManager.GetAllUsers().OrderBy(x => x.Name); + + foreach (var profile in profiles) + { + ViewModel.Profiles.Add(new UserProfile(profile, this)); + } + + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, 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]; + + HashSet lostAccounts = new(); + + 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 (ViewModel.Profiles.Cast().FirstOrDefault( x=> x.UserId == id) == null) + { + lostAccounts.Add(id); + } + } + } + + foreach(var account in lostAccounts) + { + ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), this)); + } + + ViewModel.Profiles.Add(new BaseModel()); + } + + public async void DeleteUser(UserProfile userProfile) + { + var lastUserId = AccountManager.LastOpenedUser.UserId; + + if (userProfile.UserId == lastUserId) + { + // If we are deleting the currently open profile, then we must open something else before deleting. + var profile = ViewModel.Profiles.Cast().FirstOrDefault(x => x.UserId != lastUserId); + + if (profile == null) + { + async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + AccountManager.OpenUser(profile.UserId); + } + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], + "", + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + ""); + + if (result == UserResult.Yes) + { + GoBack(); + AccountManager.DeleteUser(userProfile.UserId); + } + + LoadProfiles(); + } + + public void AddUser() + { + Navigate(typeof(UserEditorView), (this, (UserProfile)null, true)); + } + + public void EditUser(UserProfile userProfile) + { + Navigate(typeof(UserEditorView), (this, userProfile, false)); + } + + public void RecoverLostAccounts() + { + Navigate(typeof(UserRecovererView), this); + } + + public void ManageSaves() + { + Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem)); } } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml deleted file mode 100644 index 56f8152ae..000000000 --- a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/SaveManager.axaml b/Ryujinx.Ava/UI/Controls/SaveManager.axaml deleted file mode 100644 index 64674b65b..000000000 --- a/Ryujinx.Ava/UI/Controls/SaveManager.axaml +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs b/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs deleted file mode 100644 index 9910481c5..000000000 --- a/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs +++ /dev/null @@ -1,160 +0,0 @@ -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/UserRecoverer.axaml b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml deleted file mode 100644 index 69f3d36a2..000000000 --- a/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs similarity index 69% rename from Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs rename to Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs index 46a2f5079..18f76f805 100644 --- a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs +++ b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -4,25 +4,26 @@ 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.Windows; +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.Controls +namespace Ryujinx.Ava.UI.Views.User { - public partial class ProfileImageSelectionDialog : UserControl + public partial class UserProfileImageSelectorView : UserControl { private ContentManager _contentManager; private NavigationDialogHost _parent; private TempProfile _profile; - public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null; + internal UserProfileImageSelectorViewModel ViewModel { get; private set; } - public ProfileImageSelectionDialog() + public UserProfileImageSelectorView() { InitializeComponent(); AddHandler(Frame.NavigatedToEvent, (s, e) => @@ -40,13 +41,23 @@ namespace Ryujinx.Ava.UI.Controls 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: - _parent.GoBack(); + if (_profile.Image != null) + { + _parent.GoBack(); + } break; } - - DataContext = this; } } @@ -73,17 +84,25 @@ namespace Ryujinx.Ava.UI.Controls string imageFile = image[0]; _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); - } - _parent.GoBack(); + if (_profile.Image != null) + { + _parent.GoBack(); + } + } } } + private void GoBack(object sender, RoutedEventArgs e) + { + _parent.GoBack(); + } + private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) { - if (FirmwareFound) + if (ViewModel.FirmwareFound) { - _parent.Navigate(typeof(AvatarWindow), (_parent, _profile)); + _parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile)); } } diff --git a/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml new file mode 100644 index 000000000..62b5e1840 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 000000000..0c53e53d7 --- /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 000000000..cdf74d52f --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 000000000..9d955326f --- /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(); + 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(); + + _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); + } + } + } + + 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 000000000..9a6ba054e --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +