From 934b5a64e5638ae5228acb52faf48efadefdea8d Mon Sep 17 00:00:00 2001
From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
Date: Wed, 11 Jan 2023 00:20:19 -0500
Subject: Ava GUI: User Profile Manager + Other Fixes (#4166)

* Fix redundancies

* Add back elses

* Loading Screen fixes

* Redesign User Profile Manager

- Backported long selection bar in Grid/List view not working
- Backported UserSelector is jank

* Fix SelectionIndicator

* Fix DataType

* Fix SaveManager bug

* Remove debug log

* Load saves on UIThread

* Reduce UI thread blocking

* Fix locale keys

* Use block namespaces

* Fix close button width

* Make UserProfile ordering consistent

* Alphabetical order

* Adjust layout, remove green circle for blue selector

* Fix some inconsistencies

* Fix no inital selected profile

* Adjust appearance of edit button

* Adjust SaveManager

* Remove redundant warning dialog

* Make firmware avatar selector clearer

* View redesign again :hero_depressed:

* Consistency adjustments

* Adjust margins

* Make `UserProfileImageSelector` consistent

* Make `UserFirmwareAvatarSelector` consistent

* Fix long grid view selector

* Switch case

* Remove long selection bar

Handled in #4178

* Consistency

* Started dialog titles

* Fixes

* Remaining titles

* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml

Co-authored-by: Mary-nyan <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>
---
 .../UserFirmwareAvatarSelectorViewModel.cs         | 230 +++++++++++++++++++++
 1 file changed, 230 insertions(+)
 create mode 100644 Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs

(limited to 'Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs')

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
-- 
cgit v1.2.3-70-g09d2