diff options
| author | Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> | 2023-01-11 00:20:19 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-11 06:20:19 +0100 |
| commit | 934b5a64e5638ae5228acb52faf48efadefdea8d (patch) | |
| tree | cc65eab75c5a9a7c3438de3302ab1f6cbcec1599 /Ryujinx.Ava/UI/ViewModels | |
| parent | cee667b491f87c48546f348bad8c6f16cdf6d628 (diff) | |
Ava GUI: User Profile Manager + Other Fixes (#4166)
* Fix redundancies
* Add back elses
* Loading Screen fixes
* Redesign User Profile Manager
- Backported long selection bar in Grid/List view not working
- Backported UserSelector is jank
* Fix SelectionIndicator
* Fix DataType
* Fix SaveManager bug
* Remove debug log
* Load saves on UIThread
* Reduce UI thread blocking
* Fix locale keys
* Use block namespaces
* Fix close button width
* Make UserProfile ordering consistent
* Alphabetical order
* Adjust layout, remove green circle for blue selector
* Fix some inconsistencies
* Fix no inital selected profile
* Adjust appearance of edit button
* Adjust SaveManager
* Remove redundant warning dialog
* Make firmware avatar selector clearer
* View redesign again :hero_depressed:
* Consistency adjustments
* Adjust margins
* Make `UserProfileImageSelector` consistent
* Make `UserFirmwareAvatarSelector` consistent
* Fix long grid view selector
* Switch case
* Remove long selection bar
Handled in #4178
* Consistency
* Started dialog titles
* Fixes
* Remaining titles
* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml
Co-authored-by: Mary-nyan <thog@protonmail.com>
* Fix build
* Hide UserRecoverer if no LostProfiles are found
* UserEditor Avatar Placeholder
* Watermark + locale adjustment
* Border radius
* Remove unnecessary styles
* Fix firmware avatar image order
* Cleanup `ColorPickerButton`
* Make `UserId` copy/paste able
* Make `FirmwareAvatarSelector` 6 images wide
* Make selection bar better
* Unsaved changes dialogue
* Fix indentation
* Remove extra check
* Address suggestions
* Reorganise
- Remove unused views
- Rename views to match convention
- Fix weird namespacing
* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* UserRecovererView empty placeholder
* Update Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Models/UserProfile.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Remove AddModel
* Update Ryujinx.Ava/Assets/Locales/en_US.json
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Fix bug
Co-authored-by: Mary-nyan <thog@protonmail.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
Diffstat (limited to 'Ryujinx.Ava/UI/ViewModels')
4 files changed, 376 insertions, 195 deletions
diff --git a/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs new file mode 100644 index 00000000..9d981128 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs @@ -0,0 +1,230 @@ +using Avalonia.Media; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using Color = Avalonia.Media.Color; + +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserFirmwareAvatarSelectorViewModel : BaseModel + { + private static readonly Dictionary<string, byte[]> _avatarStore = new(); + + private ObservableCollection<ProfileImageModel> _images; + private Color _backgroundColor = Colors.White; + + private int _selectedIndex; + private byte[] _selectedImage; + + public UserFirmwareAvatarSelectorViewModel() + { + _images = new ObservableCollection<ProfileImageModel>(); + + LoadImagesFromStore(); + } + + public Color BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + OnPropertyChanged(); + ChangeImageBackground(); + } + } + + public ObservableCollection<ProfileImageModel> Images + { + get => _images; + set + { + _images = value; + OnPropertyChanged(); + } + } + + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + + if (_selectedIndex == -1) + { + SelectedImage = null; + } + else + { + SelectedImage = _images[_selectedIndex].Data; + } + + OnPropertyChanged(); + } + } + + public byte[] SelectedImage + { + get => _selectedImage; + private set => _selectedImage = value; + } + + private void LoadImagesFromStore() + { + Images.Clear(); + + foreach (var image in _avatarStore) + { + Images.Add(new ProfileImageModel(image.Key, image.Value)); + } + } + + private void ChangeImageBackground() + { + foreach (var image in Images) + { + image.BackgroundColor = new SolidColorBrush(BackgroundColor); + } + } + + public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem) + { + if (_avatarStore.Count > 0) + { + return; + } + + string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data); + string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath); + + if (!string.IsNullOrWhiteSpace(avatarPath)) + { + using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open)) + { + Nca nca = new(virtualFileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); + + foreach (DirectoryEntryEx item in romfs.EnumerateEntries()) + { + // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy. + if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs")) + { + using var file = new UniqueRef<IFile>(); + + romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using (MemoryStream stream = new()) + using (MemoryStream streamPng = new()) + { + file.Get.AsStream().CopyTo(stream); + + stream.Position = 0; + + Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256); + + avatarImage.SaveAsPng(streamPng); + + _avatarStore.Add(item.FullPath, streamPng.ToArray()); + } + } + } + } + } + } + + private static byte[] DecompressYaz0(Stream stream) + { + using (BinaryReader reader = new(stream)) + { + reader.ReadInt32(); // Magic + + uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); + + reader.ReadInt64(); // Padding + + byte[] input = new byte[stream.Length - stream.Position]; + stream.Read(input, 0, input.Length); + + uint inputOffset = 0; + + byte[] output = new byte[decodedLength]; + uint outputOffset = 0; + + ushort mask = 0; + byte header = 0; + + while (outputOffset < decodedLength) + { + if ((mask >>= 1) == 0) + { + header = input[inputOffset++]; + mask = 0x80; + } + + if ((header & mask) != 0) + { + if (outputOffset == output.Length) + { + break; + } + + output[outputOffset++] = input[inputOffset++]; + } + else + { + byte byte1 = input[inputOffset++]; + byte byte2 = input[inputOffset++]; + + uint dist = (uint)((byte1 & 0xF) << 8) | byte2; + uint position = outputOffset - (dist + 1); + + uint length = (uint)byte1 >> 4; + if (length == 0) + { + length = (uint)input[inputOffset++] + 0x12; + } + else + { + length += 2; + } + + uint gap = outputOffset - position; + uint nonOverlappingLength = length; + + if (nonOverlappingLength > gap) + { + nonOverlappingLength = gap; + } + + Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength); + outputOffset += nonOverlappingLength; + position += nonOverlappingLength; + length -= nonOverlappingLength; + + while (length-- > 0) + { + output[outputOffset++] = output[position++]; + } + } + } + + return output; + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs new file mode 100644 index 00000000..7261631c --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserProfileImageSelectorViewModel : BaseModel + { + private bool _firmwareFound; + + public bool FirmwareFound + { + get => _firmwareFound; + + set + { + _firmwareFound = value; + OnPropertyChanged(); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs index 3f0a85c9..8f997efc 100644 --- a/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs @@ -1,215 +1,25 @@ -using Avalonia; -using Avalonia.Threading; -using FluentAvalonia.UI.Controls; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Shim; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Controls; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.HLE.HOS.Services.Account.Acc; +using Microsoft.IdentityModel.Tokens; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; namespace Ryujinx.Ava.UI.ViewModels { public class UserProfileViewModel : BaseModel, IDisposable { - private readonly NavigationDialogHost _owner; - - private UserProfile _selectedProfile; - private UserProfile _highlightedProfile; - public UserProfileViewModel() { - Profiles = new ObservableCollection<UserProfile>(); + Profiles = new ObservableCollection<BaseModel>(); LostProfiles = new ObservableCollection<UserProfile>(); + IsEmpty = LostProfiles.IsNullOrEmpty(); } - public UserProfileViewModel(NavigationDialogHost owner) : this() - { - _owner = owner; - - LoadProfiles(); - } - - public ObservableCollection<UserProfile> Profiles { get; set; } + public ObservableCollection<BaseModel> Profiles { get; set; } public ObservableCollection<UserProfile> LostProfiles { get; set; } - public UserProfile SelectedProfile - { - get => _selectedProfile; - set - { - _selectedProfile = value; - - OnPropertyChanged(); - OnPropertyChanged(nameof(IsHighlightedProfileDeletable)); - OnPropertyChanged(nameof(IsHighlightedProfileEditable)); - } - } - - public bool IsHighlightedProfileEditable => _highlightedProfile != null; - - public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId; - - public UserProfile HighlightedProfile - { - get => _highlightedProfile; - set - { - _highlightedProfile = value; - - OnPropertyChanged(); - OnPropertyChanged(nameof(IsHighlightedProfileDeletable)); - OnPropertyChanged(nameof(IsHighlightedProfileEditable)); - } - } + public bool IsEmpty { get; set; } public void Dispose() { } - - public void LoadProfiles() - { - Profiles.Clear(); - LostProfiles.Clear(); - - var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open); - - foreach (var profile in profiles) - { - Profiles.Add(new UserProfile(profile, _owner)); - } - - SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId); - - if (SelectedProfile == null) - { - SelectedProfile = Profiles.First(); - - if (SelectedProfile != null) - { - _owner.AccountManager.OpenUser(_selectedProfile.UserId); - } - } - - var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, - default, saveDataId: default, index: default); - - using var saveDataIterator = new UniqueRef<SaveDataIterator>(); - - _owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); - - Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10]; - - HashSet<UserId> lostAccounts = new HashSet<UserId>(); - - while (true) - { - saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); - - if (readCount == 0) - { - break; - } - - for (int i = 0; i < readCount; i++) - { - var save = saveDataInfo[i]; - var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High); - if (Profiles.FirstOrDefault( x=> x.UserId == id) == null) - { - lostAccounts.Add(id); - } - } - } - - foreach(var account in lostAccounts) - { - LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner)); - } - } - - public void AddUser() - { - UserProfile userProfile = null; - - _owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true)); - } - - public async void ManageSaves() - { - UserProfile userProfile = _highlightedProfile ?? SelectedProfile; - - SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem); - - ContentDialog contentDialog = new ContentDialog - { - Title = string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], userProfile.Name), - PrimaryButtonText = "", - SecondaryButtonText = "", - CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], - Content = manager, - Padding = new Thickness(0) - }; - - await contentDialog.ShowAsync(); - } - - public void EditUser() - { - _owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false)); - } - - public async void DeleteUser() - { - if (_highlightedProfile != null) - { - var lastUserId = _owner.AccountManager.LastOpenedUser.UserId; - - if (_highlightedProfile.UserId == lastUserId) - { - // If we are deleting the currently open profile, then we must open something else before deleting. - var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId); - - if (profile == null) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); - }); - - return; - } - - _owner.AccountManager.OpenUser(profile.UserId); - } - - var result = - await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], "", - LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], ""); - - if (result == UserResult.Yes) - { - _owner.AccountManager.DeleteUser(_highlightedProfile.UserId); - } - } - - LoadProfiles(); - } - - public void GoBack() - { - _owner.GoBack(); - } - - public void RecoverLostAccounts() - { - _owner.Navigate(typeof(UserRecoverer), (this._owner, this)); - } } }
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs b/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs new file mode 100644 index 00000000..bd374350 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs @@ -0,0 +1,123 @@ +using DynamicData; +using DynamicData.Binding; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class UserSaveManagerViewModel : BaseModel + { + private int _sortIndex; + private int _orderIndex; + private string _search; + private ObservableCollection<SaveModel> _saves; + private ObservableCollection<SaveModel> _views; + private AccountManager _accountManager; + + public string SaveManagerHeading => + string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId); + + public int SortIndex + { + get => _sortIndex; + set + { + _sortIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public int OrderIndex + { + get => _orderIndex; + set + { + _orderIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection<SaveModel> Saves + { + get => _saves; + set + { + _saves = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection<SaveModel> Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public UserSaveManagerViewModel(AccountManager accountManager) + { + _accountManager = accountManager; + _saves = new ObservableCollection<SaveModel>(); + _views = new ObservableCollection<SaveModel>(); + } + + public void Sort() + { + Saves.AsObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + OnPropertyChanged(nameof(Views)); + } + + private bool Filter(object arg) + { + if (arg is SaveModel save) + { + return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + private IComparer<SaveModel> GetComparer() + { + switch (SortIndex) + { + case 0: + return OrderIndex == 0 + ? SortExpressionComparer<SaveModel>.Ascending(save => save.Title) + : SortExpressionComparer<SaveModel>.Descending(save => save.Title); + case 1: + return OrderIndex == 0 + ? SortExpressionComparer<SaveModel>.Ascending(save => save.Size) + : SortExpressionComparer<SaveModel>.Descending(save => save.Size); + default: + return null; + } + } + } +}
\ No newline at end of file |
