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: [PATCH] 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 <thog@protonmail.com> * 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 <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml Co-authored-by: Ac_K <Acoustik666@gmail.com> * UserRecovererView empty placeholder * Update Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Models/UserProfile.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Remove AddModel * Update Ryujinx.Ava/Assets/Locales/en_US.json Co-authored-by: Ac_K <Acoustik666@gmail.com> * Fix bug Co-authored-by: Mary-nyan <thog@protonmail.com> Co-authored-by: Ac_K <Acoustik666@gmail.com> --- 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/Program.cs | 2 +- Ryujinx.Ava/Ryujinx.Ava.csproj | 12 + Ryujinx.Ava/UI/Controls/GameGridView.axaml | 24 -- Ryujinx.Ava/UI/Controls/GameListView.axaml | 29 --- .../UI/Controls/NavigationDialogHost.axaml | 3 +- .../UI/Controls/NavigationDialogHost.axaml.cs | 139 ++++++++++- .../ProfileImageSelectionDialog.axaml | 57 ----- Ryujinx.Ava/UI/Controls/SaveManager.axaml | 175 ------------- Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs | 160 ------------ Ryujinx.Ava/UI/Controls/UserRecoverer.axaml | 72 ------ .../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 +- .../{Helper => UI/Helpers}/LoggerAdapter.cs | 2 +- .../{Helper => UI/Helpers}/MetalHelper.cs | 2 +- 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 ++ .../UI/ViewModels/UserProfileViewModel.cs | 200 +-------------- .../UI/ViewModels/UserSaveManagerViewModel.cs | 123 ++++++++++ .../User/UserEditorView.axaml} | 109 ++++++--- .../User/UserEditorView.axaml.cs} | 81 ++++-- .../User/UserFirmwareAvatarSelectorView.axaml | 114 +++++++++ .../UserFirmwareAvatarSelectorView.axaml.cs} | 35 ++- .../User/UserProfileImageSelectorView.axaml | 63 +++++ .../UserProfileImageSelectorView.axaml.cs} | 43 +++- .../UI/Views/User/UserRecovererView.axaml | 83 +++++++ .../UI/Views/User/UserRecovererView.axaml.cs | 51 ++++ .../UI/Views/User/UserSaveManagerView.axaml | 199 +++++++++++++++ .../Views/User/UserSaveManagerView.axaml.cs | 148 +++++++++++ .../UI/Views/User/UserSelectorView.axaml | 165 +++++++++++++ .../UI/Views/User/UserSelectorView.axaml.cs | 128 ++++++++++ Ryujinx.Ava/UI/Windows/AvatarWindow.axaml | 54 ---- 49 files changed, 1787 insertions(+), 1170 deletions(-) delete mode 100644 Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml 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/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 rename Ryujinx.Ava/{Helper => UI/Helpers}/LoggerAdapter.cs (99%) rename Ryujinx.Ava/{Helper => UI/Helpers}/MetalHelper.cs (99%) 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 rename Ryujinx.Ava/UI/{Controls/UserEditor.axaml => Views/User/UserEditorView.axaml} (52%) rename Ryujinx.Ava/UI/{Controls/UserEditor.axaml.cs => Views/User/UserEditorView.axaml.cs} (52%) create mode 100644 Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml rename Ryujinx.Ava/UI/{Windows/AvatarWindow.axaml.cs => Views/User/UserFirmwareAvatarSelectorView.axaml.cs} (55%) create mode 100644 Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml rename Ryujinx.Ava/UI/{Controls/ProfileImageSelectionDialog.axaml.cs => Views/User/UserProfileImageSelectorView.axaml.cs} (69%) 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 diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json index 671f369e78..4d656bc99e 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 5cd7a55401..ca3be8b9ad 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 46203463ac..0c767871ba 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 1922318d0a..660d62a1eb 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 938d0cc77f..71f32c6ee6 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 c88477f968..b1e0a43bf5 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 3c1b541edb..0cc0b4f917 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 036b0a4bf0..ded6cf95f3 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 b3ad82be73..7b25f45541 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 ae14cdaf32..f277713ba1 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 963c0a8346..e683299575 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 c5e760e81d..fc4e9ddd63 100644 --- a/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -179,6 +179,9 @@ <Style Selector="Button"> <Setter Property="MinWidth" Value="80" /> </Style> + <Style Selector="ProgressBar /template/ Border#ProgressBarTrack"> + <Setter Property="IsVisible" Value="False" /> + </Style> <Style Selector="ToggleButton"> <Setter Property="Padding" Value="0,-5,0,0" /> </Style> @@ -234,6 +237,35 @@ <Style Selector="TextBox.NumberBoxTextBoxStyle"> <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" /> </Style> + <Style Selector="ListBox ListBoxItem"> + <Setter Property="Padding" Value="0" /> + <Setter Property="Margin" Value="0" /> + <Setter Property="CornerRadius" Value="5" /> + <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> + <Setter Property="BorderThickness" Value="2"/> + <Style.Animations> + <Animation Duration="0:0:0.7"> + <KeyFrame Cue="0%"> + <Setter Property="MaxHeight" Value="0" /> + <Setter Property="Opacity" Value="0.0" /> + </KeyFrame> + <KeyFrame Cue="50%"> + <Setter Property="MaxHeight" Value="1000" /> + <Setter Property="Opacity" Value="0.3" /> + </KeyFrame> + <KeyFrame Cue="100%"> + <Setter Property="MaxHeight" Value="1000" /> + <Setter Property="Opacity" Value="1.0" /> + </KeyFrame> + </Animation> + </Style.Animations> + </Style> + <Style Selector="ListBox ListBoxItem:selected /template/ ContentPresenter"> + <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> + </Style> + <Style Selector="ListBox ListBoxItem:pointerover /template/ ContentPresenter"> + <Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" /> + </Style> <Styles.Resources> <SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" /> <StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" /> diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 010aff5148..46e135a9ba 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 996817b9d5..88b60d0ba2 100644 --- a/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -130,6 +130,18 @@ <DependentUpon>GameListView.axaml</DependentUpon> <SubType>Code</SubType> </Compile> + <Compile Update="UI\Views\User\UserEditorView.axaml.cs"> + <DependentUpon>UserEditor.axaml</DependentUpon> + <SubType>Code</SubType> + </Compile> + <Compile Update="UI\Views\User\UserRecovererView.axaml.cs"> + <DependentUpon>UserRecoverer.axaml</DependentUpon> + <SubType>Code</SubType> + </Compile> + <Compile Update="UI\Views\User\UserSelectorView.axaml.cs"> + <DependentUpon>UserSelector.axaml</DependentUpon> + <SubType>Code</SubType> + </Compile> </ItemGroup> <ItemGroup> diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml b/Ryujinx.Ava/UI/Controls/GameGridView.axaml index c757f066c1..862bc6d30e 100644 --- a/Ryujinx.Ava/UI/Controls/GameGridView.axaml +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml @@ -112,32 +112,8 @@ </ListBox.ItemsPanel> <ListBox.Styles> <Style Selector="ListBoxItem"> - <Setter Property="Padding" Value="0" /> <Setter Property="Margin" Value="5" /> <Setter Property="CornerRadius" Value="4" /> - <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> - <Style.Animations> - <Animation Duration="0:0:0.7"> - <KeyFrame Cue="0%"> - <Setter Property="MaxWidth" Value="0" /> - <Setter Property="Opacity" Value="0.0" /> - </KeyFrame> - <KeyFrame Cue="50%"> - <Setter Property="MaxWidth" Value="1000" /> - <Setter Property="Opacity" Value="0.3" /> - </KeyFrame> - <KeyFrame Cue="100%"> - <Setter Property="MaxWidth" Value="1000" /> - <Setter Property="Opacity" Value="1.0" /> - </KeyFrame> - </Animation> - </Style.Animations> - </Style> - <Style Selector="ListBoxItem:selected /template/ ContentPresenter"> - <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> - </Style> - <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter"> - <Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" /> </Style> <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator"> <Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" /> diff --git a/Ryujinx.Ava/UI/Controls/GameListView.axaml b/Ryujinx.Ava/UI/Controls/GameListView.axaml index 9fb5497b12..bb4e37b016 100644 --- a/Ryujinx.Ava/UI/Controls/GameListView.axaml +++ b/Ryujinx.Ava/UI/Controls/GameListView.axaml @@ -111,35 +111,6 @@ </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.Styles> - <Style Selector="ListBoxItem"> - <Setter Property="Padding" Value="0" /> - <Setter Property="Margin" Value="0" /> - <Setter Property="CornerRadius" Value="5" /> - <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> - <Setter Property="BorderThickness" Value="2"/> - <Style.Animations> - <Animation Duration="0:0:0.7"> - <KeyFrame Cue="0%"> - <Setter Property="MaxHeight" Value="0" /> - <Setter Property="Opacity" Value="0.0" /> - </KeyFrame> - <KeyFrame Cue="50%"> - <Setter Property="MaxHeight" Value="1000" /> - <Setter Property="Opacity" Value="0.3" /> - </KeyFrame> - <KeyFrame Cue="100%"> - <Setter Property="MaxHeight" Value="1000" /> - <Setter Property="Opacity" Value="1.0" /> - </KeyFrame> - </Animation> - </Style.Animations> - </Style> - <Style Selector="ListBoxItem:selected /template/ ContentPresenter"> - <Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" /> - </Style> - <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter"> - <Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" /> - </Style> <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator"> <Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" /> </Style> diff --git a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml index 90720478d9..bf34b303a7 100644 --- a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml +++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml @@ -12,5 +12,6 @@ <ui:Frame HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - x:Name="ContentFrame" /> + x:Name="ContentFrame"> + </ui:Frame> </UserControl> \ 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 0c30026756..6911a4d4c9 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<Border>()); + 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<SaveDataIterator>(); + + HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10]; + + HashSet<HLE.HOS.Services.Account.Acc.UserId> 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<UserProfile>().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<UserProfile>().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 56f8152ae4..0000000000 --- a/Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml +++ /dev/null @@ -1,57 +0,0 @@ -<UserControl - xmlns="https://github.com/avaloniaui" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - mc:Ignorable="d" - x:Class="Ryujinx.Ava.UI.Controls.ProfileImageSelectionDialog" - Focusable="True"> - <Grid - HorizontalAlignment="Stretch" - VerticalAlignment="Center" - Margin="5,10,5, 5"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - <RowDefinition Height="70" /> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - </Grid.RowDefinitions> - <TextBlock - FontWeight="Bold" - FontSize="18" - HorizontalAlignment="Center" - Grid.Row="1" - Text="{locale:Locale ProfileImageSelectionHeader}" /> - <TextBlock - FontWeight="Bold" - Grid.Row="2" - Margin="10" - MaxWidth="400" - TextWrapping="Wrap" - HorizontalAlignment="Center" - TextAlignment="Center" - Text="{locale:Locale ProfileImageSelectionNote}" /> - <StackPanel - Margin="5,0" - Spacing="10" - Grid.Row="4" - HorizontalAlignment="Center" - Orientation="Horizontal"> - <Button - Name="Import" - Click="Import_OnClick" - Width="200"> - <TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" /> - </Button> - <Button - Name="SelectFirmwareImage" - IsEnabled="{Binding FirmwareFound}" - Click="SelectFirmwareImage_OnClick" - Width="200"> - <TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" /> - </Button> - </StackPanel> - </Grid> -</UserControl> \ 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 64674b65bb..0000000000 --- a/Ryujinx.Ava/UI/Controls/SaveManager.axaml +++ /dev/null @@ -1,175 +0,0 @@ -<UserControl - xmlns="https://github.com/avaloniaui" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" - mc:Ignorable="d" - d:DesignWidth="800" - d:DesignHeight="450" - Height="400" - Width="550" - x:Class="Ryujinx.Ava.UI.Controls.SaveManager" - Focusable="True"> - <UserControl.Resources> - <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> - </UserControl.Resources> - <Grid> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition /> - </Grid.RowDefinitions> - <Grid - Grid.Row="0" - HorizontalAlignment="Stretch"> - <Grid.ColumnDefinitions> - <ColumnDefinition Width="Auto" /> - <ColumnDefinition /> - </Grid.ColumnDefinitions> - <StackPanel - Spacing="10" - Orientation="Horizontal" - HorizontalAlignment="Left" - VerticalAlignment="Center"> - <Label - Content="{locale:Locale CommonSort}" - VerticalAlignment="Center" /> - <ComboBox SelectedIndex="{Binding SortIndex}" Width="100"> - <ComboBoxItem> - <Label - VerticalAlignment="Center" - HorizontalContentAlignment="Left" - Content="{locale:Locale Name}" /> - </ComboBoxItem> - <ComboBoxItem> - <Label - VerticalAlignment="Center" - HorizontalContentAlignment="Left" - Content="{locale:Locale Size}" /> - </ComboBoxItem> - </ComboBox> - <ComboBox SelectedIndex="{Binding OrderIndex}" Width="150"> - <ComboBoxItem> - <Label - VerticalAlignment="Center" - HorizontalContentAlignment="Left" - Content="{locale:Locale OrderAscending}" /> - </ComboBoxItem> - <ComboBoxItem> - <Label - VerticalAlignment="Center" - HorizontalContentAlignment="Left" - Content="{locale:Locale OrderDescending}" /> - </ComboBoxItem> - </ComboBox> - </StackPanel> - <Grid - Grid.Column="1" - HorizontalAlignment="Stretch" - Margin="10,0, 0, 0"> - <Grid.ColumnDefinitions> - <ColumnDefinition Width="Auto"/> - <ColumnDefinition/> - </Grid.ColumnDefinitions> - <Label - Content="{locale:Locale Search}" - VerticalAlignment="Center"/> - <TextBox - Margin="5,0,0,0" - Grid.Column="1" - HorizontalAlignment="Stretch" - Text="{Binding Search}"/> - </Grid> - </Grid> - <Border - Grid.Row="1" - Margin="0,5" - BorderThickness="1" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch"> - <ListBox - Name="SaveList" - Items="{Binding View}" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch"> - <ListBox.ItemTemplate> - <DataTemplate x:DataType="models:SaveModel"> - <Grid HorizontalAlignment="Stretch" Margin="0,5"> - <Grid.ColumnDefinitions> - <ColumnDefinition /> - <ColumnDefinition Width="Auto" /> - </Grid.ColumnDefinitions> - <StackPanel Grid.Column="0" Orientation="Horizontal"> - <Border - Height="42" - Margin="2" - Width="42" - Padding="10" - IsVisible="{Binding !InGameList}"> - <ui:SymbolIcon - Symbol="Help" - FontSize="30" - HorizontalAlignment="Center" - VerticalAlignment="Center" /> - </Border> - <Image - IsVisible="{Binding InGameList}" - Margin="2" - Width="42" - Height="42" - Source="{Binding Icon, - Converter={StaticResource ByteImage}}" /> - <TextBlock - MaxLines="3" - Width="320" - Margin="5" - TextWrapping="Wrap" - Text="{Binding Title}" VerticalAlignment="Center" /> - </StackPanel> - <StackPanel - Grid.Column="1" - Spacing="10" - HorizontalAlignment="Right" - Orientation="Horizontal"> - <Label - Content="{Binding SizeString}" - IsVisible="{Binding SizeAvailable}" - VerticalAlignment="Center" - HorizontalAlignment="Right" /> - <Button - VerticalAlignment="Center" - HorizontalAlignment="Right" - Padding="10" - MinWidth="0" - MinHeight="0" - Name="OpenLocation" - Command="{Binding OpenLocation}"> - <ui:SymbolIcon - Symbol="OpenFolder" - HorizontalAlignment="Center" - VerticalAlignment="Center" /> - </Button> - <Button - VerticalAlignment="Center" - HorizontalAlignment="Right" - Padding="10" - MinWidth="0" - MinHeight="0" - Name="Delete" - Command="{Binding Delete}"> - <ui:SymbolIcon - Symbol="Delete" - HorizontalAlignment="Center" - VerticalAlignment="Center" /> - </Button> - </StackPanel> - </Grid> - </DataTemplate> - </ListBox.ItemTemplate> - </ListBox> - </Border> - </Grid> -</UserControl> \ 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 9910481c5c..0000000000 --- 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<SaveModel> _view = new ObservableCollection<SaveModel>(); - private string _search; - - public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>(); - - public ObservableCollection<SaveModel> 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<SaveDataIterator>(); - - _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); - - Span<SaveDataInfo> 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<SaveModel> GetComparer() - { - switch (SortIndex) - { - case 0: - return OrderIndex == 0 - ? SortExpressionComparer<SaveModel>.Ascending(save => save.Title) - : SortExpressionComparer<SaveModel>.Descending(save => save.Title); - case 1: - return OrderIndex == 0 - ? SortExpressionComparer<SaveModel>.Ascending(save => save.Size) - : SortExpressionComparer<SaveModel>.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 69f3d36a2d..0000000000 --- a/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml +++ /dev/null @@ -1,72 +0,0 @@ -<UserControl - xmlns="https://github.com/avaloniaui" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - mc:Ignorable="d" - d:DesignWidth="800" - d:DesignHeight="450" - MinWidth="500" - MinHeight="400" - x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer" - Focusable="True"> - <Design.DataContext> - <viewModels:UserProfileViewModel /> - </Design.DataContext> - <Grid HorizontalAlignment="Stretch" - VerticalAlignment="Stretch"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto"/> - <RowDefinition Height="Auto"/> - <RowDefinition/> - </Grid.RowDefinitions> - <Button Grid.Row="0" - Margin="5" - Height="30" - Width="50" - MinWidth="50" - HorizontalAlignment="Left" - Command="{Binding GoBack}"> - <ui:SymbolIcon Symbol="Back"/> - </Button> - <TextBlock Grid.Row="1" - Text="{locale:Locale UserProfilesRecoverHeading}"/> - <ListBox - Margin="5" - Grid.Row="2" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch" - Items="{Binding LostProfiles}"> - <ListBox.ItemTemplate> - <DataTemplate> - <Border - Margin="2" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch" - ClipToBounds="True" - CornerRadius="5"> - <Grid Margin="0"> - <Grid.ColumnDefinitions> - <ColumnDefinition/> - <ColumnDefinition Width="Auto"/> - </Grid.ColumnDefinitions> - <TextBlock - HorizontalAlignment="Stretch" - Text="{Binding UserId}" - TextAlignment="Left" - TextWrapping="Wrap" /> - <Button Grid.Column="1" - HorizontalAlignment="Right" - Command="{Binding Recover}" - CommandParameter="{Binding}" - Content="{locale:Locale Recover}"/> - </Grid> - </Border> - </DataTemplate> - </ListBox.ItemTemplate> - </ListBox> - </Grid> -</UserControl> diff --git a/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs b/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs deleted file mode 100644 index 9f29fddbd6..0000000000 --- a/Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 002d27a064..0000000000 --- a/Ryujinx.Ava/UI/Controls/UserSelector.axaml +++ /dev/null @@ -1,145 +0,0 @@ -<UserControl - x:Class="Ryujinx.Ava.UI.Controls.UserSelector" - xmlns="https://github.com/avaloniaui" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" - d:DesignHeight="450" - MinWidth="500" - d:DesignWidth="800" - mc:Ignorable="d" - Focusable="True"> - <UserControl.Resources> - <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> - </UserControl.Resources> - <Design.DataContext> - <viewModels:UserProfileViewModel /> - </Design.DataContext> - <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - <Grid.RowDefinitions> - <RowDefinition /> - <RowDefinition Height="Auto" /> - </Grid.RowDefinitions> - <ListBox - Margin="5" - MaxHeight="300" - HorizontalAlignment="Stretch" - VerticalAlignment="Center" - DoubleTapped="ProfilesList_DoubleTapped" - Items="{Binding Profiles}" - SelectionChanged="SelectingItemsControl_SelectionChanged"> - <ListBox.ItemsPanel> - <ItemsPanelTemplate> - <flex:FlexPanel - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch" - AlignContent="FlexStart" - JustifyContent="Center" /> - </ItemsPanelTemplate> - </ListBox.ItemsPanel> - <ListBox.ItemTemplate> - <DataTemplate> - <Grid> - <Border - Margin="2" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch" - ClipToBounds="True" - CornerRadius="5"> - <Grid Margin="0"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - </Grid.RowDefinitions> - <Image - Grid.Row="0" - Width="96" - Height="96" - Margin="0" - HorizontalAlignment="Stretch" - VerticalAlignment="Top" - Source="{Binding Image, Converter={StaticResource ByteImage}}" /> - <StackPanel - Grid.Row="1" - Height="30" - Margin="5" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch"> - <TextBlock - HorizontalAlignment="Stretch" - Text="{Binding Name}" - TextAlignment="Center" - TextWrapping="Wrap" /> - </StackPanel> - </Grid> - </Border> - <Border - Width="10" - Height="10" - Margin="5" - HorizontalAlignment="Left" - VerticalAlignment="Top" - Background="LimeGreen" - CornerRadius="5" - IsVisible="{Binding IsOpened}" /> - </Grid> - </DataTemplate> - </ListBox.ItemTemplate> - </ListBox> - <Grid - Grid.Row="1" - HorizontalAlignment="Center"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto"/> - <RowDefinition Height="Auto"/> - <RowDefinition Height="Auto"/> - </Grid.RowDefinitions> - <Grid.ColumnDefinitions> - <ColumnDefinition Width="Auto"/> - <ColumnDefinition Width="Auto"/> - </Grid.ColumnDefinitions> - <Button - HorizontalAlignment="Stretch" - Grid.Row="0" - Grid.Column="0" - Margin="2" - Command="{Binding AddUser}" - Content="{locale:Locale UserProfilesAddNewProfile}" /> - <Button - HorizontalAlignment="Stretch" - Grid.Row="0" - Margin="2" - Grid.Column="1" - Command="{Binding EditUser}" - Content="{locale:Locale UserProfilesEditProfile}" - IsEnabled="{Binding IsSelectedProfiledEditable}" /> - <Button - HorizontalAlignment="Stretch" - Grid.Row="1" - Grid.Column="0" - Margin="2" - Content="{locale:Locale UserProfilesManageSaves}" - Command="{Binding ManageSaves}" /> - <Button - HorizontalAlignment="Stretch" - Grid.Row="1" - Grid.Column="1" - Margin="2" - Command="{Binding DeleteUser}" - Content="{locale:Locale UserProfilesDeleteSelectedProfile}" - IsEnabled="{Binding IsSelectedProfileDeletable}" /> - <Button - HorizontalAlignment="Stretch" - Grid.Row="2" - Grid.ColumnSpan="2" - Grid.Column="0" - Margin="2" - Command="{Binding RecoverLostAccounts}" - Content="{locale:Locale UserProfilesRecoverLostAccounts}" /> - </Grid> - </Grid> -</UserControl> \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs b/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs deleted file mode 100644 index bd8c561e68..0000000000 --- a/Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; -using FluentAvalonia.UI.Controls; -using FluentAvalonia.UI.Navigation; -using Ryujinx.Ava.UI.ViewModels; -using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; - -namespace Ryujinx.Ava.UI.Controls -{ - public partial class UserSelector : UserControl - { - private NavigationDialogHost _parent; - public UserProfileViewModel ViewModel { get; set; } - - public UserSelector() - { - InitializeComponent(); - - if (Program.PreviewerDetached) - { - AddHandler(Frame.NavigatedToEvent, (s, e) => - { - NavigatedTo(e); - }, RoutingStrategies.Direct); - } - } - - private void NavigatedTo(NavigationEventArgs arg) - { - if (Program.PreviewerDetached) - { - if (arg.NavigationMode == NavigationMode.New) - { - _parent = (NavigationDialogHost)arg.Parameter; - ViewModel = _parent.ViewModel; - } - - DataContext = ViewModel; - } - } - - private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e) - { - if (sender is ListBox listBox) - { - int selectedIndex = listBox.SelectedIndex; - - if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count) - { - ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex]; - - _parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.UserId); - - ViewModel.LoadProfiles(); - - foreach (UserProfile profile in ViewModel.Profiles) - { - profile.UpdateState(); - } - } - } - } - - private void SelectingItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is ListBox listBox) - { - int selectedIndex = listBox.SelectedIndex; - - if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count) - { - ViewModel.HighlightedProfile = ViewModel.Profiles[selectedIndex]; - } - } - } - } -} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs index bdeceaeae6..8247a89b59 100644 --- a/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs +++ b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs @@ -2,7 +2,6 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Platform; -using Ryujinx.Ava.UI.Helper; using SPB.Graphics; using SPB.Platform; using SPB.Platform.GLX; @@ -148,9 +147,9 @@ namespace Ryujinx.Ava.UI.Helpers IntPtr.Zero); WindowHandle = handle; - + Marshal.FreeHGlobal(wndClassEx.lpszClassName); - + return new PlatformHandle(WindowHandle, "HWND"); } diff --git a/Ryujinx.Ava/Helper/LoggerAdapter.cs b/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs similarity index 99% rename from Ryujinx.Ava/Helper/LoggerAdapter.cs rename to Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs index c8f3fea149..ba251f6040 100644 --- a/Ryujinx.Ava/Helper/LoggerAdapter.cs +++ b/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs @@ -2,7 +2,7 @@ using Avalonia.Utilities; using System; using System.Text; -namespace Ryujinx.Ava.UI.Helper +namespace Ryujinx.Ava.UI.Helpers { using AvaLogger = Avalonia.Logging.Logger; using AvaLogLevel = Avalonia.Logging.LogEventLevel; diff --git a/Ryujinx.Ava/Helper/MetalHelper.cs b/Ryujinx.Ava/UI/Helpers/MetalHelper.cs similarity index 99% rename from Ryujinx.Ava/Helper/MetalHelper.cs rename to Ryujinx.Ava/UI/Helpers/MetalHelper.cs index ea3477eb94..5eb8660a15 100644 --- a/Ryujinx.Ava/Helper/MetalHelper.cs +++ b/Ryujinx.Ava/UI/Helpers/MetalHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.Versioning; using System.Runtime.InteropServices; using Avalonia; -namespace Ryujinx.Ava.UI.Helper +namespace Ryujinx.Ava.UI.Helpers { public delegate void UpdateBoundsCallbackDelegate(Rect rect); diff --git a/Ryujinx.Ava/UI/Models/ProfileImageModel.cs b/Ryujinx.Ava/UI/Models/ProfileImageModel.cs index 63da7b4498..8aa1940051 100644 --- a/Ryujinx.Ava/UI/Models/ProfileImageModel.cs +++ b/Ryujinx.Ava/UI/Models/ProfileImageModel.cs @@ -1,6 +1,9 @@ +using Avalonia.Media; +using Ryujinx.Ava.UI.ViewModels; + namespace Ryujinx.Ava.UI.Models { - public class ProfileImageModel + public class ProfileImageModel : BaseModel { public ProfileImageModel(string name, byte[] data) { @@ -10,5 +13,20 @@ namespace Ryujinx.Ava.UI.Models public string Name { get; set; } public byte[] Data { get; set; } + + private SolidColorBrush _backgroundColor = new(Colors.White); + + public SolidColorBrush BackgroundColor + { + get + { + return _backgroundColor; + } + set + { + _backgroundColor = value; + OnPropertyChanged(); + } + } } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Models/SaveModel.cs b/Ryujinx.Ava/UI/Models/SaveModel.cs index 3c20741fc3..7096f9d794 100644 --- a/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -4,13 +4,10 @@ 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.Helpers; 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; @@ -22,7 +19,6 @@ namespace Ryujinx.Ava.UI.Models 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}"; @@ -99,25 +95,5 @@ namespace Ryujinx.Ava.UI.Models }); } - - public void OpenLocation() - { - ApplicationHelper.OpenSaveDir(SaveId); - } - - public async void Delete() - { - 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, 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 2dd7a6c828..05e16632d6 100644 --- a/Ryujinx.Ava/UI/Models/TempProfile.cs +++ b/Ryujinx.Ava/UI/Models/TempProfile.cs @@ -7,10 +7,12 @@ namespace Ryujinx.Ava.UI.Models public class TempProfile : BaseModel { private readonly UserProfile _profile; - private byte[] _image = null; + private byte[] _image; private string _name = String.Empty; private UserId _userId; + public uint MaxProfileNameLength => 0x20; + public byte[] Image { get => _image; @@ -28,9 +30,12 @@ namespace Ryujinx.Ava.UI.Models { _userId = value; OnPropertyChanged(); + OnPropertyChanged(nameof(UserIdString)); } } + public string UserIdString => _userId.ToString(); + public string Name { get => _name; @@ -52,7 +57,5 @@ namespace Ryujinx.Ava.UI.Models UserId = profile.UserId; } } - - public TempProfile(){} } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Models/UserProfile.cs b/Ryujinx.Ava/UI/Models/UserProfile.cs index 869db66106..e7cd53007a 100644 --- a/Ryujinx.Ava/UI/Models/UserProfile.cs +++ b/Ryujinx.Ava/UI/Models/UserProfile.cs @@ -1,5 +1,7 @@ +using Avalonia.Media; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; using Ryujinx.HLE.HOS.Services.Account.Acc; using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile; @@ -12,6 +14,8 @@ namespace Ryujinx.Ava.UI.Models private byte[] _image; private string _name; private UserId _userId; + private bool _isPointerOver; + private IBrush _backgroundColor; public byte[] Image { @@ -43,27 +47,57 @@ namespace Ryujinx.Ava.UI.Models } } + public bool IsPointerOver + { + get => _isPointerOver; + set + { + _isPointerOver = value; + OnPropertyChanged(); + } + } + + public IBrush BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + OnPropertyChanged(); + } + } + public UserProfile(Profile profile, NavigationDialogHost owner) { _profile = profile; _owner = owner; + UpdateBackground(); + Image = profile.Image; Name = profile.Name; UserId = profile.UserId; } - public bool IsOpened => _profile.AccountState == AccountState.Open; - public void UpdateState() { - OnPropertyChanged(nameof(IsOpened)); + UpdateBackground(); OnPropertyChanged(nameof(Name)); } + private void UpdateBackground() + { + Avalonia.Application.Current.Styles.TryGetResource("ControlFillColorSecondary", out object color); + + if (color is not null) + { + BackgroundColor = _profile.AccountState == AccountState.Open ? new SolidColorBrush((Color)color) : Brushes.Transparent; + } + } + public void Recover(UserProfile userProfile) { - _owner.Navigate(typeof(UserEditor), (_owner, userProfile, true)); + _owner.Navigate(typeof(UserEditorView), (_owner, userProfile, true)); } } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs new file mode 100644 index 0000000000..9d981128c7 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs @@ -0,0 +1,230 @@ +using Avalonia.Media; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using Color = Avalonia.Media.Color; + +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserFirmwareAvatarSelectorViewModel : BaseModel + { + private static readonly Dictionary<string, byte[]> _avatarStore = new(); + + private ObservableCollection<ProfileImageModel> _images; + private Color _backgroundColor = Colors.White; + + private int _selectedIndex; + private byte[] _selectedImage; + + public UserFirmwareAvatarSelectorViewModel() + { + _images = new ObservableCollection<ProfileImageModel>(); + + LoadImagesFromStore(); + } + + public Color BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + OnPropertyChanged(); + ChangeImageBackground(); + } + } + + public ObservableCollection<ProfileImageModel> Images + { + get => _images; + set + { + _images = value; + OnPropertyChanged(); + } + } + + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + + if (_selectedIndex == -1) + { + SelectedImage = null; + } + else + { + SelectedImage = _images[_selectedIndex].Data; + } + + OnPropertyChanged(); + } + } + + public byte[] SelectedImage + { + get => _selectedImage; + private set => _selectedImage = value; + } + + private void LoadImagesFromStore() + { + Images.Clear(); + + foreach (var image in _avatarStore) + { + Images.Add(new ProfileImageModel(image.Key, image.Value)); + } + } + + private void ChangeImageBackground() + { + foreach (var image in Images) + { + image.BackgroundColor = new SolidColorBrush(BackgroundColor); + } + } + + public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem) + { + if (_avatarStore.Count > 0) + { + return; + } + + string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data); + string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath); + + if (!string.IsNullOrWhiteSpace(avatarPath)) + { + using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open)) + { + Nca nca = new(virtualFileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); + + foreach (DirectoryEntryEx item in romfs.EnumerateEntries()) + { + // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy. + if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs")) + { + using var file = new UniqueRef<IFile>(); + + romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using (MemoryStream stream = new()) + using (MemoryStream streamPng = new()) + { + file.Get.AsStream().CopyTo(stream); + + stream.Position = 0; + + Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256); + + avatarImage.SaveAsPng(streamPng); + + _avatarStore.Add(item.FullPath, streamPng.ToArray()); + } + } + } + } + } + } + + private static byte[] DecompressYaz0(Stream stream) + { + using (BinaryReader reader = new(stream)) + { + reader.ReadInt32(); // Magic + + uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); + + reader.ReadInt64(); // Padding + + byte[] input = new byte[stream.Length - stream.Position]; + stream.Read(input, 0, input.Length); + + uint inputOffset = 0; + + byte[] output = new byte[decodedLength]; + uint outputOffset = 0; + + ushort mask = 0; + byte header = 0; + + while (outputOffset < decodedLength) + { + if ((mask >>= 1) == 0) + { + header = input[inputOffset++]; + mask = 0x80; + } + + if ((header & mask) != 0) + { + if (outputOffset == output.Length) + { + break; + } + + output[outputOffset++] = input[inputOffset++]; + } + else + { + byte byte1 = input[inputOffset++]; + byte byte2 = input[inputOffset++]; + + uint dist = (uint)((byte1 & 0xF) << 8) | byte2; + uint position = outputOffset - (dist + 1); + + uint length = (uint)byte1 >> 4; + if (length == 0) + { + length = (uint)input[inputOffset++] + 0x12; + } + else + { + length += 2; + } + + uint gap = outputOffset - position; + uint nonOverlappingLength = length; + + if (nonOverlappingLength > gap) + { + nonOverlappingLength = gap; + } + + Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength); + outputOffset += nonOverlappingLength; + position += nonOverlappingLength; + length -= nonOverlappingLength; + + while (length-- > 0) + { + output[outputOffset++] = output[position++]; + } + } + } + + return output; + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs new file mode 100644 index 0000000000..7261631c16 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserProfileImageSelectorViewModel : BaseModel + { + private bool _firmwareFound; + + public bool FirmwareFound + { + get => _firmwareFound; + + set + { + _firmwareFound = value; + OnPropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs index 3f0a85c9d2..8f997efc1a 100644 --- a/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs @@ -1,215 +1,25 @@ -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.Ava.UI.Helpers; -using Ryujinx.HLE.HOS.Services.Account.Acc; +using Microsoft.IdentityModel.Tokens; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; namespace Ryujinx.Ava.UI.ViewModels { public class UserProfileViewModel : BaseModel, IDisposable { - private readonly NavigationDialogHost _owner; - - private UserProfile _selectedProfile; - private UserProfile _highlightedProfile; - public UserProfileViewModel() { - Profiles = new ObservableCollection<UserProfile>(); + Profiles = new ObservableCollection<BaseModel>(); LostProfiles = new ObservableCollection<UserProfile>(); + IsEmpty = LostProfiles.IsNullOrEmpty(); } - public UserProfileViewModel(NavigationDialogHost owner) : this() - { - _owner = owner; - - LoadProfiles(); - } - - public ObservableCollection<UserProfile> Profiles { get; set; } + public ObservableCollection<BaseModel> Profiles { get; set; } public ObservableCollection<UserProfile> LostProfiles { get; set; } - public UserProfile SelectedProfile - { - get => _selectedProfile; - set - { - _selectedProfile = value; - - OnPropertyChanged(); - OnPropertyChanged(nameof(IsHighlightedProfileDeletable)); - OnPropertyChanged(nameof(IsHighlightedProfileEditable)); - } - } - - public bool IsHighlightedProfileEditable => _highlightedProfile != null; - - public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId; - - public UserProfile HighlightedProfile - { - get => _highlightedProfile; - set - { - _highlightedProfile = value; - - OnPropertyChanged(); - OnPropertyChanged(nameof(IsHighlightedProfileDeletable)); - OnPropertyChanged(nameof(IsHighlightedProfileEditable)); - } - } + public bool IsEmpty { get; set; } public void Dispose() { } - - 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, _owner)); - } - - SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId); - - if (SelectedProfile == null) - { - SelectedProfile = Profiles.First(); - - if (SelectedProfile != null) - { - _owner.AccountManager.OpenUser(_selectedProfile.UserId); - } - } - - var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, - default, saveDataId: default, index: default); - - using var saveDataIterator = new UniqueRef<SaveDataIterator>(); - - _owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); - - Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10]; - - HashSet<UserId> lostAccounts = new HashSet<UserId>(); - - 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 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() - { - UserProfile userProfile = null; - - _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[LocaleKeys.SaveManagerHeading], userProfile.Name), - PrimaryButtonText = "", - SecondaryButtonText = "", - CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], - Content = manager, - Padding = new Thickness(0) - }; - - await contentDialog.ShowAsync(); - } - - public void EditUser() - { - _owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false)); - } - - public async void DeleteUser() - { - if (_highlightedProfile != null) - { - var lastUserId = _owner.AccountManager.LastOpenedUser.UserId; - - if (_highlightedProfile.UserId == lastUserId) - { - // If we are deleting the currently open profile, then we must open something else before deleting. - var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId); - - if (profile == null) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); - }); - - return; - } - - _owner.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) - { - _owner.AccountManager.DeleteUser(_highlightedProfile.UserId); - } - } - - 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/ViewModels/UserSaveManagerViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs new file mode 100644 index 0000000000..bd37435084 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs @@ -0,0 +1,123 @@ +using DynamicData; +using DynamicData.Binding; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class UserSaveManagerViewModel : BaseModel + { + private int _sortIndex; + private int _orderIndex; + private string _search; + private ObservableCollection<SaveModel> _saves; + private ObservableCollection<SaveModel> _views; + private AccountManager _accountManager; + + public string SaveManagerHeading => + string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId); + + public int SortIndex + { + get => _sortIndex; + set + { + _sortIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public int OrderIndex + { + get => _orderIndex; + set + { + _orderIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection<SaveModel> Saves + { + get => _saves; + set + { + _saves = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection<SaveModel> Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public UserSaveManagerViewModel(AccountManager accountManager) + { + _accountManager = accountManager; + _saves = new ObservableCollection<SaveModel>(); + _views = new ObservableCollection<SaveModel>(); + } + + public void Sort() + { + Saves.AsObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + OnPropertyChanged(nameof(Views)); + } + + private bool Filter(object arg) + { + if (arg is SaveModel save) + { + return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + private IComparer<SaveModel> GetComparer() + { + switch (SortIndex) + { + case 0: + return OrderIndex == 0 + ? SortExpressionComparer<SaveModel>.Ascending(save => save.Title) + : SortExpressionComparer<SaveModel>.Descending(save => save.Title); + case 1: + return OrderIndex == 0 + ? SortExpressionComparer<SaveModel>.Ascending(save => save.Size) + : SortExpressionComparer<SaveModel>.Descending(save => save.Size); + default: + return null; + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UserEditor.axaml b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml similarity index 52% rename from Ryujinx.Ava/UI/Controls/UserEditor.axaml rename to Ryujinx.Ava/UI/Views/User/UserEditorView.axaml index 155f1cfecf..7e55f25e49 100644 --- a/Ryujinx.Ava/UI/Controls/UserEditor.axaml +++ b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml @@ -1,16 +1,20 @@ <UserControl - x:Class="Ryujinx.Ava.UI.Controls.UserEditor" + x:Class="Ryujinx.Ava.UI.Views.User.UserEditorView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" Margin="0" MinWidth="500" Padding="0" mc:Ignorable="d" - Focusable="True"> + Focusable="True" + x:CompileBindings="True" + x:DataType="models:TempProfile"> <UserControl.Resources> <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> </UserControl.Resources> @@ -23,35 +27,9 @@ <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> - <StackPanel - HorizontalAlignment="Left" - VerticalAlignment="Stretch" - Orientation="Vertical"> - <Image - Name="ProfileImage" - Width="96" - Height="96" - Margin="0" - HorizontalAlignment="Stretch" - VerticalAlignment="Top" - Source="{Binding Image, Converter={StaticResource ByteImage}}" /> - <Button - Name="ChangePictureButton" - Margin="5" - HorizontalAlignment="Stretch" - Click="ChangePictureButton_Click" - Content="{locale:Locale UserProfilesChangeProfileImage}" /> - <Button - Name="AddPictureButton" - Margin="5" - HorizontalAlignment="Stretch" - Click="ChangePictureButton_Click" - Content="{locale:Locale UserProfilesSetProfileImage}" /> - </StackPanel> <StackPanel Grid.Row="0" - Grid.Column="1" - Margin="5,10" + Grid.Column="0" HorizontalAlignment="Stretch" Orientation="Vertical" Spacing="10"> @@ -61,9 +39,60 @@ Width="300" HorizontalAlignment="Stretch" MaxLength="{Binding MaxProfileNameLength}" + Watermark="{locale:Locale ProfileNameSelectionWatermark}" Text="{Binding Name}" /> <TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" /> - <TextBlock Name="IdLabel" Text="{Binding UserId}" /> + <TextBox + Name="IdLabel" + Width="300" + HorizontalAlignment="Stretch" + IsReadOnly="True" + Text="{Binding UserIdString}" /> + </StackPanel> + <StackPanel + Grid.Row="0" + Grid.Column="1" + HorizontalAlignment="Right" + VerticalAlignment="Stretch" + Orientation="Vertical"> + <Border + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + BorderThickness="1"> + <Panel> + <ui:SymbolIcon + FontSize="60" + Width="96" + Height="96" + Margin="0" + Foreground="{DynamicResource AppListHoverBackgroundColor}" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Symbol="Camera" /> + <Image + Name="ProfileImage" + Width="96" + Height="96" + Margin="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Source="{Binding Image, Converter={StaticResource ByteImage}}" /> + </Panel> + </Border> + </StackPanel> + <StackPanel + Grid.Row="1" + Grid.Column="0" + Grid.ColumnSpan="2" + HorizontalAlignment="Left" + Orientation="Horizontal" + Margin="0 24 0 0" + Spacing="10"> + <Button + Width="50" + MinWidth="50" + Click="BackButton_Click"> + <ui:SymbolIcon Symbol="Back" /> + </Button> </StackPanel> <StackPanel Grid.Row="1" @@ -71,16 +100,24 @@ Grid.ColumnSpan="2" HorizontalAlignment="Right" Orientation="Horizontal" + Margin="0 24 0 0" Spacing="10"> + <Button + Name="DeleteButton" + Click="DeleteButton_Click" + Content="{locale:Locale UserProfilesDelete}" /> + <Button + Name="ChangePictureButton" + Click="ChangePictureButton_Click" + Content="{locale:Locale UserProfilesChangeProfileImage}" /> + <Button + Name="AddPictureButton" + Click="ChangePictureButton_Click" + Content="{locale:Locale UserProfilesSetProfileImage}" /> <Button Name="SaveButton" Click="SaveButton_Click" Content="{locale:Locale Save}" /> - <Button - Name="CloseButton" - HorizontalAlignment="Right" - Click="CloseButton_Click" - Content="{locale:Locale Discard}" /> </StackPanel> </Grid> -</UserControl> +</UserControl> \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs similarity index 52% rename from Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs rename to Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs index 18bb8b22e8..fb33dcf8fd 100644 --- a/Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs +++ b/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs @@ -4,13 +4,16 @@ using Avalonia.Interactivity; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Navigation; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; -namespace Ryujinx.Ava.UI.Controls +namespace Ryujinx.Ava.UI.Views.User { - public partial class UserEditor : UserControl + public partial class UserEditorView : UserControl { private NavigationDialogHost _parent; private UserProfile _profile; @@ -18,8 +21,9 @@ namespace Ryujinx.Ava.UI.Controls public TempProfile TempProfile { get; set; } public uint MaxProfileNameLength => 0x20; + public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId; - public UserEditor() + public UserEditorView() { InitializeComponent(); AddHandler(Frame.NavigatedToEvent, (s, e) => @@ -44,41 +48,84 @@ namespace Ryujinx.Ava.UI.Controls break; } + ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " + + $"{ (_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}"; + DataContext = TempProfile; AddPictureButton.IsVisible = _isNewUser; + ChangePictureButton.IsVisible = !_isNewUser; IdLabel.IsVisible = _profile != null; IdText.IsVisible = _profile != null; - ChangePictureButton.IsVisible = !_isNewUser; + if (!_isNewUser && IsDeletable) + { + DeleteButton.IsVisible = true; + } + else + { + DeleteButton.IsVisible = false; + } } } - private void CloseButton_Click(object sender, RoutedEventArgs e) + private async void BackButton_Click(object sender, RoutedEventArgs e) { - _parent?.GoBack(); + if (_isNewUser) + { + if (TempProfile.Name != String.Empty || TempProfile.Image != null) + { + if (await ContentDialogHelper.CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle], + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage], + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage])) + { + _parent?.GoBack(); + } + } + else + { + _parent?.GoBack(); + } + } + else + { + if (_profile.Name != TempProfile.Name || _profile.Image != TempProfile.Image) + { + if (await ContentDialogHelper.CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle], + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage], + LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage])) + { + _parent?.GoBack(); + } + } + else + { + _parent?.GoBack(); + } + } } - private async void SaveButton_Click(object sender, RoutedEventArgs e) + private void DeleteButton_Click(object sender, RoutedEventArgs e) + { + _parent.DeleteUser(_profile); + } + + private void SaveButton_Click(object sender, RoutedEventArgs e) { DataValidationErrors.ClearErrors(NameBox); - bool isInvalid = false; if (string.IsNullOrWhiteSpace(TempProfile.Name)) { DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError])); - isInvalid = true; + return; } if (TempProfile.Image == null) { - await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UserProfileNoImageError], ""); + _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile)); - isInvalid = true; - } - - if(isInvalid) - { return; } @@ -104,7 +151,7 @@ namespace Ryujinx.Ava.UI.Controls public void SelectProfileImage() { - _parent.Navigate(typeof(ProfileImageSelectionDialog), (_parent, TempProfile)); + _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile)); } private void ChangePictureButton_Click(object sender, RoutedEventArgs e) diff --git a/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml new file mode 100644 index 0000000000..d46fcefc23 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml @@ -0,0 +1,114 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + Width="528" + d:DesignWidth="578" + d:DesignHeight="350" + x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + x:CompileBindings="True" + x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel" + Focusable="True"> + <Design.DataContext> + <viewModels:UserFirmwareAvatarSelectorViewModel /> + </Design.DataContext> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + </UserControl.Resources> + <Grid + Margin="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <ListBox + Grid.Row="1" + BorderThickness="0" + SelectedIndex="{Binding SelectedIndex}" + Height="400" + Items="{Binding Images}" + HorizontalAlignment="Stretch" + VerticalAlignment="Center"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <WrapPanel + Orientation="Horizontal" + Margin="0" + HorizontalAlignment="Center" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="CornerRadius" Value="4" /> + <Setter Property="Width" Value="85" /> + <Setter Property="MaxWidth" Value="85" /> + <Setter Property="MinWidth" Value="85" /> + </Style> + <Style Selector="ListBoxItem /template/ Border#SelectionIndicator"> + <Setter Property="MinHeight" Value="70" /> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate> + <Panel + Background="{Binding BackgroundColor}" + Margin="5"> + <Image Source="{Binding Data, Converter={StaticResource ByteImage}}" /> + </Panel> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + <StackPanel + Grid.Row="3" + Orientation="Horizontal" + Spacing="10" + Margin="0 24 0 0" + HorizontalAlignment="Left"> + <Button + Width="50" + MinWidth="50" + Height="35" + Click="GoBack"> + <ui:SymbolIcon Symbol="Back" /> + </Button> + </StackPanel> + <StackPanel + Grid.Row="3" + Orientation="Horizontal" + Spacing="10" + Margin="0 24 0 0" + HorizontalAlignment="Right"> + <ui:ColorPickerButton + FlyoutPlacement="Top" + IsMoreButtonVisible="False" + UseColorPalette="False" + UseColorTriangle="False" + UseColorWheel="False" + ShowAcceptDismissButtons="False" + IsAlphaEnabled="False" + Color="{Binding BackgroundColor, Mode=TwoWay}" + Name="ColorButton"> + <ui:ColorPickerButton.Styles> + <Style Selector="Grid#Root > DockPanel > Grid"> + <Setter Property="IsVisible" Value="False" /> + </Style> + </ui:ColorPickerButton.Styles> + </ui:ColorPickerButton> + <Button + Content="{locale:Locale AvatarChoose}" + Height="35" + Name="ChooseButton" + Click="ChooseButton_OnClick" /> + </StackPanel> + </Grid> +</UserControl> \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs similarity index 55% rename from Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs rename to Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs index e060d65e90..7c9191ab2d 100644 --- a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs @@ -6,15 +6,20 @@ 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.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.IO; -namespace Ryujinx.Ava.UI.Windows +namespace Ryujinx.Ava.UI.Views.User { - public partial class AvatarWindow : UserControl + public partial class UserFirmwareAvatarSelectorView : UserControl { private NavigationDialogHost _parent; private TempProfile _profile; - public AvatarWindow(ContentManager contentManager) + public UserFirmwareAvatarSelectorView(ContentManager contentManager) { ContentManager = contentManager; @@ -23,7 +28,7 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public AvatarWindow() + public UserFirmwareAvatarSelectorView() { InitializeComponent(); @@ -43,7 +48,7 @@ namespace Ryujinx.Ava.UI.Windows ContentManager = _parent.ContentManager; if (Program.PreviewerDetached) { - ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages()); + ViewModel = new UserFirmwareAvatarSelectorViewModel(); } DataContext = ViewModel; @@ -53,22 +58,28 @@ namespace Ryujinx.Ava.UI.Windows public ContentManager ContentManager { get; private set; } - internal AvatarProfileViewModel ViewModel { get; set; } + internal UserFirmwareAvatarSelectorViewModel ViewModel { get; set; } - private void CloseButton_OnClick(object sender, RoutedEventArgs e) + private void GoBack(object sender, RoutedEventArgs e) { - ViewModel.Dispose(); - _parent.GoBack(); } private void ChooseButton_OnClick(object sender, RoutedEventArgs e) { - if (ViewModel.SelectedIndex > -1) + if (ViewModel.SelectedImage != null) { - _profile.Image = ViewModel.SelectedImage; + MemoryStream streamJpg = new(); + SixLabors.ImageSharp.Image avatarImage = SixLabors.ImageSharp.Image.Load(ViewModel.SelectedImage, new PngDecoder()); - ViewModel.Dispose(); + avatarImage.Mutate(x => x.BackgroundColor(new Rgba32( + ViewModel.BackgroundColor.R, + ViewModel.BackgroundColor.G, + ViewModel.BackgroundColor.B, + ViewModel.BackgroundColor.A))); + avatarImage.SaveAsJpeg(streamJpg); + + _profile.Image = streamJpg.ToArray(); _parent.GoBack(); } diff --git a/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml new file mode 100644 index 0000000000..b9f51fdc7a --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml @@ -0,0 +1,63 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels" + Focusable="True" + mc:Ignorable="d" + x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView" + x:CompileBindings="True" + x:DataType="viewModles:UserProfileImageSelectorViewModel" + Width="500" + d:DesignWidth="500"> + <Design.DataContext> + <viewModles:UserProfileImageSelectorViewModel /> + </Design.DataContext> + <Grid + HorizontalAlignment="Stretch" + VerticalAlignment="Center"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="70" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock + Grid.Row="0" + TextWrapping="Wrap" + HorizontalAlignment="Left" + TextAlignment="Left" + Text="{locale:Locale ProfileImageSelectionNote}" /> + <StackPanel + Grid.Row="2" + Spacing="10" + HorizontalAlignment="Left" + Orientation="Horizontal"> + <Button + Width="50" + MinWidth="50" + Click="GoBack"> + <ui:SymbolIcon Symbol="Back" /> + </Button> + </StackPanel> + <StackPanel + Grid.Row="2" + Spacing="10" + HorizontalAlignment="Right" + Orientation="Horizontal"> + <Button + Name="Import" + Click="Import_OnClick"> + <TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" /> + </Button> + <Button + Name="SelectFirmwareImage" + IsEnabled="{Binding FirmwareFound}" + Click="SelectFirmwareImage_OnClick"> + <TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" /> + </Button> + </StackPanel> + </Grid> +</UserControl> \ 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 46a2f5079b..18f76f805c 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 0000000000..62b5e1840b --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml @@ -0,0 +1,83 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + d:DesignWidth="550" + d:DesignHeight="450" + Width="500" + Height="400" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView" + x:CompileBindings="True" + x:DataType="viewModels:UserProfileViewModel" + Focusable="True"> + <Design.DataContext> + <viewModels:UserProfileViewModel /> + </Design.DataContext> + <Grid HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition/> + <RowDefinition Height="Auto"/> + </Grid.RowDefinitions> + <Border + CornerRadius="5" + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + BorderThickness="1" + Grid.Row="0"> + <Panel> + <ListBox + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Items="{Binding LostProfiles}"> + <ListBox.ItemTemplate> + <DataTemplate> + <Border + Margin="2" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + ClipToBounds="True" + CornerRadius="5"> + <Grid Margin="0"> + <Grid.ColumnDefinitions> + <ColumnDefinition/> + <ColumnDefinition Width="Auto"/> + </Grid.ColumnDefinitions> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding UserId}" + TextAlignment="Left" + TextWrapping="Wrap" /> + <Button Grid.Column="1" + HorizontalAlignment="Right" + Click="Recover" + CommandParameter="{Binding}" + Content="{locale:Locale Recover}"/> + </Grid> + </Border> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + <TextBlock + IsVisible="{Binding IsEmpty}" + TextAlignment="Center" + Text="{locale:Locale UserProfilesRecoverEmptyList}"/> + </Panel> + </Border> + <StackPanel + Grid.Row="1" + Margin="0 24 0 0" + Orientation="Horizontal"> + <Button + Width="50" + MinWidth="50" + Click="GoBack"> + <ui:SymbolIcon Symbol="Back"/> + </Button> + </StackPanel> + </Grid> +</UserControl> \ 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 0000000000..0c53e53d70 --- /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 0000000000..cdf74d52f1 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml @@ -0,0 +1,199 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + mc:Ignorable="d" + d:DesignWidth="600" + d:DesignHeight="500" + Height="450" + Width="550" + x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView" + x:CompileBindings="True" + x:DataType="viewModels:UserSaveManagerViewModel" + Focusable="True"> + <Design.DataContext> + <viewModels:UserSaveManagerViewModel /> + </Design.DataContext> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + </UserControl.Resources> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Grid + Grid.Row="0" + HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <StackPanel + Spacing="10" + Orientation="Horizontal" + HorizontalAlignment="Left" + VerticalAlignment="Center"> + <Label Content="{locale:Locale CommonSort}" VerticalAlignment="Center" /> + <ComboBox SelectedIndex="{Binding SortIndex}" Width="100"> + <ComboBoxItem> + <Label + VerticalAlignment="Center" + HorizontalContentAlignment="Left" + Content="{locale:Locale Name}" /> + </ComboBoxItem> + <ComboBoxItem> + <Label + VerticalAlignment="Center" + HorizontalContentAlignment="Left" + Content="{locale:Locale Size}" /> + </ComboBoxItem> + </ComboBox> + <ComboBox SelectedIndex="{Binding OrderIndex}" Width="150"> + <ComboBoxItem> + <Label + VerticalAlignment="Center" + HorizontalContentAlignment="Left" + Content="{locale:Locale OrderAscending}" /> + </ComboBoxItem> + <ComboBoxItem> + <Label + VerticalAlignment="Center" + HorizontalContentAlignment="Left" + Content="{locale:Locale OrderDescending}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <Grid + Grid.Column="1" + HorizontalAlignment="Stretch" + Margin="10,0, 0, 0"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto"/> + <ColumnDefinition/> + </Grid.ColumnDefinitions> + <Label Content="{locale:Locale Search}" VerticalAlignment="Center" /> + <TextBox + Margin="5,0,0,0" + Grid.Column="1" + HorizontalAlignment="Stretch" + Text="{Binding Search}" /> + </Grid> + </Grid> + <Border + Grid.Row="1" + Margin="0,5" + BorderThickness="1" + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + CornerRadius="5" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <ListBox + Name="SaveList" + Items="{Binding Views}" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Padding" Value="10" /> + <Setter Property="Margin" Value="5" /> + <Setter Property="CornerRadius" Value="4" /> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate x:DataType="models:SaveModel"> + <Grid HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <StackPanel + Grid.Column="0" + Orientation="Horizontal" + Spacing="5"> + <Border + Height="42" + Width="42" + Padding="10" + IsVisible="{Binding !InGameList}"> + <ui:SymbolIcon + Symbol="Help" + FontSize="30" + HorizontalAlignment="Center" + VerticalAlignment="Center" /> + </Border> + <Image + IsVisible="{Binding InGameList}" + Width="42" + Height="42" + Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + <TextBlock + MaxLines="3" + Width="320" + Margin="5" + TextWrapping="Wrap" + Text="{Binding Title}" + VerticalAlignment="Center" /> + </StackPanel> + <StackPanel + Grid.Column="1" + Spacing="10" + HorizontalAlignment="Right" + Orientation="Horizontal"> + <Label + Content="{Binding SizeString}" + IsVisible="{Binding SizeAvailable}" + VerticalAlignment="Center" + HorizontalAlignment="Right" /> + <Button + VerticalAlignment="Center" + HorizontalAlignment="Right" + Padding="10" + MinWidth="0" + MinHeight="0" + Name="OpenLocation" + Click="OpenLocation"> + <ui:SymbolIcon + Symbol="OpenFolder" + HorizontalAlignment="Center" + VerticalAlignment="Center" /> + </Button> + <Button + VerticalAlignment="Center" + HorizontalAlignment="Right" + Padding="10" + MinWidth="0" + MinHeight="0" + Name="Delete" + Click="Delete"> + <ui:SymbolIcon + Symbol="Delete" + HorizontalAlignment="Center" + VerticalAlignment="Center" /> + </Button> + </StackPanel> + </Grid> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + </Border> + <StackPanel + Grid.Row="2" + Margin="0 24 0 0" + Orientation="Horizontal"> + <Button + Width="50" + MinWidth="50" + Click="GoBack"> + <ui:SymbolIcon Symbol="Back" /> + </Button> + </StackPanel> + </Grid> +</UserControl> \ 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 0000000000..9d955326f7 --- /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<SaveModel>(); + 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<SaveDataIterator>(); + + _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span<SaveDataInfo> 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 0000000000..9a6ba054e2 --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml @@ -0,0 +1,165 @@ +<UserControl + x:Class="Ryujinx.Ava.UI.Views.User.UserSelectorViews" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + d:DesignHeight="450" + MinWidth="500" + d:DesignWidth="800" + mc:Ignorable="d" + Focusable="True" + x:CompileBindings="True" + x:DataType="viewModels:UserProfileViewModel"> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + </UserControl.Resources> + <Design.DataContext> + <viewModels:UserProfileViewModel /> + </Design.DataContext> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Border + CornerRadius="5" + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + BorderThickness="1"> + <ListBox + MaxHeight="300" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + SelectionChanged="ProfilesList_SelectionChanged" + Background="Transparent" + Items="{Binding Profiles}"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <flex:FlexPanel + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + AlignContent="FlexStart" + JustifyContent="FlexStart" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Margin" Value="5 5 0 5" /> + <Setter Property="CornerRadius" Value="5" /> + </Style> + <Style Selector="Border#SelectionIndicator"> + <Setter Property="Opacity" Value="0" /> + </Style> + </ListBox.Styles> + <ListBox.DataTemplates> + <DataTemplate + DataType="models:UserProfile"> + <Grid + PointerEnter="Grid_PointerEntered" + PointerLeave="Grid_OnPointerExited"> + <Border + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + ClipToBounds="True" + CornerRadius="5" + Background="{Binding BackgroundColor}"> + <StackPanel + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Image + Width="96" + Height="96" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Source="{Binding Image, Converter={StaticResource ByteImage}}" /> + <TextBlock + HorizontalAlignment="Stretch" + MaxWidth="90" + Text="{Binding Name}" + TextAlignment="Center" + TextWrapping="Wrap" + TextTrimming="CharacterEllipsis" + MaxLines="2" + Margin="5" /> + </StackPanel> + </Border> + <Border + Margin="2" + Height="24" + Width="24" + CornerRadius="12" + HorizontalAlignment="Right" + VerticalAlignment="Top" + Background="{DynamicResource ThemeContentBackgroundColor}" + IsVisible="{Binding IsPointerOver}"> + <Button + MaxHeight="24" + MaxWidth="24" + MinHeight="24" + MinWidth="24" + CornerRadius="12" + Padding="0" + Click="EditUser"> + <ui:SymbolIcon Symbol="Edit" /> + </Button> + </Border> + </Grid> + </DataTemplate> + <DataTemplate + DataType="viewModels:BaseModel"> + <Panel + Height="118" + Width="96"> + <Button + MinWidth="50" + MinHeight="50" + MaxWidth="50" + MaxHeight="50" + CornerRadius="25" + Margin="10" + Padding="0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Click="AddUser"> + <ui:SymbolIcon Symbol="Add" /> + </Button> + <Panel.Styles> + <Style Selector="Panel"> + <Setter Property="Background" Value="{DynamicResource ListBoxBackground}"/> + </Style> + </Panel.Styles> + </Panel> + </DataTemplate> + </ListBox.DataTemplates> + </ListBox> + </Border> + <StackPanel + Grid.Row="1" + Margin="0 24 0 0" + HorizontalAlignment="Left" + Orientation="Horizontal" + Spacing="10"> + <Button + Click="ManageSaves" + Content="{locale:Locale UserProfilesManageSaves}" /> + <Button + Click="RecoverLostAccounts" + Content="{locale:Locale UserProfilesRecoverLostAccounts}" /> + </StackPanel> + <StackPanel + Grid.Row="1" + Margin="0 24 0 0" + HorizontalAlignment="Right" + Orientation="Horizontal"> + <Button + Click="Close" + Content="{locale:Locale UserProfilesClose}" /> + </StackPanel> + </Grid> +</UserControl> \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs new file mode 100644 index 0000000000..aa89fea9ee --- /dev/null +++ b/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs @@ -0,0 +1,128 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.ViewModels; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; + +namespace Ryujinx.Ava.UI.Views.User +{ + public partial class UserSelectorViews : UserControl + { + private NavigationDialogHost _parent; + + public UserProfileViewModel ViewModel { get; set; } + + public UserSelectorViews() + { + InitializeComponent(); + + if (Program.PreviewerDetached) + { + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + if (arg.NavigationMode == NavigationMode.New) + { + _parent = (NavigationDialogHost)arg.Parameter; + ViewModel = _parent.ViewModel; + } + + if (arg.NavigationMode == NavigationMode.Back) + { + ((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]; + } + + DataContext = ViewModel; + } + } + + private void Grid_PointerEntered(object sender, PointerEventArgs e) + { + if (sender is Grid grid) + { + if (grid.DataContext is UserProfile profile) + { + profile.IsPointerOver = true; + } + } + } + + private void Grid_OnPointerExited(object sender, PointerEventArgs e) + { + if (sender is Grid grid) + { + if (grid.DataContext is UserProfile profile) + { + profile.IsPointerOver = false; + } + } + } + + private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is ListBox listBox) + { + int selectedIndex = listBox.SelectedIndex; + + if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count) + { + if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile) + { + _parent?.AccountManager?.OpenUser(userProfile.UserId); + + foreach (BaseModel profile in ViewModel.Profiles) + { + if (profile is UserProfile uProfile) + { + uProfile.UpdateState(); + } + } + } + } + } + } + + private void AddUser(object sender, RoutedEventArgs e) + { + _parent.AddUser(); + } + + private void EditUser(object sender, RoutedEventArgs e) + { + if (sender is Avalonia.Controls.Button button) + { + if (button.DataContext is UserProfile userProfile) + { + _parent.EditUser(userProfile); + } + } + } + + private void ManageSaves(object sender, RoutedEventArgs e) + { + _parent.ManageSaves(); + } + + private void RecoverLostAccounts(object sender, RoutedEventArgs e) + { + _parent.RecoverLostAccounts(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)_parent.Parent).Hide(); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml b/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml deleted file mode 100644 index 1d30fff583..0000000000 --- a/Ryujinx.Ava/UI/Windows/AvatarWindow.axaml +++ /dev/null @@ -1,54 +0,0 @@ -<UserControl - xmlns="https://github.com/avaloniaui" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" - mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350" - x:Class="Ryujinx.Ava.UI.Windows.AvatarWindow" - Margin="0" - Padding="0" - x:CompileBindings="True" - x:DataType="viewModels:AvatarProfileViewModel" - Focusable="True"> - <Design.DataContext> - <viewModels:AvatarProfileViewModel /> - </Design.DataContext> - <UserControl.Resources> - <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> - </UserControl.Resources> - <Grid Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="*" /> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - </Grid.RowDefinitions> - <ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Height="400" - Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center"> - <ListBox.ItemsPanel> - <ItemsPanelTemplate> - <WrapPanel Orientation="Horizontal" MaxWidth="700" Margin="0" HorizontalAlignment="Center" /> - </ItemsPanelTemplate> - </ListBox.ItemsPanel> - <ListBox.ItemTemplate> - <DataTemplate> - <Image Margin="5" Height="96" Width="96" - Source="{Binding Data, Converter={StaticResource ByteImage}}" /> - </DataTemplate> - </ListBox.ItemTemplate> - </ListBox> - <ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5" - Maximum="{Binding ImageCount}" Minimum="0" /> - <StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center"> - <Button Content="{locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" /> - <ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" /> - <Button HorizontalAlignment="Right" Content="{locale:Locale Discard}" Click="CloseButton_OnClick" - Name="CloseButton" - Width="200" /> - </StackPanel> - </Grid> -</UserControl> \ No newline at end of file