From 934b5a64e5638ae5228acb52faf48efadefdea8d Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Wed, 11 Jan 2023 00:20:19 -0500 Subject: Ava GUI: User Profile Manager + Other Fixes (#4166) * Fix redundancies * Add back elses * Loading Screen fixes * Redesign User Profile Manager - Backported long selection bar in Grid/List view not working - Backported UserSelector is jank * Fix SelectionIndicator * Fix DataType * Fix SaveManager bug * Remove debug log * Load saves on UIThread * Reduce UI thread blocking * Fix locale keys * Use block namespaces * Fix close button width * Make UserProfile ordering consistent * Alphabetical order * Adjust layout, remove green circle for blue selector * Fix some inconsistencies * Fix no inital selected profile * Adjust appearance of edit button * Adjust SaveManager * Remove redundant warning dialog * Make firmware avatar selector clearer * View redesign again :hero_depressed: * Consistency adjustments * Adjust margins * Make `UserProfileImageSelector` consistent * Make `UserFirmwareAvatarSelector` consistent * Fix long grid view selector * Switch case * Remove long selection bar Handled in #4178 * Consistency * Started dialog titles * Fixes * Remaining titles * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml Co-authored-by: Mary-nyan * Fix build * Hide UserRecoverer if no LostProfiles are found * UserEditor Avatar Placeholder * Watermark + locale adjustment * Border radius * Remove unnecessary styles * Fix firmware avatar image order * Cleanup `ColorPickerButton` * Make `UserId` copy/paste able * Make `FirmwareAvatarSelector` 6 images wide * Make selection bar better * Unsaved changes dialogue * Fix indentation * Remove extra check * Address suggestions * Reorganise - Remove unused views - Rename views to match convention - Fix weird namespacing * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml Co-authored-by: Ac_K * UserRecovererView empty placeholder * Update Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Models/UserProfile.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K * Remove AddModel * Update Ryujinx.Ava/Assets/Locales/en_US.json Co-authored-by: Ac_K * Fix bug Co-authored-by: Mary-nyan Co-authored-by: Ac_K --- Ryujinx.Ava/Assets/Locales/de_DE.json | 2 +- Ryujinx.Ava/Assets/Locales/el_GR.json | 2 +- Ryujinx.Ava/Assets/Locales/en_US.json | 18 +- Ryujinx.Ava/Assets/Locales/es_ES.json | 2 +- Ryujinx.Ava/Assets/Locales/fr_FR.json | 2 +- Ryujinx.Ava/Assets/Locales/ja_JP.json | 2 +- Ryujinx.Ava/Assets/Locales/pl_PL.json | 2 +- Ryujinx.Ava/Assets/Locales/pt_BR.json | 2 +- Ryujinx.Ava/Assets/Locales/ru_RU.json | 2 +- Ryujinx.Ava/Assets/Locales/tr_TR.json | 2 +- Ryujinx.Ava/Assets/Locales/zh_TW.json | 2 +- Ryujinx.Ava/Assets/Styles/Styles.xaml | 32 +++ Ryujinx.Ava/Helper/LoggerAdapter.cs | 111 ---------- Ryujinx.Ava/Helper/MetalHelper.cs | 127 ------------ Ryujinx.Ava/Program.cs | 2 +- Ryujinx.Ava/Ryujinx.Ava.csproj | 12 ++ Ryujinx.Ava/UI/Controls/GameGridView.axaml | 24 --- Ryujinx.Ava/UI/Controls/GameListView.axaml | 29 --- Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml | 3 +- .../UI/Controls/NavigationDialogHost.axaml.cs | 139 ++++++++++++- .../UI/Controls/ProfileImageSelectionDialog.axaml | 57 ----- .../Controls/ProfileImageSelectionDialog.axaml.cs | 105 ---------- Ryujinx.Ava/UI/Controls/SaveManager.axaml | 175 ---------------- Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs | 160 -------------- Ryujinx.Ava/UI/Controls/UserEditor.axaml | 86 -------- Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs | 118 ----------- Ryujinx.Ava/UI/Controls/UserRecoverer.axaml | 72 ------- Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs | 44 ---- Ryujinx.Ava/UI/Controls/UserSelector.axaml | 145 ------------- Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs | 77 ------- Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs | 5 +- Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs | 111 ++++++++++ Ryujinx.Ava/UI/Helpers/MetalHelper.cs | 127 ++++++++++++ Ryujinx.Ava/UI/Models/ProfileImageModel.cs | 20 +- Ryujinx.Ava/UI/Models/SaveModel.cs | 24 --- Ryujinx.Ava/UI/Models/TempProfile.cs | 9 +- Ryujinx.Ava/UI/Models/UserProfile.cs | 42 +++- .../UserFirmwareAvatarSelectorViewModel.cs | 230 +++++++++++++++++++++ .../UserProfileImageSelectorViewModel.cs | 18 ++ Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs | 200 +----------------- .../UI/ViewModels/UserSaveManagerViewModel.cs | 123 +++++++++++ Ryujinx.Ava/UI/Views/User/UserEditorView.axaml | 123 +++++++++++ Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs | 165 +++++++++++++++ .../User/UserFirmwareAvatarSelectorView.axaml | 114 ++++++++++ .../User/UserFirmwareAvatarSelectorView.axaml.cs | 88 ++++++++ .../Views/User/UserProfileImageSelectorView.axaml | 63 ++++++ .../User/UserProfileImageSelectorView.axaml.cs | 124 +++++++++++ Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml | 83 ++++++++ .../UI/Views/User/UserRecovererView.axaml.cs | 51 +++++ .../UI/Views/User/UserSaveManagerView.axaml | 199 ++++++++++++++++++ .../UI/Views/User/UserSaveManagerView.axaml.cs | 148 +++++++++++++ Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml | 165 +++++++++++++++ .../UI/Views/User/UserSelectorView.axaml.cs | 128 ++++++++++++ Ryujinx.Ava/UI/Windows/AvatarWindow.axaml | 54 ----- Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs | 77 ------- 55 files changed, 2332 insertions(+), 1715 deletions(-) delete mode 100644 Ryujinx.Ava/Helper/LoggerAdapter.cs delete mode 100644 Ryujinx.Ava/Helper/MetalHelper.cs delete mode 100644 Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml delete mode 100644 Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs delete mode 100644 Ryujinx.Ava/UI/Controls/SaveManager.axaml delete mode 100644 Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs delete mode 100644 Ryujinx.Ava/UI/Controls/UserEditor.axaml delete mode 100644 Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs delete mode 100644 Ryujinx.Ava/UI/Controls/UserRecoverer.axaml delete mode 100644 Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs delete mode 100644 Ryujinx.Ava/UI/Controls/UserSelector.axaml delete mode 100644 Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs create mode 100644 Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs create mode 100644 Ryujinx.Ava/UI/Helpers/MetalHelper.cs create mode 100644 Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs create mode 100644 Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs create mode 100644 Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserEditorView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs create mode 100644 Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml create mode 100644 Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs delete mode 100644 Ryujinx.Ava/UI/Windows/AvatarWindow.axaml delete mode 100644 Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json index 671f369e..4d656bc9 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 5cd7a554..ca3be8b9 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 46203463..0c767871 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 1922318d..660d62a1 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 938d0cc7..71f32c6e 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 c88477f9..b1e0a43b 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 3c1b541e..0cc0b4f9 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 036b0a4b..ded6cf95 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 b3ad82be..7b25f455 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 ae14cdaf..f277713b 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 963c0a83..e6832995 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 c5e760e8..fc4e9ddd 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/Helper/LoggerAdapter.cs b/Ryujinx.Ava/Helper/LoggerAdapter.cs deleted file mode 100644 index c8f3fea1..00000000 --- a/Ryujinx.Ava/Helper/LoggerAdapter.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Avalonia.Utilities; -using System; -using System.Text; - -namespace Ryujinx.Ava.UI.Helper -{ - using AvaLogger = Avalonia.Logging.Logger; - using AvaLogLevel = Avalonia.Logging.LogEventLevel; - using RyuLogger = Ryujinx.Common.Logging.Logger; - using RyuLogClass = Ryujinx.Common.Logging.LogClass; - - internal class LoggerAdapter : Avalonia.Logging.ILogSink - { - public static void Register() - { - AvaLogger.Sink = new LoggerAdapter(); - } - - private static RyuLogger.Log? GetLog(AvaLogLevel level) - { - return level switch - { - AvaLogLevel.Verbose => RyuLogger.Trace, - AvaLogLevel.Debug => RyuLogger.Debug, - AvaLogLevel.Information => RyuLogger.Info, - AvaLogLevel.Warning => RyuLogger.Warning, - AvaLogLevel.Error => RyuLogger.Error, - AvaLogLevel.Fatal => RyuLogger.Notice, - _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) - }; - } - - public bool IsEnabled(AvaLogLevel level, string area) - { - return GetLog(level) != null; - } - - public void Log(AvaLogLevel level, string area, object source, string messageTemplate) - { - GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, null)); - } - - public void Log(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0) - { - GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0 })); - } - - public void Log(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 })); - } - - public void Log(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 })); - } - - public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues) - { - GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, propertyValues)); - } - - private static string Format(string area, string template, object source, object[] v) - { - var result = new StringBuilder(); - var r = new CharacterReader(template.AsSpan()); - var i = 0; - - result.Append('['); - result.Append(area); - result.Append("] "); - - while (!r.End) - { - var c = r.Take(); - - if (c != '{') - { - result.Append(c); - } - else - { - if (r.Peek != '{') - { - result.Append('\''); - result.Append(i < v.Length ? v[i++] : null); - result.Append('\''); - r.TakeUntil('}'); - r.Take(); - } - else - { - result.Append('{'); - r.Take(); - } - } - } - - if (source != null) - { - result.Append(" ("); - result.Append(source.GetType().Name); - result.Append(" #"); - result.Append(source.GetHashCode()); - result.Append(')'); - } - - return result.ToString(); - } - } -} \ No newline at end of file diff --git a/Ryujinx.Ava/Helper/MetalHelper.cs b/Ryujinx.Ava/Helper/MetalHelper.cs deleted file mode 100644 index ea3477eb..00000000 --- a/Ryujinx.Ava/Helper/MetalHelper.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Runtime.Versioning; -using System.Runtime.InteropServices; -using Avalonia; - -namespace Ryujinx.Ava.UI.Helper -{ - public delegate void UpdateBoundsCallbackDelegate(Rect rect); - - [SupportedOSPlatform("macos")] - static partial class MetalHelper - { - private const string LibObjCImport = "/usr/lib/libobjc.A.dylib"; - - private struct Selector - { - public readonly IntPtr NativePtr; - - public unsafe Selector(string value) - { - int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); - byte* data = stackalloc byte[size]; - - fixed (char* pValue = value) - { - System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); - } - - NativePtr = sel_registerName(data); - } - - public static implicit operator Selector(string value) => new Selector(value); - } - - private static unsafe IntPtr GetClass(string value) - { - int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); - byte* data = stackalloc byte[size]; - - fixed (char* pValue = value) - { - System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); - } - - return objc_getClass(data); - } - - private struct NSPoint - { - public double X; - public double Y; - - public NSPoint(double x, double y) - { - X = x; - Y = y; - } - } - - private struct NSRect - { - public NSPoint Pos; - public NSPoint Size; - - public NSRect(double x, double y, double width, double height) - { - Pos = new NSPoint(x, y); - Size = new NSPoint(width, height); - } - } - - public static IntPtr GetMetalLayer(out IntPtr nsView, out UpdateBoundsCallbackDelegate updateBounds) - { - // Create a new CAMetalLayer. - IntPtr layerClass = GetClass("CAMetalLayer"); - IntPtr metalLayer = IntPtr_objc_msgSend(layerClass, "alloc"); - objc_msgSend(metalLayer, "init"); - - // Create a child NSView to render into. - IntPtr nsViewClass = GetClass("NSView"); - IntPtr child = IntPtr_objc_msgSend(nsViewClass, "alloc"); - objc_msgSend(child, "init", new NSRect(0, 0, 0, 0)); - - // Make its renderer our metal layer. - objc_msgSend(child, "setWantsLayer:", (byte)1); - objc_msgSend(child, "setLayer:", metalLayer); - objc_msgSend(metalLayer, "setContentsScale:", Program.DesktopScaleFactor); - - // Ensure the scale factor is up to date. - updateBounds = (Rect rect) => { - objc_msgSend(metalLayer, "setContentsScale:", Program.DesktopScaleFactor); - }; - - nsView = child; - return metalLayer; - } - - public static void DestroyMetalLayer(IntPtr nsView, IntPtr metalLayer) - { - // TODO - } - - [LibraryImport(LibObjCImport)] - private static unsafe partial IntPtr sel_registerName(byte* data); - - [LibraryImport(LibObjCImport)] - private static unsafe partial IntPtr objc_getClass(byte* data); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, NSRect point); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, double value); - - [LibraryImport(LibObjCImport, EntryPoint = "objc_msgSend")] - private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); - } -} \ No newline at end of file diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 010aff51..46e135a9 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 996817b9..88b60d0b 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 c757f066..862bc6d3 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 90720478..bf34b303 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 0c300267..6911a4d4 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 56f8152a..00000000 --- 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/ProfileImageSelectionDialog.axaml.cs b/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs deleted file mode 100644 index 46a2f507..00000000 --- a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.VisualTree; -using FluentAvalonia.UI.Controls; -using FluentAvalonia.UI.Navigation; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Windows; -using Ryujinx.HLE.FileSystem; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; -using System.IO; -using Image = SixLabors.ImageSharp.Image; - -namespace Ryujinx.Ava.UI.Controls -{ - public partial class ProfileImageSelectionDialog : UserControl - { - private ContentManager _contentManager; - private NavigationDialogHost _parent; - private TempProfile _profile; - - public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null; - - public ProfileImageSelectionDialog() - { - InitializeComponent(); - AddHandler(Frame.NavigatedToEvent, (s, e) => - { - NavigatedTo(e); - }, RoutingStrategies.Direct); - } - - private void NavigatedTo(NavigationEventArgs arg) - { - if (Program.PreviewerDetached) - { - switch (arg.NavigationMode) - { - case NavigationMode.New: - (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; - _contentManager = _parent.ContentManager; - break; - case NavigationMode.Back: - _parent.GoBack(); - break; - } - - DataContext = this; - } - } - - private async void Import_OnClick(object sender, RoutedEventArgs e) - { - OpenFileDialog dialog = new(); - dialog.Filters.Add(new FileDialogFilter - { - Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], - Extensions = { "jpg", "jpeg", "png", "bmp" } - }); - dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } }); - - dialog.AllowMultiple = false; - - string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); - - if (image != null) - { - if (image.Length > 0) - { - string imageFile = image[0]; - - _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); - } - - _parent.GoBack(); - } - } - - private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) - { - if (FirmwareFound) - { - _parent.Navigate(typeof(AvatarWindow), (_parent, _profile)); - } - } - - private static byte[] ProcessProfileImage(byte[] buffer) - { - using (Image image = Image.Load(buffer)) - { - image.Mutate(x => x.Resize(256, 256)); - - using (MemoryStream streamJpg = new()) - { - image.SaveAsJpeg(streamJpg); - - return streamJpg.ToArray(); - } - } - } - } -} \ 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 64674b65..00000000 --- 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 9910481c..00000000 --- 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/UserEditor.axaml b/Ryujinx.Ava/UI/Controls/UserEditor.axaml deleted file mode 100644 index 155f1cfe..00000000 --- a/Ryujinx.Ava/UI/Controls/UserEditor.axaml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs new file mode 100644 index 00000000..18f76f80 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -0,0 +1,124 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +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.ViewModels; +using Ryujinx.HLE.FileSystem; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System.IO; +using Image = SixLabors.ImageSharp.Image; + +namespace Ryujinx.Ava.UI.Views.User +{ + public partial class UserProfileImageSelectorView : UserControl + { + private ContentManager _contentManager; + private NavigationDialogHost _parent; + private TempProfile _profile; + + internal UserProfileImageSelectorViewModel ViewModel { get; private set; } + + public UserProfileImageSelectorView() + { + InitializeComponent(); + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + 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: + if (_profile.Image != null) + { + _parent.GoBack(); + } + break; + } + } + } + + private async void Import_OnClick(object sender, RoutedEventArgs e) + { + OpenFileDialog dialog = new(); + dialog.Filters.Add(new FileDialogFilter + { + Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], + Extensions = { "jpg", "jpeg", "png", "bmp" } + }); + dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } }); + + dialog.AllowMultiple = false; + + string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); + + if (image != null) + { + if (image.Length > 0) + { + string imageFile = image[0]; + + _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); + + if (_profile.Image != null) + { + _parent.GoBack(); + } + } + } + } + + private void GoBack(object sender, RoutedEventArgs e) + { + _parent.GoBack(); + } + + private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) + { + if (ViewModel.FirmwareFound) + { + _parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile)); + } + } + + private static byte[] ProcessProfileImage(byte[] buffer) + { + using (Image image = Image.Load(buffer)) + { + image.Mutate(x => x.Resize(256, 256)); + + using (MemoryStream streamJpg = new()) + { + image.SaveAsJpeg(streamJpg); + + return streamJpg.ToArray(); + } + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml new file mode 100644 index 00000000..62b5e184 --- /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 00000000..0c53e53d --- /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 00000000..cdf74d52 --- /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 00000000..9d955326 --- /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 00000000..9a6ba054 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +