mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2025-03-14 22:40:18 +00:00
Redesign User Profile Manager
- Backported long selection bar in Grid/List view not working - Backported UserSelector is jank
This commit is contained in:
parent
d6d18d76da
commit
39f46c0b84
38 changed files with 1779 additions and 539 deletions
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Profilbild ändern",
|
"UserProfilesChangeProfileImage": "Profilbild ändern",
|
||||||
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
|
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
|
||||||
"UserProfilesAddNewProfile": "Neues Profil",
|
"UserProfilesAddNewProfile": "Neues Profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Profil löschen",
|
"UserProfilesDelete": "Löschen",
|
||||||
"UserProfilesClose": "Schließen",
|
"UserProfilesClose": "Schließen",
|
||||||
"ProfileImageSelectionTitle": "Auswahl des Profilbildes",
|
"ProfileImageSelectionTitle": "Auswahl des Profilbildes",
|
||||||
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus",
|
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
|
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
|
||||||
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
|
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
|
||||||
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
|
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
|
||||||
"UserProfilesDeleteSelectedProfile": "Διαγραφή Επιλεγμένου Προφίλ",
|
"UserProfilesDelete": "Διαγράφω",
|
||||||
"UserProfilesClose": "Κλείσιμο",
|
"UserProfilesClose": "Κλείσιμο",
|
||||||
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
|
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
|
||||||
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",
|
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Change Profile Image",
|
"UserProfilesChangeProfileImage": "Change Profile Image",
|
||||||
"UserProfilesAvailableUserProfiles": "Available User Profiles:",
|
"UserProfilesAvailableUserProfiles": "Available User Profiles:",
|
||||||
"UserProfilesAddNewProfile": "Create Profile",
|
"UserProfilesAddNewProfile": "Create Profile",
|
||||||
"UserProfilesDeleteSelectedProfile": "Delete Selected",
|
"UserProfilesDelete": "Delete",
|
||||||
"UserProfilesClose": "Close",
|
"UserProfilesClose": "Close",
|
||||||
"ProfileImageSelectionTitle": "Profile Image Selection",
|
"ProfileImageSelectionTitle": "Profile Image Selection",
|
||||||
"ProfileImageSelectionHeader": "Choose a profile Image",
|
"ProfileImageSelectionHeader": "Choose a profile Image",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
|
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
|
||||||
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
|
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
|
||||||
"UserProfilesAddNewProfile": "Añadir nuevo perfil",
|
"UserProfilesAddNewProfile": "Añadir nuevo perfil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Eliminar perfil seleccionado",
|
"UserProfilesDelete": "Eliminar",
|
||||||
"UserProfilesClose": "Cerrar",
|
"UserProfilesClose": "Cerrar",
|
||||||
"ProfileImageSelectionTitle": "Selección de imagen de perfil",
|
"ProfileImageSelectionTitle": "Selección de imagen de perfil",
|
||||||
"ProfileImageSelectionHeader": "Elige una imagen de perfil",
|
"ProfileImageSelectionHeader": "Elige una imagen de perfil",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Changer l'image du profil",
|
"UserProfilesChangeProfileImage": "Changer l'image du profil",
|
||||||
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
|
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
|
||||||
"UserProfilesAddNewProfile": "Ajouter un nouveau profil",
|
"UserProfilesAddNewProfile": "Ajouter un nouveau profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Supprimer le profil sélectionné",
|
"UserProfilesDelete": "Supprimer",
|
||||||
"UserProfilesClose": "Fermer",
|
"UserProfilesClose": "Fermer",
|
||||||
"ProfileImageSelectionTitle": "Sélection de l'image du profil",
|
"ProfileImageSelectionTitle": "Sélection de l'image du profil",
|
||||||
"ProfileImageSelectionHeader": "Choisir l'image du profil",
|
"ProfileImageSelectionHeader": "Choisir l'image du profil",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
|
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
|
||||||
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
|
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
|
||||||
"UserProfilesAddNewProfile": "プロファイルを作成",
|
"UserProfilesAddNewProfile": "プロファイルを作成",
|
||||||
"UserProfilesDeleteSelectedProfile": "削除",
|
"UserProfilesDelete": "削除",
|
||||||
"UserProfilesClose": "閉じる",
|
"UserProfilesClose": "閉じる",
|
||||||
"ProfileImageSelectionTitle": "プロファイル画像選択",
|
"ProfileImageSelectionTitle": "プロファイル画像選択",
|
||||||
"ProfileImageSelectionHeader": "プロファイル画像を選択",
|
"ProfileImageSelectionHeader": "プロファイル画像を選択",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
|
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
|
||||||
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
|
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
|
||||||
"UserProfilesAddNewProfile": "Utwórz Profil",
|
"UserProfilesAddNewProfile": "Utwórz Profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Usuń Zaznaczone",
|
"UserProfilesDelete": "Usuwać",
|
||||||
"UserProfilesClose": "Zamknij",
|
"UserProfilesClose": "Zamknij",
|
||||||
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
|
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
|
||||||
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",
|
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
|
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
|
||||||
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
|
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
|
||||||
"UserProfilesAddNewProfile": "Adicionar novo perfil",
|
"UserProfilesAddNewProfile": "Adicionar novo perfil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Apagar perfil selecionado",
|
"UserProfilesDelete": "Apagar",
|
||||||
"UserProfilesClose": "Fechar",
|
"UserProfilesClose": "Fechar",
|
||||||
"ProfileImageSelectionTitle": "Seleção da imagem de perfil",
|
"ProfileImageSelectionTitle": "Seleção da imagem de perfil",
|
||||||
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil",
|
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Изменить изображение профиля",
|
"UserProfilesChangeProfileImage": "Изменить изображение профиля",
|
||||||
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
|
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
|
||||||
"UserProfilesAddNewProfile": "Добавить новый профиль",
|
"UserProfilesAddNewProfile": "Добавить новый профиль",
|
||||||
"UserProfilesDeleteSelectedProfile": "Удалить выбранный профиль",
|
"UserProfilesDelete": "Удалить",
|
||||||
"UserProfilesClose": "Закрыть",
|
"UserProfilesClose": "Закрыть",
|
||||||
"ProfileImageSelectionTitle": "Выбор изображения профиля",
|
"ProfileImageSelectionTitle": "Выбор изображения профиля",
|
||||||
"ProfileImageSelectionHeader": "Выберите изображение профиля",
|
"ProfileImageSelectionHeader": "Выберите изображение профиля",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
|
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
|
||||||
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
|
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
|
||||||
"UserProfilesAddNewProfile": "Yeni Profil Ekle",
|
"UserProfilesAddNewProfile": "Yeni Profil Ekle",
|
||||||
"UserProfilesDeleteSelectedProfile": "Seçili Profili Sil",
|
"UserProfilesDelete": "Sil",
|
||||||
"UserProfilesClose": "Kapat",
|
"UserProfilesClose": "Kapat",
|
||||||
"ProfileImageSelectionTitle": "Profil Resmi Seçimi",
|
"ProfileImageSelectionTitle": "Profil Resmi Seçimi",
|
||||||
"ProfileImageSelectionHeader": "Profil Resmi Seç",
|
"ProfileImageSelectionHeader": "Profil Resmi Seç",
|
||||||
|
|
|
@ -259,7 +259,7 @@
|
||||||
"UserProfilesChangeProfileImage": "更換頭貼",
|
"UserProfilesChangeProfileImage": "更換頭貼",
|
||||||
"UserProfilesAvailableUserProfiles": "現有的帳號:",
|
"UserProfilesAvailableUserProfiles": "現有的帳號:",
|
||||||
"UserProfilesAddNewProfile": "建立帳號",
|
"UserProfilesAddNewProfile": "建立帳號",
|
||||||
"UserProfilesDeleteSelectedProfile": "刪除選擇的帳號",
|
"UserProfilesDelete": "刪除",
|
||||||
"UserProfilesClose": "關閉",
|
"UserProfilesClose": "關閉",
|
||||||
"ProfileImageSelectionTitle": "頭貼選擇",
|
"ProfileImageSelectionTitle": "頭貼選擇",
|
||||||
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片",
|
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片",
|
||||||
|
|
|
@ -237,6 +237,35 @@
|
||||||
<Style Selector="TextBox.NumberBoxTextBoxStyle">
|
<Style Selector="TextBox.NumberBoxTextBoxStyle">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
|
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
|
||||||
</Style>
|
</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>
|
<Styles.Resources>
|
||||||
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||||
<StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" />
|
<StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" />
|
||||||
|
|
|
@ -120,6 +120,18 @@
|
||||||
<DependentUpon>GameListView.axaml</DependentUpon>
|
<DependentUpon>GameListView.axaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserEditor.axaml.cs">
|
||||||
|
<DependentUpon>UserEditor.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserRecoverer.axaml.cs">
|
||||||
|
<DependentUpon>UserRecoverer.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserSelector.axaml.cs">
|
||||||
|
<DependentUpon>UserSelector.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -112,32 +112,11 @@
|
||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.Styles>
|
<ListBox.Styles>
|
||||||
<Style Selector="ListBoxItem">
|
<Style Selector="ListBoxItem">
|
||||||
<Setter Property="Padding" Value="0" />
|
|
||||||
<Setter Property="Margin" Value="5" />
|
<Setter Property="Margin" Value="5" />
|
||||||
<Setter Property="CornerRadius" Value="4" />
|
<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>
|
||||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.Styles>
|
</ListBox.Styles>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
|
|
|
@ -111,37 +111,8 @@
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.Styles>
|
<ListBox.Styles>
|
||||||
<Style Selector="ListBoxItem">
|
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
|
||||||
<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:selected /template/ Border#SelectionIndicator">
|
|
||||||
<Setter Property="MinHeight" Value="100" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.Styles>
|
</ListBox.Styles>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
|
|
|
@ -12,5 +12,6 @@
|
||||||
<ui:Frame
|
<ui:Frame
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
x:Name="ContentFrame" />
|
x:Name="ContentFrame">
|
||||||
|
</ui:Frame>
|
||||||
</UserControl>
|
</UserControl>
|
|
@ -1,13 +1,24 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using LibHac;
|
using LibHac;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.Ava.UI.Views.User;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
|
@ -31,14 +42,14 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
HorizonClient = horizonClient;
|
HorizonClient = horizonClient;
|
||||||
ViewModel = new UserProfileViewModel(this);
|
ViewModel = new UserProfileViewModel();
|
||||||
|
LoadProfiles();
|
||||||
|
|
||||||
if (contentManager.GetCurrentFirmwareVersion() != null)
|
if (contentManager.GetCurrentFirmwareVersion() != null)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
@ -51,7 +62,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
ContentFrame.GoBack();
|
ContentFrame.GoBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewModel.LoadProfiles();
|
LoadProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Navigate(Type sourcePageType, object parameter)
|
public void Navigate(Type sourcePageType, object parameter)
|
||||||
|
@ -78,6 +89,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
content.ViewModel.Dispose();
|
content.ViewModel.Dispose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Style closeButton = new(x => x.Name("CloseButton"));
|
||||||
|
closeButton.Setters.Add(new Setter(WidthProperty, 70d));
|
||||||
|
|
||||||
|
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||||
|
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right));
|
||||||
|
|
||||||
|
contentDialog.Styles.Add(closeButton);
|
||||||
|
contentDialog.Styles.Add(closeButtonParent);
|
||||||
|
|
||||||
await contentDialog.ShowAsync();
|
await contentDialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,5 +107,125 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
Navigate(typeof(UserSelector), this);
|
Navigate(typeof(UserSelector), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void LoadProfiles()
|
||||||
|
{
|
||||||
|
ViewModel.Profiles.Clear();
|
||||||
|
ViewModel.LostProfiles.Clear();
|
||||||
|
|
||||||
|
var profiles = AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||||
|
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
ViewModel.Profiles.Add(new UserProfile(profile, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewModel.SelectedProfile = ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault(x => x.UserId == AccountManager.LastOpenedUser.UserId);
|
||||||
|
|
||||||
|
if (ViewModel.SelectedProfile == null)
|
||||||
|
{
|
||||||
|
ViewModel.SelectedProfile = (UserProfile)ViewModel.Profiles.First();
|
||||||
|
|
||||||
|
if (ViewModel.SelectedProfile != null)
|
||||||
|
{
|
||||||
|
AccountManager.OpenUser(ViewModel.SelectedProfile.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AddModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
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["DialogUserProfileDeletionWarningMessage"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(Action);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.OpenUser(profile.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DialogUserProfileDeletionConfirmMessage"], "",
|
||||||
|
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
|
||||||
|
|
||||||
|
if (result == UserResult.Yes)
|
||||||
|
{
|
||||||
|
GoBack();
|
||||||
|
AccountManager.DeleteUser(userProfile.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUser()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserEditor), (this, (UserProfile)null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditUser(UserProfile userProfile)
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserEditor), (this, userProfile, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecoverLostAccounts()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserRecoverer), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ManageSaves()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserSaveManager), (this, AccountManager, HorizonClient, VirtualFileSystem));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
8
Ryujinx.Ava/UI/Models/AddModel.cs
Normal file
8
Ryujinx.Ava/UI/Models/AddModel.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Models;
|
||||||
|
|
||||||
|
public class AddModel : BaseModel
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
using Ryujinx.Ava.UI.Controls;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.Ava.UI.Views.User;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
||||||
|
using UserEditor = Ryujinx.Ava.UI.Views.User.UserEditor;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
{
|
{
|
||||||
|
@ -12,6 +14,8 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
private byte[] _image;
|
private byte[] _image;
|
||||||
private string _name;
|
private string _name;
|
||||||
private UserId _userId;
|
private UserId _userId;
|
||||||
|
private bool _isPointerOver;
|
||||||
|
public uint MaxProfileNameLength => 0x20;
|
||||||
|
|
||||||
public byte[] Image
|
public byte[] Image
|
||||||
{
|
{
|
||||||
|
@ -43,6 +47,16 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsPointerOver
|
||||||
|
{
|
||||||
|
get => _isPointerOver;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isPointerOver = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public UserProfile(Profile profile, NavigationDialogHost owner)
|
public UserProfile(Profile profile, NavigationDialogHost owner)
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
|
|
|
@ -695,6 +695,62 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3;
|
public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3;
|
||||||
public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4;
|
public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4;
|
||||||
|
|
||||||
|
public int ListItemSelectorSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsGridSmall)
|
||||||
|
{
|
||||||
|
return 78;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridMedium)
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridLarge)
|
||||||
|
{
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridHuge)
|
||||||
|
{
|
||||||
|
return 140;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GridItemSelectorSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsGridSmall)
|
||||||
|
{
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridMedium)
|
||||||
|
{
|
||||||
|
return 150;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridLarge)
|
||||||
|
{
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGridHuge)
|
||||||
|
{
|
||||||
|
return 220;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int GridSizeScale
|
public int GridSizeScale
|
||||||
{
|
{
|
||||||
get => ConfigurationState.Instance.Ui.GridSize;
|
get => ConfigurationState.Instance.Ui.GridSize;
|
||||||
|
@ -712,6 +768,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
OnPropertyChanged(nameof(IsGridMedium));
|
OnPropertyChanged(nameof(IsGridMedium));
|
||||||
OnPropertyChanged(nameof(IsGridLarge));
|
OnPropertyChanged(nameof(IsGridLarge));
|
||||||
OnPropertyChanged(nameof(IsGridHuge));
|
OnPropertyChanged(nameof(IsGridHuge));
|
||||||
|
OnPropertyChanged(nameof(ListItemSelectorSize));
|
||||||
|
OnPropertyChanged(nameof(GridItemSelectorSize));
|
||||||
OnPropertyChanged(nameof(ShowNames));
|
OnPropertyChanged(nameof(ShowNames));
|
||||||
|
|
||||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||||
|
|
378
Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Normal file
378
Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
using Avalonia.Media;
|
||||||
|
using DynamicData;
|
||||||
|
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.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Color = Avalonia.Media.Color;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
internal class UserFirmwareAvatarSelectorViewModel : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
private const int MaxImageTasks = 4;
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
||||||
|
private static bool _isPreloading;
|
||||||
|
private static Action _loadCompleteAction;
|
||||||
|
|
||||||
|
private ObservableCollection<ProfileImageModel> _images;
|
||||||
|
private Color _backgroundColor = Colors.White;
|
||||||
|
|
||||||
|
private int _selectedIndex;
|
||||||
|
private int _imagesLoaded;
|
||||||
|
private bool _isActive;
|
||||||
|
private byte[] _selectedImage;
|
||||||
|
private bool _isIndeterminate = true;
|
||||||
|
private bool _isVisible;
|
||||||
|
|
||||||
|
public bool IsActive
|
||||||
|
{
|
||||||
|
get => _isActive;
|
||||||
|
set => _isActive = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserFirmwareAvatarSelectorViewModel()
|
||||||
|
{
|
||||||
|
_images = new ObservableCollection<ProfileImageModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserFirmwareAvatarSelectorViewModel(Action loadCompleteAction)
|
||||||
|
{
|
||||||
|
_images = new ObservableCollection<ProfileImageModel>();
|
||||||
|
|
||||||
|
if (_isPreloading)
|
||||||
|
{
|
||||||
|
_loadCompleteAction = loadCompleteAction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReloadImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color BackgroundColor
|
||||||
|
{
|
||||||
|
get => _backgroundColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
|
||||||
|
IsActive = false;
|
||||||
|
|
||||||
|
ReloadImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ProfileImageModel> Images
|
||||||
|
{
|
||||||
|
get => _images;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_images = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIndeterminate
|
||||||
|
{
|
||||||
|
get => _isIndeterminate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isIndeterminate = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ImageCount => _avatarStore.Count;
|
||||||
|
|
||||||
|
public int ImagesLoaded
|
||||||
|
{
|
||||||
|
get => _imagesLoaded;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_imagesLoaded = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVisible
|
||||||
|
{
|
||||||
|
get => _isVisible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isVisible = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadImages()
|
||||||
|
{
|
||||||
|
if (_isPreloading)
|
||||||
|
{
|
||||||
|
IsIndeterminate = false;
|
||||||
|
IsVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
IsActive = true;
|
||||||
|
|
||||||
|
Images.Clear();
|
||||||
|
int selectedIndex = _selectedIndex;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
ImagesLoaded = 0;
|
||||||
|
IsIndeterminate = false;
|
||||||
|
IsVisible = true;
|
||||||
|
|
||||||
|
var keys = _avatarStore.Keys.ToList();
|
||||||
|
|
||||||
|
var newImages = new List<ProfileImageModel>();
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
for (int i = 0; i < MaxImageTasks; i++)
|
||||||
|
{
|
||||||
|
var start = i;
|
||||||
|
tasks.Add(Task.Run(() => ImageTask(start)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks.ToArray());
|
||||||
|
|
||||||
|
Images.AddRange(newImages);
|
||||||
|
|
||||||
|
IsVisible = false;
|
||||||
|
|
||||||
|
void ImageTask(int start)
|
||||||
|
{
|
||||||
|
for (int i = start; i < keys.Count; i += MaxImageTasks)
|
||||||
|
{
|
||||||
|
if (!IsActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = keys[i];
|
||||||
|
var image = _avatarStore[keys[i]];
|
||||||
|
|
||||||
|
var data = ProcessImage(image);
|
||||||
|
newImages.Add(new ProfileImageModel(key, data));
|
||||||
|
if (index++ == selectedIndex)
|
||||||
|
{
|
||||||
|
SelectedImage = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _imagesLoaded);
|
||||||
|
OnPropertyChanged(nameof(ImagesLoaded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ProcessImage(byte[] data)
|
||||||
|
{
|
||||||
|
using (MemoryStream streamJpg = new())
|
||||||
|
{
|
||||||
|
Image avatarImage = Image.Load(data, new PngDecoder());
|
||||||
|
|
||||||
|
avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(BackgroundColor.R,
|
||||||
|
BackgroundColor.G,
|
||||||
|
BackgroundColor.B,
|
||||||
|
BackgroundColor.A)));
|
||||||
|
avatarImage.SaveAsJpeg(streamJpg);
|
||||||
|
|
||||||
|
return streamJpg.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_avatarStore.Count > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isPreloading = true;
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isPreloading = false;
|
||||||
|
_loadCompleteAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loadCompleteAction = null;
|
||||||
|
IsActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
internal class UserProfileImageSelectorViewModel : BaseModel
|
||||||
|
{
|
||||||
|
private bool _firmwareFound;
|
||||||
|
|
||||||
|
public bool FirmwareFound
|
||||||
|
{
|
||||||
|
get => _firmwareFound;
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_firmwareFound = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,43 +1,20 @@
|
||||||
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 System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
|
||||||
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
|
|
||||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
public class UserProfileViewModel : BaseModel, IDisposable
|
public class UserProfileViewModel : BaseModel, IDisposable
|
||||||
{
|
{
|
||||||
private readonly NavigationDialogHost _owner;
|
|
||||||
|
|
||||||
private UserProfile _selectedProfile;
|
private UserProfile _selectedProfile;
|
||||||
private UserProfile _highlightedProfile;
|
|
||||||
|
|
||||||
public UserProfileViewModel()
|
public UserProfileViewModel()
|
||||||
{
|
{
|
||||||
Profiles = new ObservableCollection<UserProfile>();
|
Profiles = new ObservableCollection<BaseModel>();
|
||||||
LostProfiles = new ObservableCollection<UserProfile>();
|
LostProfiles = new ObservableCollection<UserProfile>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserProfileViewModel(NavigationDialogHost owner) : this()
|
public ObservableCollection<BaseModel> Profiles { get; set; }
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
|
|
||||||
LoadProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<UserProfile> Profiles { get; set; }
|
|
||||||
|
|
||||||
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||||
|
|
||||||
|
@ -49,167 +26,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
_selectedProfile = value;
|
_selectedProfile = value;
|
||||||
|
|
||||||
OnPropertyChanged();
|
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 void Dispose() { }
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<UserControl
|
<UserControl
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.UserEditor"
|
x:Class="Ryujinx.Ava.UI.Views.User.UserEditor"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
@ -23,35 +23,9 @@
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</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
|
<StackPanel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="0"
|
||||||
Margin="5,10"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
Spacing="10">
|
Spacing="10">
|
||||||
|
@ -65,13 +39,36 @@
|
||||||
<TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
|
<TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
|
||||||
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<Panel
|
||||||
|
Background="{DynamicResource AppListBackgroundColor}">
|
||||||
|
<Image
|
||||||
|
Name="ProfileImage"
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
|
Margin="0 10 0 0"
|
||||||
Spacing="10">
|
Spacing="10">
|
||||||
|
<Button
|
||||||
|
Name="DeleteButton"
|
||||||
|
Click="DeleteButton_Click"
|
||||||
|
Content="{locale:Locale UserProfilesDelete}" />
|
||||||
<Button
|
<Button
|
||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
Click="SaveButton_Click"
|
Click="SaveButton_Click"
|
||||||
|
@ -80,7 +77,15 @@
|
||||||
Name="CloseButton"
|
Name="CloseButton"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Click="CloseButton_Click"
|
Click="CloseButton_Click"
|
||||||
Content="{locale:Locale Discard}" />
|
Content="{locale:Locale Cancel}" />
|
||||||
|
<Button
|
||||||
|
Name="ChangePictureButton"
|
||||||
|
Click="ChangePictureButton_Click"
|
||||||
|
Content="{locale:Locale UserProfilesChangeProfileImage}" />
|
||||||
|
<Button
|
||||||
|
Name="AddPictureButton"
|
||||||
|
Click="ChangePictureButton_Click"
|
||||||
|
Content="{locale:Locale UserProfilesSetProfileImage}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
|
@ -4,11 +4,13 @@ using Avalonia.Interactivity;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using FluentAvalonia.UI.Navigation;
|
using FluentAvalonia.UI.Navigation;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
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 UserEditor : UserControl
|
||||||
{
|
{
|
||||||
|
@ -18,6 +20,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
public TempProfile TempProfile { get; set; }
|
public TempProfile TempProfile { get; set; }
|
||||||
public uint MaxProfileNameLength => 0x20;
|
public uint MaxProfileNameLength => 0x20;
|
||||||
|
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
|
||||||
|
|
||||||
public UserEditor()
|
public UserEditor()
|
||||||
{
|
{
|
||||||
|
@ -47,9 +50,17 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
DataContext = TempProfile;
|
DataContext = TempProfile;
|
||||||
|
|
||||||
AddPictureButton.IsVisible = _isNewUser;
|
AddPictureButton.IsVisible = _isNewUser;
|
||||||
|
ChangePictureButton.IsVisible = !_isNewUser;
|
||||||
IdLabel.IsVisible = _profile != null;
|
IdLabel.IsVisible = _profile != null;
|
||||||
IdText.IsVisible = _profile != null;
|
IdText.IsVisible = _profile != null;
|
||||||
ChangePictureButton.IsVisible = !_isNewUser;
|
if (!_isNewUser && IsDeletable)
|
||||||
|
{
|
||||||
|
DeleteButton.IsVisible = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DeleteButton.IsVisible = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +69,11 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
_parent?.GoBack();
|
_parent?.GoBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_parent.DeleteUser(_profile);
|
||||||
|
}
|
||||||
|
|
||||||
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DataValidationErrors.ClearErrors(NameBox);
|
DataValidationErrors.ClearErrors(NameBox);
|
||||||
|
@ -104,7 +120,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
public void SelectProfileImage()
|
public void SelectProfileImage()
|
||||||
{
|
{
|
||||||
_parent.Navigate(typeof(ProfileImageSelectionDialog), (_parent, TempProfile));
|
_parent.Navigate(typeof(UserProfileImageSelector), (_parent, TempProfile));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
|
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
|
97
Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelector.axaml
Normal file
97
Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelector.axaml
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<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" d:DesignWidth="400" d:DesignHeight="350"
|
||||||
|
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelector"
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
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"
|
||||||
|
MaxWidth="700"
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Margin" Value="5" />
|
||||||
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Image
|
||||||
|
Margin="10"
|
||||||
|
Height="96"
|
||||||
|
Width="96"
|
||||||
|
Source="{Binding Data,
|
||||||
|
Converter={StaticResource ByteImage}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
<ProgressBar
|
||||||
|
Grid.Row="2"
|
||||||
|
IsIndeterminate="{Binding IsIndeterminate}"
|
||||||
|
IsVisible="{Binding IsVisible}"
|
||||||
|
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 Cancel}"
|
||||||
|
Click="CloseButton_OnClick"
|
||||||
|
Name="CloseButton"
|
||||||
|
Width="200" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
|
@ -0,0 +1,77 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using FluentAvalonia.UI.Navigation;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Views.User
|
||||||
|
{
|
||||||
|
public partial class UserFirmwareAvatarSelector : UserControl
|
||||||
|
{
|
||||||
|
private NavigationDialogHost _parent;
|
||||||
|
private TempProfile _profile;
|
||||||
|
|
||||||
|
public UserFirmwareAvatarSelector(ContentManager contentManager)
|
||||||
|
{
|
||||||
|
ContentManager = contentManager;
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserFirmwareAvatarSelector()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||||
|
{
|
||||||
|
NavigatedTo(e);
|
||||||
|
}, RoutingStrategies.Direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigatedTo(NavigationEventArgs arg)
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
if (arg.NavigationMode == NavigationMode.New)
|
||||||
|
{
|
||||||
|
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||||
|
ContentManager = _parent.ContentManager;
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
ViewModel = new UserFirmwareAvatarSelectorViewModel(() => ViewModel.ReloadImages());
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentManager ContentManager { get; private set; }
|
||||||
|
|
||||||
|
internal UserFirmwareAvatarSelectorViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.Dispose();
|
||||||
|
|
||||||
|
_parent.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel.SelectedIndex > -1)
|
||||||
|
{
|
||||||
|
_profile.Image = ViewModel.SelectedImage;
|
||||||
|
|
||||||
|
ViewModel.Dispose();
|
||||||
|
|
||||||
|
_parent.GoBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
Ryujinx.Ava/UI/Views/User/UserProfileImageSelector.axaml
Normal file
64
Ryujinx.Ava/UI/Views/User/UserProfileImageSelector.axaml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<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"
|
||||||
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelector"
|
||||||
|
Focusable="True"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="viewModles:UserProfileImageSelectorViewModel">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModles:UserProfileImageSelectorViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<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">
|
||||||
|
<TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="SelectFirmwareImage"
|
||||||
|
IsEnabled="{Binding FirmwareFound}"
|
||||||
|
Click="SelectFirmwareImage_OnClick">
|
||||||
|
<TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="Cancel"
|
||||||
|
Click="Cancel_OnClick">
|
||||||
|
<TextBlock Text="{locale:Locale Cancel}" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
116
Ryujinx.Ava/UI/Views/User/UserProfileImageSelector.axaml.cs
Normal file
116
Ryujinx.Ava/UI/Views/User/UserProfileImageSelector.axaml.cs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using FluentAvalonia.UI.Navigation;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System.IO;
|
||||||
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Views.User
|
||||||
|
{
|
||||||
|
public partial class UserProfileImageSelector : UserControl
|
||||||
|
{
|
||||||
|
private ContentManager _contentManager;
|
||||||
|
private NavigationDialogHost _parent;
|
||||||
|
private TempProfile _profile;
|
||||||
|
|
||||||
|
internal UserProfileImageSelectorViewModel ViewModel { get; private set; }
|
||||||
|
|
||||||
|
public UserProfileImageSelector()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||||
|
{
|
||||||
|
NavigatedTo(e);
|
||||||
|
}, RoutingStrategies.Direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigatedTo(NavigationEventArgs arg)
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
switch (arg.NavigationMode)
|
||||||
|
{
|
||||||
|
case NavigationMode.New:
|
||||||
|
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||||
|
_contentManager = _parent.ContentManager;
|
||||||
|
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
DataContext = ViewModel = new UserProfileImageSelectorViewModel();
|
||||||
|
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case NavigationMode.Back:
|
||||||
|
_parent.GoBack();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new();
|
||||||
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = LocaleManager.Instance["AllSupportedFormats"],
|
||||||
|
Extensions = { "jpg", "jpeg", "png", "bmp" }
|
||||||
|
});
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } });
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } });
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } });
|
||||||
|
|
||||||
|
dialog.AllowMultiple = false;
|
||||||
|
|
||||||
|
string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window);
|
||||||
|
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
if (image.Length > 0)
|
||||||
|
{
|
||||||
|
string imageFile = image[0];
|
||||||
|
|
||||||
|
_profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
_parent.GoBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_parent.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel.FirmwareFound)
|
||||||
|
{
|
||||||
|
_parent.Navigate(typeof(UserFirmwareAvatarSelector), (_parent, _profile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||||
|
{
|
||||||
|
using (Image image = Image.Load(buffer))
|
||||||
|
{
|
||||||
|
image.Mutate(x => x.Resize(256, 256));
|
||||||
|
|
||||||
|
using (MemoryStream streamJpg = new())
|
||||||
|
{
|
||||||
|
image.SaveAsJpeg(streamJpg);
|
||||||
|
|
||||||
|
return streamJpg.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,15 +3,17 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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"
|
mc:Ignorable="d"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
MinWidth="500"
|
MinWidth="500"
|
||||||
MinHeight="400"
|
MinHeight="400"
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer"
|
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.UserRecoverer"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="viewModels:UserProfileViewModel"
|
||||||
Focusable="True">
|
Focusable="True">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:UserProfileViewModel />
|
<viewModels:UserProfileViewModel />
|
||||||
|
@ -19,24 +21,27 @@
|
||||||
<Grid HorizontalAlignment="Stretch"
|
<Grid HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Button Grid.Row="0"
|
<StackPanel
|
||||||
Margin="5"
|
Grid.Row="0"
|
||||||
|
Margin="0 0 0 5"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
Height="30"
|
Height="30"
|
||||||
Width="50"
|
Width="50"
|
||||||
MinWidth="50"
|
MinWidth="50"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Command="{Binding GoBack}">
|
Click="GoBack">
|
||||||
<ui:SymbolIcon Symbol="Back"/>
|
<ui:SymbolIcon Symbol="Back"/>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock
|
||||||
|
Margin="10"
|
||||||
Text="{locale:Locale UserProfilesRecoverHeading}"/>
|
Text="{locale:Locale UserProfilesRecoverHeading}"/>
|
||||||
|
</StackPanel>
|
||||||
<ListBox
|
<ListBox
|
||||||
Margin="5"
|
Grid.Row="1"
|
||||||
Grid.Row="2"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Items="{Binding LostProfiles}">
|
Items="{Binding LostProfiles}">
|
||||||
|
@ -60,7 +65,7 @@
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Command="{Binding Recover}"
|
Click="Recover"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="{locale:Locale Recover}"/>
|
Content="{locale:Locale Recover}"/>
|
||||||
</Grid>
|
</Grid>
|
|
@ -1,17 +1,13 @@
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using FluentAvalonia.UI.Navigation;
|
using FluentAvalonia.UI.Navigation;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Views.User
|
||||||
{
|
{
|
||||||
public partial class UserRecoverer : UserControl
|
public partial class UserRecoverer : UserControl
|
||||||
{
|
{
|
||||||
private UserProfileViewModel _viewModel;
|
|
||||||
private NavigationDialogHost _parent;
|
private NavigationDialogHost _parent;
|
||||||
|
|
||||||
public UserRecoverer()
|
public UserRecoverer()
|
||||||
|
@ -30,15 +26,22 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
switch (arg.NavigationMode)
|
switch (arg.NavigationMode)
|
||||||
{
|
{
|
||||||
case NavigationMode.New:
|
case NavigationMode.New:
|
||||||
var args = ((NavigationDialogHost parent, UserProfileViewModel viewModel))arg.Parameter;
|
var parent = (NavigationDialogHost)arg.Parameter;
|
||||||
|
|
||||||
_viewModel = args.viewModel;
|
_parent = parent;
|
||||||
_parent = args.parent;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = _viewModel;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GoBack(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_parent?.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Recover(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_parent?.RecoverLostAccounts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
185
Ryujinx.Ava/UI/Views/User/UserSaveManager.axaml
Normal file
185
Ryujinx.Ava/UI/Views/User/UserSaveManager.axaml
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
<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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
Height="400"
|
||||||
|
Width="550"
|
||||||
|
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManager"
|
||||||
|
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">
|
||||||
|
<Button
|
||||||
|
Height="30"
|
||||||
|
Width="50"
|
||||||
|
MinWidth="50"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Click="GoBack">
|
||||||
|
<ui:SymbolIcon Symbol="Back"/>
|
||||||
|
</Button>
|
||||||
|
<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 Descending}" />
|
||||||
|
</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.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">
|
||||||
|
<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}"
|
||||||
|
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>
|
184
Ryujinx.Ava/UI/Views/User/UserSaveManager.axaml.cs
Normal file
184
Ryujinx.Ava/UI/Views/User/UserSaveManager.axaml.cs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using DynamicData;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using FluentAvalonia.UI.Navigation;
|
||||||
|
using LibHac;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserId = LibHac.Fs.UserId;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Views.User
|
||||||
|
{
|
||||||
|
public partial class UserSaveManager : UserControl
|
||||||
|
{
|
||||||
|
private AccountManager _accountManager;
|
||||||
|
private HorizonClient _horizonClient;
|
||||||
|
private VirtualFileSystem _virtualFileSystem;
|
||||||
|
private int _sortIndex;
|
||||||
|
private int _orderIndex;
|
||||||
|
private ObservableCollection<SaveModel> _view = new();
|
||||||
|
private string _search;
|
||||||
|
|
||||||
|
private NavigationDialogHost _parent;
|
||||||
|
|
||||||
|
public ObservableCollection<SaveModel> Saves { get; set; } = new();
|
||||||
|
|
||||||
|
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 UserSaveManager()
|
||||||
|
{
|
||||||
|
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 = this;
|
||||||
|
Task.Run(LoadSaves);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadSaves()
|
||||||
|
{
|
||||||
|
Saves.Clear();
|
||||||
|
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);
|
||||||
|
saveModel.DeleteAction = () => { Saves.Remove(saveModel); };
|
||||||
|
}
|
||||||
|
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoBack(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_parent?.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
159
Ryujinx.Ava/UI/Views/User/UserSelector.axaml
Normal file
159
Ryujinx.Ava/UI/Views/User/UserSelector.axaml
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.UI.Views.User.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: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>
|
||||||
|
<ListBox
|
||||||
|
MaxHeight="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
SelectionChanged="ProfilesList_SelectionChanged"
|
||||||
|
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="4" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Rectangle#SelectionIndicator">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
<ListBox.DataTemplates>
|
||||||
|
<DataTemplate
|
||||||
|
x:DataType="models:UserProfile">
|
||||||
|
<Grid
|
||||||
|
PointerEnter="Grid_PointerEntered"
|
||||||
|
PointerLeave="Grid_OnPointerExited">
|
||||||
|
<Border
|
||||||
|
Margin="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CornerRadius="5">
|
||||||
|
<StackPanel
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Spacing="5"
|
||||||
|
Margin="5">
|
||||||
|
<Image
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Width="96"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
MaxLines="2" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Width="10"
|
||||||
|
Height="10"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="LimeGreen"
|
||||||
|
CornerRadius="5"
|
||||||
|
IsVisible="{Binding IsOpened}" />
|
||||||
|
<Border
|
||||||
|
Height="30"
|
||||||
|
Width="30"
|
||||||
|
CornerRadius="15"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||||
|
IsVisible="{Binding IsPointerOver}">
|
||||||
|
<Button
|
||||||
|
MaxHeight="30"
|
||||||
|
MaxWidth="30"
|
||||||
|
MinHeight="30"
|
||||||
|
MinWidth="30"
|
||||||
|
CornerRadius="15"
|
||||||
|
Padding="0"
|
||||||
|
Click="EditUser">
|
||||||
|
<ui:SymbolIcon Symbol="Edit"/>
|
||||||
|
</Button>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate
|
||||||
|
x:DataType="models:AddModel">
|
||||||
|
<Panel
|
||||||
|
Height="131"
|
||||||
|
Width="110">
|
||||||
|
<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>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0 10 0 0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
Margin="10 0"
|
||||||
|
Click="ManageSaves"
|
||||||
|
Content="{locale:Locale UserProfilesManageSaves}" />
|
||||||
|
<Button
|
||||||
|
Click="RecoverLostAccounts"
|
||||||
|
Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
118
Ryujinx.Ava/UI/Views/User/UserSelector.axaml.cs
Normal file
118
Ryujinx.Ava/UI/Views/User/UserSelector.axaml.cs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using FluentAvalonia.UI.Navigation;
|
||||||
|
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 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 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)
|
||||||
|
{
|
||||||
|
ViewModel.SelectedProfile = userProfile;
|
||||||
|
|
||||||
|
_parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,7 +77,14 @@
|
||||||
<ListBox
|
<ListBox
|
||||||
Name="GameList"
|
Name="GameList"
|
||||||
MinHeight="250"
|
MinHeight="250"
|
||||||
Items="{Binding GameDirectories}" />
|
Items="{Binding GameDirectories}">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="10" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
</ListBox>
|
||||||
<Grid HorizontalAlignment="Stretch">
|
<Grid HorizontalAlignment="Stretch">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|
Loading…
Reference in a new issue