aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.Ava/Assets/Locales/de_DE.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/el_GR.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/en_US.json18
-rw-r--r--Ryujinx.Ava/Assets/Locales/es_ES.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/fr_FR.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/ja_JP.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/pl_PL.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/pt_BR.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/ru_RU.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/tr_TR.json2
-rw-r--r--Ryujinx.Ava/Assets/Locales/zh_TW.json2
-rw-r--r--Ryujinx.Ava/Assets/Styles/Styles.xaml32
-rw-r--r--Ryujinx.Ava/Program.cs2
-rw-r--r--Ryujinx.Ava/Ryujinx.Ava.csproj12
-rw-r--r--Ryujinx.Ava/UI/Controls/GameGridView.axaml24
-rw-r--r--Ryujinx.Ava/UI/Controls/GameListView.axaml29
-rw-r--r--Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml3
-rw-r--r--Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs139
-rw-r--r--Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml57
-rw-r--r--Ryujinx.Ava/UI/Controls/SaveManager.axaml175
-rw-r--r--Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs160
-rw-r--r--Ryujinx.Ava/UI/Controls/UserRecoverer.axaml72
-rw-r--r--Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs44
-rw-r--r--Ryujinx.Ava/UI/Controls/UserSelector.axaml145
-rw-r--r--Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs77
-rw-r--r--Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs5
-rw-r--r--Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs (renamed from Ryujinx.Ava/Helper/LoggerAdapter.cs)2
-rw-r--r--Ryujinx.Ava/UI/Helpers/MetalHelper.cs (renamed from Ryujinx.Ava/Helper/MetalHelper.cs)2
-rw-r--r--Ryujinx.Ava/UI/Models/ProfileImageModel.cs20
-rw-r--r--Ryujinx.Ava/UI/Models/SaveModel.cs24
-rw-r--r--Ryujinx.Ava/UI/Models/TempProfile.cs9
-rw-r--r--Ryujinx.Ava/UI/Models/UserProfile.cs42
-rw-r--r--Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs230
-rw-r--r--Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs18
-rw-r--r--Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs200
-rw-r--r--Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs123
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserEditorView.axaml (renamed from Ryujinx.Ava/UI/Controls/UserEditor.axaml)109
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs (renamed from Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs)81
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml114
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs (renamed from Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs)35
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml63
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs (renamed from Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs)43
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml83
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs51
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml199
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs148
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml165
-rw-r--r--Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs128
-rw-r--r--Ryujinx.Ava/UI/Windows/AvatarWindow.axaml54
49 files changed, 1787 insertions, 1170 deletions
diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json
index 671f369e..4d656bc9 100644
--- a/Ryujinx.Ava/Assets/Locales/de_DE.json
+++ b/Ryujinx.Ava/Assets/Locales/de_DE.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Profilbild ändern",
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
"UserProfilesAddNewProfile": "Neues Profil",
- "UserProfilesDeleteSelectedProfile": "Profil löschen",
+ "UserProfilesDelete": "Löschen",
"UserProfilesClose": "Schließen",
"ProfileImageSelectionTitle": "Auswahl des Profilbildes",
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus",
diff --git a/Ryujinx.Ava/Assets/Locales/el_GR.json b/Ryujinx.Ava/Assets/Locales/el_GR.json
index 5cd7a554..ca3be8b9 100644
--- a/Ryujinx.Ava/Assets/Locales/el_GR.json
+++ b/Ryujinx.Ava/Assets/Locales/el_GR.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
- "UserProfilesDeleteSelectedProfile": "Διαγραφή Επιλεγμένου Προφίλ",
+ "UserProfilesDelete": "Διαγράφω",
"UserProfilesClose": "Κλείσιμο",
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",
diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json
index 46203463..0c767871 100644
--- a/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -260,8 +260,9 @@
"UserProfilesChangeProfileImage": "Change Profile Image",
"UserProfilesAvailableUserProfiles": "Available User Profiles:",
"UserProfilesAddNewProfile": "Create Profile",
- "UserProfilesDeleteSelectedProfile": "Delete Selected",
+ "UserProfilesDelete": "Delete",
"UserProfilesClose": "Close",
+ "ProfileNameSelectionWatermark": "Choose a nickname",
"ProfileImageSelectionTitle": "Profile Image Selection",
"ProfileImageSelectionHeader": "Choose a profile Image",
"ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware",
@@ -273,7 +274,7 @@
"InputDialogAddNewProfileTitle": "Choose the Profile Name",
"InputDialogAddNewProfileHeader": "Please Enter a Profile Name",
"InputDialogAddNewProfileSubtext": "(Max Length: {0})",
- "AvatarChoose": "Choose",
+ "AvatarChoose": "Choose Avatar",
"AvatarSetBackgroundColor": "Set Background Color",
"AvatarClose": "Close",
"ControllerSettingsLoadProfileToolTip": "Load Profile",
@@ -368,6 +369,9 @@
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
"DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
"DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
+ "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes",
+ "DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.",
+ "DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?",
"DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.",
"DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?",
"DialogDlcLoadNcaErrorMessage": "{0}. Errored File: {1}",
@@ -584,7 +588,7 @@
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
"UserProfilesName": "Name:",
- "UserProfilesUserId": "User Id:",
+ "UserProfilesUserId": "User ID:",
"SettingsTabGraphicsBackend": "Graphics Backend",
"SettingsTabGraphicsBackendTooltip": "Graphics Backend to use",
"SettingsEnableTextureRecompression": "Enable Texture Recompression",
@@ -603,13 +607,15 @@
"UserProfilesManageSaves": "Manage Saves",
"DeleteUserSave": "Do you want to delete user save for this game?",
"IrreversibleActionNote": "This action is not reversible.",
- "SaveManagerHeading": "Manage Saves for {0}",
+ "SaveManagerHeading": "Manage Saves for {0} ({1})",
"SaveManagerTitle": "Save Manager",
"Name": "Name",
"Size": "Size",
"Search": "Search",
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
"Recover": "Recover",
- "UserProfilesRecoverHeading" : "Saves were found for the following accounts"
+ "UserProfilesRecoverHeading" : "Saves were found for the following accounts",
+ "UserProfilesRecoverEmptyList": "No profiles to recover",
+ "UserEditorTitle" : "Edit User",
+ "UserEditorTitleCreate" : "Create User"
}
-
diff --git a/Ryujinx.Ava/Assets/Locales/es_ES.json b/Ryujinx.Ava/Assets/Locales/es_ES.json
index 1922318d..660d62a1 100644
--- a/Ryujinx.Ava/Assets/Locales/es_ES.json
+++ b/Ryujinx.Ava/Assets/Locales/es_ES.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
"UserProfilesAddNewProfile": "Añadir nuevo perfil",
- "UserProfilesDeleteSelectedProfile": "Eliminar perfil seleccionado",
+ "UserProfilesDelete": "Eliminar",
"UserProfilesClose": "Cerrar",
"ProfileImageSelectionTitle": "Selección de imagen de perfil",
"ProfileImageSelectionHeader": "Elige una imagen de perfil",
diff --git a/Ryujinx.Ava/Assets/Locales/fr_FR.json b/Ryujinx.Ava/Assets/Locales/fr_FR.json
index 938d0cc7..71f32c6e 100644
--- a/Ryujinx.Ava/Assets/Locales/fr_FR.json
+++ b/Ryujinx.Ava/Assets/Locales/fr_FR.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Changer l'image du profil",
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
"UserProfilesAddNewProfile": "Ajouter un nouveau profil",
- "UserProfilesDeleteSelectedProfile": "Supprimer le profil sélectionné",
+ "UserProfilesDelete": "Supprimer",
"UserProfilesClose": "Fermer",
"ProfileImageSelectionTitle": "Sélection de l'image du profil",
"ProfileImageSelectionHeader": "Choisir l'image du profil",
diff --git a/Ryujinx.Ava/Assets/Locales/ja_JP.json b/Ryujinx.Ava/Assets/Locales/ja_JP.json
index c88477f9..b1e0a43b 100644
--- a/Ryujinx.Ava/Assets/Locales/ja_JP.json
+++ b/Ryujinx.Ava/Assets/Locales/ja_JP.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
"UserProfilesAddNewProfile": "プロファイルを作成",
- "UserProfilesDeleteSelectedProfile": "削除",
+ "UserProfilesDelete": "削除",
"UserProfilesClose": "閉じる",
"ProfileImageSelectionTitle": "プロファイル画像選択",
"ProfileImageSelectionHeader": "プロファイル画像を選択",
diff --git a/Ryujinx.Ava/Assets/Locales/pl_PL.json b/Ryujinx.Ava/Assets/Locales/pl_PL.json
index 3c1b541e..0cc0b4f9 100644
--- a/Ryujinx.Ava/Assets/Locales/pl_PL.json
+++ b/Ryujinx.Ava/Assets/Locales/pl_PL.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
"UserProfilesAddNewProfile": "Utwórz Profil",
- "UserProfilesDeleteSelectedProfile": "Usuń Zaznaczone",
+ "UserProfilesDelete": "Usuwać",
"UserProfilesClose": "Zamknij",
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",
diff --git a/Ryujinx.Ava/Assets/Locales/pt_BR.json b/Ryujinx.Ava/Assets/Locales/pt_BR.json
index 036b0a4b..ded6cf95 100644
--- a/Ryujinx.Ava/Assets/Locales/pt_BR.json
+++ b/Ryujinx.Ava/Assets/Locales/pt_BR.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
"UserProfilesAddNewProfile": "Adicionar novo perfil",
- "UserProfilesDeleteSelectedProfile": "Apagar perfil selecionado",
+ "UserProfilesDelete": "Apagar",
"UserProfilesClose": "Fechar",
"ProfileImageSelectionTitle": "Seleção da imagem de perfil",
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil",
diff --git a/Ryujinx.Ava/Assets/Locales/ru_RU.json b/Ryujinx.Ava/Assets/Locales/ru_RU.json
index b3ad82be..7b25f455 100644
--- a/Ryujinx.Ava/Assets/Locales/ru_RU.json
+++ b/Ryujinx.Ava/Assets/Locales/ru_RU.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Изменить изображение профиля",
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
"UserProfilesAddNewProfile": "Добавить новый профиль",
- "UserProfilesDeleteSelectedProfile": "Удалить выбранный профиль",
+ "UserProfilesDelete": "Удалить",
"UserProfilesClose": "Закрыть",
"ProfileImageSelectionTitle": "Выбор изображения профиля",
"ProfileImageSelectionHeader": "Выберите изображение профиля",
diff --git a/Ryujinx.Ava/Assets/Locales/tr_TR.json b/Ryujinx.Ava/Assets/Locales/tr_TR.json
index ae14cdaf..f277713b 100644
--- a/Ryujinx.Ava/Assets/Locales/tr_TR.json
+++ b/Ryujinx.Ava/Assets/Locales/tr_TR.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
"UserProfilesAddNewProfile": "Yeni Profil Ekle",
- "UserProfilesDeleteSelectedProfile": "Seçili Profili Sil",
+ "UserProfilesDelete": "Sil",
"UserProfilesClose": "Kapat",
"ProfileImageSelectionTitle": "Profil Resmi Seçimi",
"ProfileImageSelectionHeader": "Profil Resmi Seç",
diff --git a/Ryujinx.Ava/Assets/Locales/zh_TW.json b/Ryujinx.Ava/Assets/Locales/zh_TW.json
index 963c0a83..e6832995 100644
--- a/Ryujinx.Ava/Assets/Locales/zh_TW.json
+++ b/Ryujinx.Ava/Assets/Locales/zh_TW.json
@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "更換頭貼",
"UserProfilesAvailableUserProfiles": "現有的帳號:",
"UserProfilesAddNewProfile": "建立帳號",
- "UserProfilesDeleteSelectedProfile": "刪除選擇的帳號",
+ "UserProfilesDelete": "刪除",
"UserProfilesClose": "關閉",
"ProfileImageSelectionTitle": "頭貼選擇",
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片",
diff --git a/Ryujinx.Ava/Assets/Styles/Styles.xaml b/Ryujinx.Ava/Assets/Styles/Styles.xaml
index c5e760e8..fc4e9ddd 100644
--- a/Ryujinx.Ava/Assets/Styles/Styles.xaml
+++ b/Ryujinx.Ava/Assets/Styles/Styles.xaml
@@ -179,6 +179,9 @@
<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 010aff51..46e135a9 100644
--- a/Ryujinx.Ava/Program.cs
+++ b/Ryujinx.Ava/Program.cs
@@ -1,6 +1,6 @@
using Avalonia;
using Avalonia.Threading;
-using Ryujinx.Ava.UI.Helper;
+using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
diff --git a/Ryujinx.Ava/Ryujinx.Ava.csproj b/Ryujinx.Ava/Ryujinx.Ava.csproj
index 996817b9..88b60d0b 100644
--- a/Ryujinx.Ava/Ryujinx.Ava.csproj
+++ b/Ryujinx.Ava/Ryujinx.Ava.csproj
@@ -130,6 +130,18 @@
<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 c757f066..862bc6d3 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 9fb5497b..bb4e37b0 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 90720478..bf34b303 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 0c300267..6911a4d4 100644
--- a/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
+++ b/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
@@ -1,13 +1,25 @@
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using LibHac;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ava.UI.Views.User;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Controls
{
@@ -31,14 +43,14 @@ namespace Ryujinx.Ava.UI.Controls
ContentManager = contentManager;
VirtualFileSystem = virtualFileSystem;
HorizonClient = horizonClient;
- ViewModel = new UserProfileViewModel(this);
-
+ ViewModel = new UserProfileViewModel();
+ LoadProfiles();
if (contentManager.GetCurrentFirmwareVersion() != null)
{
Task.Run(() =>
{
- AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
+ UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem);
});
}
InitializeComponent();
@@ -51,7 +63,7 @@ namespace Ryujinx.Ava.UI.Controls
ContentFrame.GoBack();
}
- ViewModel.LoadProfiles();
+ LoadProfiles();
}
public void Navigate(Type sourcePageType, object parameter)
@@ -68,7 +80,7 @@ namespace Ryujinx.Ava.UI.Controls
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = "",
SecondaryButtonText = "",
- CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
+ CloseButtonText = "",
Content = content,
Padding = new Thickness(0)
};
@@ -78,6 +90,11 @@ namespace Ryujinx.Ava.UI.Controls
content.ViewModel.Dispose();
};
+ Style footer = new(x => x.Name("DialogSpace").Child().OfType<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 56f8152a..00000000
--- 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 64674b65..00000000
--- 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 9910481c..00000000
--- a/Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using Avalonia.Controls;
-using DynamicData;
-using DynamicData.Binding;
-using LibHac;
-using LibHac.Common;
-using LibHac.Fs;
-using LibHac.Fs.Shim;
-using Ryujinx.Ava.Common;
-using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Models;
-using Ryujinx.HLE.FileSystem;
-using Ryujinx.Ui.App.Common;
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Threading.Tasks;
-using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
-
-namespace Ryujinx.Ava.UI.Controls
-{
- public partial class SaveManager : UserControl
- {
- private readonly UserProfile _userProfile;
- private readonly HorizonClient _horizonClient;
- private readonly VirtualFileSystem _virtualFileSystem;
- private int _sortIndex;
- private int _orderIndex;
- private ObservableCollection<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 69f3d36a..00000000
--- 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 9f29fddb..00000000
--- 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 002d27a0..00000000
--- 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 bd8c561e..00000000
--- 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 bdeceaea..8247a89b 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
index c8f3fea1..ba251f60 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
index ea3477eb..5eb8660a 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 63da7b44..8aa19400 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 3c20741f..7096f9d7 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 2dd7a6c8..05e16632 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 869db661..e7cd5300 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 00000000..9d981128
--- /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 00000000..7261631c
--- /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 3f0a85c9..8f997efc 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 00000000..bd374350
--- /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
index 155f1cfe..7e55f25e 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>
@@ -24,34 +28,8 @@
<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
index 18bb8b22..fb33dcf8 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 00000000..d46fcefc
--- /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
index e060d65e..7c9191ab 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());
+
+ avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(
+ ViewModel.BackgroundColor.R,
+ ViewModel.BackgroundColor.G,
+ ViewModel.BackgroundColor.B,
+ ViewModel.BackgroundColor.A)));
+ avatarImage.SaveAsJpeg(streamJpg);
- ViewModel.Dispose();
+ _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 00000000..b9f51fdc
--- /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
index 46a2f507..18f76f80 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 00000000..62b5e184
--- /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 00000000..0c53e53d
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs
@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserRecovererView : UserControl
+ {
+ private NavigationDialogHost _parent;
+
+ public UserRecovererView()
+ {
+ InitializeComponent();
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ switch (arg.NavigationMode)
+ {
+ case NavigationMode.New:
+ var parent = (NavigationDialogHost)arg.Parameter;
+
+ _parent = parent;
+
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
+
+ break;
+ }
+ }
+ }
+
+ private void GoBack(object sender, RoutedEventArgs e)
+ {
+ _parent?.GoBack();
+ }
+
+ private void Recover(object sender, RoutedEventArgs e)
+ {
+ _parent?.RecoverLostAccounts();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml
new file mode 100644
index 00000000..cdf74d52
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml
@@ -0,0 +1,199 @@
+<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 00000000..9d955326
--- /dev/null
+++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
@@ -0,0 +1,148 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Navigation;
+using LibHac;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
+using Ryujinx.Ava.Common;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using UserId = LibHac.Fs.UserId;
+
+namespace Ryujinx.Ava.UI.Views.User
+{
+ public partial class UserSaveManagerView : UserControl
+ {
+ internal UserSaveManagerViewModel ViewModel { get; private set; }
+
+ private AccountManager _accountManager;
+ private HorizonClient _horizonClient;
+ private VirtualFileSystem _virtualFileSystem;
+ private NavigationDialogHost _parent;
+
+ public UserSaveManagerView()
+ {
+ InitializeComponent();
+ AddHandler(Frame.NavigatedToEvent, (s, e) =>
+ {
+ NavigatedTo(e);
+ }, RoutingStrategies.Direct);
+ }
+
+ private void NavigatedTo(NavigationEventArgs arg)
+ {
+ if (Program.PreviewerDetached)
+ {
+ switch (arg.NavigationMode)
+ {
+ case NavigationMode.New:
+ var args = ((NavigationDialogHost parent, AccountManager accountManager, HorizonClient client, VirtualFileSystem virtualFileSystem))arg.Parameter;
+ _accountManager = args.accountManager;
+ _horizonClient = args.client;
+ _virtualFileSystem = args.virtualFileSystem;
+
+ _parent = args.parent;
+ break;
+ }
+
+ DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager);
+ ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}";
+
+ Task.Run(LoadSaves);
+ }
+ }
+
+ public void LoadSaves()
+ {
+ ViewModel.Saves.Clear();
+ var saves = new ObservableCollection<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 00000000..9a6ba054
--- /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 00000000..aa89fea9
--- /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 1d30fff5..00000000
--- 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