From 3af42d6c7e9e71c504b87a7b0f7f960fe83418fb Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Fri, 8 Jul 2022 18:47:11 +0000
Subject: [PATCH] UI - Avalonia Part 3 (#3441)

* Add all other windows

* addreesed review

* Prevent "No Update" option from being deleted

* Select no update is the current update is removed from the title update window

* fix amiibo crash
---
 Ryujinx.Ava/Assets/Locales/en_US.json         |  18 +-
 .../ProfileImageSelectionDialog.axaml         |  35 ++
 .../ProfileImageSelectionDialog.axaml.cs      | 105 ++++
 Ryujinx.Ava/Ui/Models/Amiibo.cs               |  72 +++
 Ryujinx.Ava/Ui/Models/CheatModel.cs           |  37 ++
 Ryujinx.Ava/Ui/Models/CheatsList.cs           |  51 ++
 Ryujinx.Ava/Ui/Models/DlcModel.cs             |  18 +
 Ryujinx.Ava/Ui/Models/ProfileImageModel.cs    |   2 +-
 Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs     |   7 +-
 Ryujinx.Ava/Ui/Models/UserProfile.cs          |  61 +++
 .../Ui/ViewModels/AmiiboWindowViewModel.cs    | 450 ++++++++++++++++++
 .../Ui/ViewModels/AvatarProfileViewModel.cs   | 363 ++++++++++++++
 .../Ui/ViewModels/MainWindowViewModel.cs      |  79 ++-
 .../Ui/ViewModels/UserProfileViewModel.cs     | 166 +++++++
 Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml     |  68 +++
 Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs  |  70 +++
 Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml     |  53 +++
 Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs  |  71 +++
 Ryujinx.Ava/Ui/Windows/CheatWindow.axaml      |  90 ++++
 Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs   | 137 ++++++
 Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml | 132 +++++
 .../Ui/Windows/DlcManagerWindow.axaml.cs      | 261 ++++++++++
 .../Ui/Windows/TitleUpdateWindow.axaml        | 104 ++++
 .../Ui/Windows/TitleUpdateWindow.axaml.cs     | 265 +++++++++++
 .../Ui/Windows/UserProfileWindow.axaml        | 107 +++++
 .../Ui/Windows/UserProfileWindow.axaml.cs     | 102 ++++
 26 files changed, 2901 insertions(+), 23 deletions(-)
 create mode 100644 Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml
 create mode 100644 Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Models/Amiibo.cs
 create mode 100644 Ryujinx.Ava/Ui/Models/CheatModel.cs
 create mode 100644 Ryujinx.Ava/Ui/Models/CheatsList.cs
 create mode 100644 Ryujinx.Ava/Ui/Models/DlcModel.cs
 create mode 100644 Ryujinx.Ava/Ui/Models/UserProfile.cs
 create mode 100644 Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs
 create mode 100644 Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs
 create mode 100644 Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
 create mode 100644 Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
 create mode 100644 Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs

diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json
index ebf30df601..d9483db47a 100644
--- a/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -553,6 +553,20 @@
   "SettingsTabHotkeysToggleMuteHotkey": "Mute:",
   "ControllerMotionTitle": "Motion Control Settings",
   "ControllerRumbleTitle": "Rumble Settings",
-  "SettingsSelectThemeFileDialogTitle" : "Select Theme File",
-  "SettingsXamlThemeFile" : "Xaml Theme File"
+  "SettingsSelectThemeFileDialogTitle": "Select Theme File",
+  "SettingsXamlThemeFile": "Xaml Theme File",
+  "AvatarWindowTitle": "Manage Accounts - Avatar",
+  "Amiibo": "Amiibo",
+  "Unknown": "Unknown",
+  "Usage": "Usage",
+  "Writable": "Writable",
+  "SelectDlcDialogTitle": "Select DLC files",
+  "SelectUpdateDialogTitle": "Select update files",
+  "UserProfileWindowTitle": "Manage User Profiles",
+  "CheatWindowTitle": "Manage Game Cheats",
+  "DlcWindowTitle": "Manage Game DLC",
+  "UpdateWindowTitle": "Manage Game Updates",
+  "CheatWindowHeading": "Cheats Available for {0} [{1}]",
+  "DlcWindowHeading": "DLC Available for {0} [{1}]",
+  "GameUpdateWindowHeading": "DLC Available for {0} [{1}]"
 }
diff --git a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml
new file mode 100644
index 0000000000..c6f43f43f8
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml
@@ -0,0 +1,35 @@
+<Window 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"
+        x:Class="Ryujinx.Ava.Ui.Controls.ProfileImageSelectionDialog"
+        SizeToContent="WidthAndHeight"
+        WindowStartupLocation="CenterOwner"
+        Title="{Locale:Locale ProfileImageSelectionTitle}"
+        CanResize="false">
+    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="70" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <TextBlock FontWeight="Bold" FontSize="18" HorizontalAlignment="Center" Grid.Row="1"
+                   Text="{Locale:Locale ProfileImageSelectionHeader}" />
+        <TextBlock FontWeight="Bold" Grid.Row="2" Margin="10" MaxWidth="400" TextWrapping="Wrap"
+                   HorizontalAlignment="Center" TextAlignment="Center" Text="{Locale:Locale ProfileImageSelectionNote}" />
+        <StackPanel Margin="5,0" Spacing="10" Grid.Row="4" HorizontalAlignment="Center"
+                    Orientation="Horizontal">
+            <Button Name="Import" Click="Import_OnClick" Width="200">
+                <TextBlock Text="{Locale:Locale ProfileImageSelectionImportImage}" />
+            </Button>
+            <Button Name="SelectFirmwareImage" IsEnabled="{Binding FirmwareFound}" Click="SelectFirmwareImage_OnClick"
+                    Width="200">
+                <TextBlock Text="{Locale:Locale ProfileImageSelectionSelectAvatar}" />
+            </Button>
+        </StackPanel>
+    </Grid>
+</Window>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs
new file mode 100644
index 0000000000..728b890695
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs
@@ -0,0 +1,105 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE.FileSystem;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using System.IO;
+using Image = SixLabors.ImageSharp.Image;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+    public class ProfileImageSelectionDialog : StyleableWindow
+    {
+        private readonly ContentManager _contentManager;
+
+        public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null;
+
+        public byte[] BufferImageProfile { get; set; }
+
+        public ProfileImageSelectionDialog(ContentManager contentManager)
+        {
+            _contentManager = contentManager;
+            DataContext = this;
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+        }
+
+        public ProfileImageSelectionDialog()
+        {
+            DataContext = this;
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        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(this);
+
+            if (image != null)
+            {
+                if (image.Length > 0)
+                {
+                    string imageFile = image[0];
+
+                    ProcessProfileImage(File.ReadAllBytes(imageFile));
+                }
+
+                Close();
+            }
+        }
+
+        private async void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
+        {
+            if (FirmwareFound)
+            {
+                AvatarWindow window = new(_contentManager);
+
+                await window.ShowDialog(this);
+
+                BufferImageProfile = window.SelectedImage;
+
+                Close();
+            }
+        }
+
+        private void ProcessProfileImage(byte[] buffer)
+        {
+            using (Image image = Image.Load(buffer))
+            {
+                image.Mutate(x => x.Resize(256, 256));
+
+                using (MemoryStream streamJpg = new())
+                {
+                    image.SaveAsJpeg(streamJpg);
+
+                    BufferImageProfile = streamJpg.ToArray();
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/Amiibo.cs b/Ryujinx.Ava/Ui/Models/Amiibo.cs
new file mode 100644
index 0000000000..8644ab52c2
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/Amiibo.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+    public class Amiibo
+    {
+        public struct AmiiboJson
+        {
+            [JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
+            [JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
+        }
+
+        public struct AmiiboApi
+        {
+            [JsonPropertyName("name")] public string Name { get; set; }
+            [JsonPropertyName("head")] public string Head { get; set; }
+            [JsonPropertyName("tail")] public string Tail { get; set; }
+            [JsonPropertyName("image")] public string Image { get; set; }
+            [JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
+            [JsonPropertyName("character")] public string Character { get; set; }
+            [JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
+            [JsonPropertyName("type")] public string Type { get; set; }
+
+            [JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
+
+            [JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
+
+            public override string ToString()
+            {
+                return Name;
+            }
+
+            public string GetId()
+            {
+                return Head + Tail;
+            }
+
+            public override bool Equals(object obj)
+            {
+                if (obj is AmiiboApi amiibo)
+                {
+                    return amiibo.Head + amiibo.Tail == Head + Tail;
+                }
+
+                return false;
+            }
+
+            public override int GetHashCode()
+            {
+                return base.GetHashCode();
+            }
+        }
+
+        public class AmiiboApiGamesSwitch
+        {
+            [JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
+
+            [JsonPropertyName("gameID")] public List<string> GameId { get; set; }
+
+            [JsonPropertyName("gameName")] public string GameName { get; set; }
+        }
+
+        public class AmiiboApiUsage
+        {
+            [JsonPropertyName("Usage")] public string Usage { get; set; }
+
+            [JsonPropertyName("write")] public bool Write { get; set; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/CheatModel.cs b/Ryujinx.Ava/Ui/Models/CheatModel.cs
new file mode 100644
index 0000000000..cdab27cd84
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/CheatModel.cs
@@ -0,0 +1,37 @@
+using Ryujinx.Ava.Ui.ViewModels;
+using System;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+    public class CheatModel : BaseModel
+    {
+        private bool _isEnabled;
+
+        public event EventHandler<bool> EnableToggled;
+
+        public CheatModel(string name, string buildId, bool isEnabled)
+        {
+            Name = name;
+            BuildId = buildId;
+            IsEnabled = isEnabled;
+        }
+
+        public bool IsEnabled
+        {
+            get => _isEnabled;
+            set
+            {
+                _isEnabled = value;
+                EnableToggled?.Invoke(this, _isEnabled);
+                OnPropertyChanged();
+            }
+        }
+
+        public string BuildId { get; }
+
+        public string BuildIdKey => $"{BuildId}-{Name}";
+        public string Name { get; }
+
+        public string CleanName => Name.Substring(1, Name.Length - 8);
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/CheatsList.cs b/Ryujinx.Ava/Ui/Models/CheatsList.cs
new file mode 100644
index 0000000000..f2b0592e97
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/CheatsList.cs
@@ -0,0 +1,51 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+    public class CheatsList : ObservableCollection<CheatModel>
+    {
+        public CheatsList(string buildId, string path)
+        {
+            BuildId = buildId;
+            Path = path;
+            CollectionChanged += CheatsList_CollectionChanged;
+        }
+
+        private void CheatsList_CollectionChanged(object sender,
+            NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add)
+            {
+                (e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
+            }
+        }
+
+        private void Item_EnableToggled(object sender, bool e)
+        {
+            OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
+        }
+
+        public string BuildId { get; }
+        public string Path { get; }
+
+        public bool IsEnabled
+        {
+            get
+            {
+                return this.ToList().TrueForAll(x => x.IsEnabled);
+            }
+            set
+            {
+                foreach (var cheat in this)
+                {
+                    cheat.IsEnabled = value;
+                }
+
+                OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/DlcModel.cs b/Ryujinx.Ava/Ui/Models/DlcModel.cs
new file mode 100644
index 0000000000..7e5f4a62fc
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/DlcModel.cs
@@ -0,0 +1,18 @@
+namespace Ryujinx.Ava.Ui.Models
+{
+    public class DlcModel
+    {
+        public bool IsEnabled { get; set; }
+        public string TitleId { get; }
+        public string ContainerPath { get; }
+        public string FullPath { get; }
+
+        public DlcModel(string titleId, string containerPath, string fullPath, bool isEnabled)
+        {
+            TitleId = titleId;
+            ContainerPath = containerPath;
+            FullPath = fullPath;
+            IsEnabled = isEnabled;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Models/ProfileImageModel.cs b/Ryujinx.Ava/Ui/Models/ProfileImageModel.cs
index a23b55ccb4..1c9f3b0539 100644
--- a/Ryujinx.Ava/Ui/Models/ProfileImageModel.cs
+++ b/Ryujinx.Ava/Ui/Models/ProfileImageModel.cs
@@ -1,6 +1,6 @@
 namespace Ryujinx.Ava.Ui.Models
 {
-    internal class ProfileImageModel
+    public class ProfileImageModel
     {
         public ProfileImageModel(string name, byte[] data)
         {
diff --git a/Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs b/Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs
index f864e70a91..2bf6dbfa1f 100644
--- a/Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs
+++ b/Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs
@@ -9,8 +9,11 @@ namespace Ryujinx.Ava.Ui.Models
         public bool IsNoUpdate { get; }
         public ApplicationControlProperty Control { get; }
         public string Path { get; }
-        public string Label => IsNoUpdate ? LocaleManager.Instance["NoUpdate"] : 
-            string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(), Path);
+
+        public string Label => IsNoUpdate
+            ? LocaleManager.Instance["NoUpdate"]
+            : string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(),
+                Path);
 
         public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
         {
diff --git a/Ryujinx.Ava/Ui/Models/UserProfile.cs b/Ryujinx.Ava/Ui/Models/UserProfile.cs
new file mode 100644
index 0000000000..351ada76f4
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Models/UserProfile.cs
@@ -0,0 +1,61 @@
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+    public class UserProfile : BaseModel
+    {
+        private readonly Profile _profile;
+        private byte[] _image;
+        private string _name;
+        private UserId _userId;
+
+        public byte[] Image
+        {
+            get => _image;
+            set
+            {
+                _image = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public UserId UserId
+        {
+            get => _userId;
+            set
+            {
+                _userId = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public string Name
+        {
+            get => _name;
+            set
+            {
+                _name = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public UserProfile(Profile profile)
+        {
+            _profile = profile;
+
+            Image = profile.Image;
+            Name = profile.Name;
+            UserId = profile.UserId;
+        }
+
+        public bool IsOpened => _profile.AccountState == AccountState.Open;
+
+        public void UpdateState()
+        {
+            OnPropertyChanged(nameof(IsOpened));
+            OnPropertyChanged(nameof(Name));
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs
new file mode 100644
index 0000000000..9f411ba2aa
--- /dev/null
+++ b/Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs
@@ -0,0 +1,450 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.ViewModels
+{
+    public class AmiiboWindowViewModel : BaseModel, IDisposable
+    {
+        private const string DefaultJson = "{ \"amiibo\": [] }";
+        private const float AmiiboImageSize = 350f;
+
+        private readonly string _amiiboJsonPath;
+        private readonly byte[] _amiiboLogoBytes;
+        private readonly HttpClient _httpClient;
+        private readonly StyleableWindow _owner;
+        
+        private Bitmap _amiiboImage;
+        private List<Amiibo.AmiiboApi> _amiiboList;
+        private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
+        private ObservableCollection<string> _amiiboSeries;
+
+        private int _amiiboSelectedIndex;
+        private int _seriesSelectedIndex;
+        private bool _enableScanning;
+        private bool _showAllAmiibo;
+        private bool _useRandomUuid;
+        private string _usage;
+
+        public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
+        {
+            _owner = owner;
+            _httpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(5000) };
+            LastScannedAmiiboId = lastScannedAmiiboId;
+            TitleId = titleId;
+
+            Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
+
+            _amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
+            _amiiboList = new List<Amiibo.AmiiboApi>();
+            _amiiboSeries = new ObservableCollection<string>();
+            _amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
+
+            _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
+
+            _ = LoadContentAsync();
+        }
+
+        public AmiiboWindowViewModel() { }
+
+        public string TitleId { get; set; }
+        public string LastScannedAmiiboId { get; set; }
+
+        public UserResult Response { get; private set; }
+
+        public bool UseRandomUuid
+        {
+            get => _useRandomUuid;
+            set
+            {
+                _useRandomUuid = value;
+
+                OnPropertyChanged();
+            }
+        }
+
+        public bool ShowAllAmiibo
+        {
+            get => _showAllAmiibo;
+            set
+            {
+                _showAllAmiibo = value;
+
+#pragma warning disable 4014
+                ParseAmiiboData();
+#pragma warning restore 4014
+
+                OnPropertyChanged();
+            }
+        }
+
+        public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
+        {
+            get => _amiibos;
+            set
+            {
+                _amiibos = value;
+
+                OnPropertyChanged();
+            }
+        }
+
+        public ObservableCollection<string> AmiiboSeries
+        {
+            get => _amiiboSeries;
+            set
+            {
+                _amiiboSeries = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public int SeriesSelectedIndex
+        {
+            get => _seriesSelectedIndex;
+            set
+            {
+                _seriesSelectedIndex = value;
+
+                FilterAmiibo();
+
+                OnPropertyChanged();
+            }
+        }
+
+        public int AmiiboSelectedIndex
+        {
+            get => _amiiboSelectedIndex;
+            set
+            {
+                _amiiboSelectedIndex = value;
+
+                EnableScanning = _amiiboSelectedIndex >= 0 && _amiiboSelectedIndex < _amiibos.Count;
+
+                SetAmiiboDetails();
+
+                OnPropertyChanged();
+            }
+        }
+
+        public Bitmap AmiiboImage
+        {
+            get => _amiiboImage;
+            set
+            {
+                _amiiboImage = value;
+
+                OnPropertyChanged();
+            }
+        }
+
+        public string Usage
+        {
+            get => _usage;
+            set
+            {
+                _usage = value;
+
+                OnPropertyChanged();
+            }
+        }
+
+        public bool EnableScanning
+        {
+            get => _enableScanning;
+            set
+            {
+                _enableScanning = value;
+
+                OnPropertyChanged();
+            }
+        }
+
+        public void Dispose()
+        {
+            _httpClient.Dispose();
+        }
+
+        private async Task LoadContentAsync()
+        {
+            string amiiboJsonString = DefaultJson;
+
+            if (File.Exists(_amiiboJsonPath))
+            {
+                amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
+
+                if (await NeedsUpdate(JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
+                {
+                    amiiboJsonString = await DownloadAmiiboJson();
+                }
+            }
+            else
+            {
+                try
+                {
+                    amiiboJsonString = await DownloadAmiiboJson();
+                }
+                catch
+                {
+                    ShowInfoDialog();
+                }
+            }
+
+            _amiiboList = JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
+            _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
+
+            ParseAmiiboData();
+        }
+
+        private void ParseAmiiboData()
+        {
+            _amiiboSeries.Clear();
+            _amiibos.Clear();
+
+            for (int i = 0; i < _amiiboList.Count; i++)
+            {
+                if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
+                {
+                    if (!ShowAllAmiibo)
+                    {
+                        foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
+                        {
+                            if (game != null)
+                            {
+                                if (game.GameId.Contains(TitleId))
+                                {
+                                    AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
+
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
+                    }
+                }
+            }
+
+            if (LastScannedAmiiboId != "")
+            {
+                SelectLastScannedAmiibo();
+            }
+            else
+            {
+                SeriesSelectedIndex = 0;
+            }
+        }
+
+        private void SelectLastScannedAmiibo()
+        {
+            Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
+
+            SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
+            AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
+        }
+
+        private void FilterAmiibo()
+        {
+            _amiibos.Clear();
+
+            if (_seriesSelectedIndex < 0)
+            {
+                return;
+            }
+
+            List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
+                .Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
+                .OrderBy(amiibo => amiibo.Name).ToList();
+
+            for (int i = 0; i < amiiboSortedList.Count; i++)
+            {
+                if (!_amiibos.Contains(amiiboSortedList[i]))
+                {
+                    if (!_showAllAmiibo)
+                    {
+                        foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
+                        {
+                            if (game != null)
+                            {
+                                if (game.GameId.Contains(TitleId))
+                                {
+                                    _amiibos.Add(amiiboSortedList[i]);
+
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        _amiibos.Add(amiiboSortedList[i]);
+                    }
+                }
+            }
+
+            AmiiboSelectedIndex = 0;
+        }
+
+        private void SetAmiiboDetails()
+        {
+            ResetAmiiboPreview();
+
+            Usage = string.Empty;
+
+            if (_amiiboSelectedIndex < 0)
+            {
+                return;
+            }
+
+            Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
+
+            string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
+
+            string usageString = "";
+
+            for (int i = 0; i < _amiiboList.Count; i++)
+            {
+                if (_amiiboList[i].Equals(selected))
+                {
+                    bool writable = false;
+
+                    foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
+                    {
+                        if (item.GameId.Contains(TitleId))
+                        {
+                            foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
+                            {
+                                usageString += Environment.NewLine +
+                                               $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
+
+                                writable = usageItem.Write;
+                            }
+                        }
+                    }
+
+                    if (usageString.Length == 0)
+                    {
+                        usageString = LocaleManager.Instance["Unknown"] + ".";
+                    }
+
+                    Usage = $"{LocaleManager.Instance["Usage"]} {(writable ? $" ({LocaleManager.Instance["Writable"]})" : "")} : {usageString}";
+                }
+            }
+
+            _ = UpdateAmiiboPreview(imageUrl);
+        }
+
+        private async Task<bool> NeedsUpdate(DateTime oldLastModified)
+        {
+            try
+            {
+                HttpResponseMessage response =
+                    await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
+
+                if (response.IsSuccessStatusCode)
+                {
+                    return response.Content.Headers.LastModified != oldLastModified;
+                }
+
+                return false;
+            }
+            catch
+            {
+                ShowInfoDialog();
+
+                return false;
+            }
+        }
+
+        private async Task<string> DownloadAmiiboJson()
+        {
+            HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
+
+            if (response.IsSuccessStatusCode)
+            {
+                string amiiboJsonString = await response.Content.ReadAsStringAsync();
+
+                using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
+                {
+                    dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
+                }
+
+                return amiiboJsonString;
+            }
+
+            await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
+                LocaleManager.Instance["DialogAmiiboApiFailFetchMessage"],
+                LocaleManager.Instance["InputDialogOk"],
+                "",
+                LocaleManager.Instance["RyujinxInfo"]);
+
+            Close();
+
+            return DefaultJson;
+        }
+
+        private void Close()
+        {
+            Dispatcher.UIThread.Post(_owner.Close);
+        }
+
+        private async Task UpdateAmiiboPreview(string imageUrl)
+        {
+            HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
+
+            if (response.IsSuccessStatusCode)
+            {
+                byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
+                using (MemoryStream memoryStream = new(amiiboPreviewBytes))
+                {
+                    Bitmap bitmap = new(memoryStream);
+
+                    double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
+                        AmiiboImageSize / bitmap.Size.Height);
+
+                    int resizeHeight = (int)(bitmap.Size.Height * ratio);
+                    int resizeWidth = (int)(bitmap.Size.Width * ratio);
+
+                    AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
+                }
+            }
+        }
+
+        private void ResetAmiiboPreview()
+        {
+            using (MemoryStream memoryStream = new(_amiiboLogoBytes))
+            {
+                Bitmap bitmap = new(memoryStream);
+
+                AmiiboImage = bitmap;
+            }
+        }
+
+        private async void ShowInfoDialog()
+        {
+            await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
+                LocaleManager.Instance["DialogAmiiboApiConnectErrorMessage"],
+                LocaleManager.Instance["InputDialogOk"],
+                "",
+                LocaleManager.Instance["RyujinxInfo"]);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs
new file mode 100644
index 0000000000..c29837414f
--- /dev/null
+++ b/Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs
@@ -0,0 +1,363 @@
+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 AvatarProfileViewModel : 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;
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => _isActive = value;
+        }
+
+        public AvatarProfileViewModel()
+        {
+            _images = new ObservableCollection<ProfileImageModel>();
+        }
+        
+        public AvatarProfileViewModel(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 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;
+                return;
+            }
+            Task.Run(() =>
+            {
+                IsActive = true;
+
+                Images.Clear();
+                int selectedIndex = _selectedIndex;
+                int index = 0;
+                
+                ImagesLoaded = 0;
+                IsIndeterminate = false;
+
+                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);
+
+                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;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
index dbc19f75f4..bc8e645081 100644
--- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
+++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs
@@ -67,6 +67,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
         private bool _isPaused;
         private bool _showContent = true;
         private bool _isLoadingIndeterminate = true;
+        private bool _showAll;
+        private string _lastScannedAmiiboId;
         private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
 
         public string TitleName { get; internal set; }
@@ -695,15 +697,28 @@ namespace Ryujinx.Ava.Ui.ViewModels
             }
         }
 
-        public void OpenAmiiboWindow()
+        public async void OpenAmiiboWindow()
         {
             if (!_isAmiiboRequested)
             {
                 return;
             }
+            
+            if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
+            {
+                string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper();
+                AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId);
 
-            // TODO : Implement Amiibo window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+                await window.ShowDialog(_owner);
+
+                if (window.IsScanned)
+                {
+                    _showAll = window.ViewModel.ShowAllAmiibo;
+                    _lastScannedAmiiboId = window.ScannedAmiibo.GetId();
+
+                    _owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid);
+                }
+            }
         }
 
         public void HandleShaderProgress(Switch emulationContext)
@@ -953,10 +968,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
             LoadConfigurableHotKeys();
         }
 
-        public void ManageProfiles()
+        public async void ManageProfiles()
         {
-            // TODO : Implement Profiles window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+            UserProfileWindow window = new(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem);
+
+            await window.ShowDialog(_owner);
         }
 
         public async void OpenAboutWindow()
@@ -1227,33 +1243,60 @@ namespace Ryujinx.Ava.Ui.ViewModels
             }
         }
 
-        public void OpenTitleUpdateManager()
+        public async void OpenTitleUpdateManager()
         {
-            // TODO : Implement Update window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+            var selection = SelectedApplication;
+
+            if (selection != null)
+            {
+                TitleUpdateWindow titleUpdateManager =
+                    new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
+
+                await titleUpdateManager.ShowDialog(_owner);
+            }
         }
 
-        public void OpenDlcManager()
+        public async void OpenDlcManager()
         {
-            // TODO : Implement Dlc window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+            var selection = SelectedApplication;
+
+            if (selection != null)
+            {
+                DlcManagerWindow dlcManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
+
+                await dlcManager.ShowDialog(_owner);
+            }
         }
 
-        public void OpenCheatManager()
+        public async void OpenCheatManager()
         {
-            // TODO : Implement cheat window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+            var selection = SelectedApplication;
+
+            if (selection != null)
+            {
+                CheatWindow cheatManager = new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
+
+                await cheatManager.ShowDialog(_owner);
+            }
         }
 
-        public void OpenCheatManagerForCurrentApp()
+        public async void OpenCheatManagerForCurrentApp()
         {
             if (!IsGameRunning)
             {
                 return;
             }
 
-            // TODO : Implement cheat window
-            ContentDialogHelper.ShowNotAvailableMessage(_owner);
+            var application = _owner.AppHost.Device.Application;
+
+            if (application != null)
+            {
+                CheatWindow cheatManager = new(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName);
+
+                await cheatManager.ShowDialog(_owner);
+
+                _owner.AppHost.Device.EnableCheats();
+            }
         }
 
         public void OpenDeviceSaveDirectory()
diff --git a/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
new file mode 100644
index 0000000000..d75f65b1fb
--- /dev/null
+++ b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
@@ -0,0 +1,166 @@
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
+
+namespace Ryujinx.Ava.Ui.ViewModels
+{
+    public class UserProfileViewModel : BaseModel, IDisposable
+    {
+        private const uint MaxProfileNameLength = 0x20;
+
+        private readonly UserProfileWindow _owner;
+
+        private UserProfile _selectedProfile;
+        private string _tempUserName;
+
+        public UserProfileViewModel()
+        {
+            Profiles = new ObservableCollection<UserProfile>();
+        }
+
+        public UserProfileViewModel(UserProfileWindow owner) : this()
+        {
+            _owner = owner;
+
+            LoadProfiles();
+        }
+
+        public ObservableCollection<UserProfile> Profiles { get; set; }
+
+        public UserProfile SelectedProfile
+        {
+            get => _selectedProfile;
+            set
+            {
+                _selectedProfile = value;
+
+                OnPropertyChanged(nameof(SelectedProfile));
+                OnPropertyChanged(nameof(IsSelectedProfileDeletable));
+            }
+        }
+
+        public bool IsSelectedProfileDeletable =>
+            _selectedProfile != null && _selectedProfile.UserId != AccountManager.DefaultUserId;
+
+        public void Dispose()
+        {
+        }
+
+        public void LoadProfiles()
+        {
+            Profiles.Clear();
+
+            var profiles = _owner.AccountManager.GetAllUsers()
+                .OrderByDescending(x => x.AccountState == AccountState.Open);
+
+            foreach (var profile in profiles)
+            {
+                Profiles.Add(new UserProfile(profile));
+            }
+
+            SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
+
+            if (SelectedProfile == null)
+            {
+                SelectedProfile = Profiles.First();
+
+                if (SelectedProfile != null)
+                {
+                    _owner.AccountManager.OpenUser(_selectedProfile.UserId);
+                }
+            }
+        }
+
+        public async void ChooseProfileImage()
+        {
+            await SelectProfileImage();
+        }
+
+        public async Task SelectProfileImage(bool isNewUser = false)
+        {
+            ProfileImageSelectionDialog selectionDialog = new(_owner.ContentManager);
+
+            await selectionDialog.ShowDialog(_owner);
+
+            if (selectionDialog.BufferImageProfile != null)
+            {
+                if (isNewUser)
+                {
+                    if (!string.IsNullOrWhiteSpace(_tempUserName))
+                    {
+                        _owner.AccountManager.AddUser(_tempUserName, selectionDialog.BufferImageProfile);
+                    }
+                }
+                else if (SelectedProfile != null)
+                {
+                    _owner.AccountManager.SetUserImage(SelectedProfile.UserId, selectionDialog.BufferImageProfile);
+                    SelectedProfile.Image = selectionDialog.BufferImageProfile;
+
+                    SelectedProfile = null;
+                }
+
+                LoadProfiles();
+            }
+        }
+
+        public async void AddUser()
+        {
+            var dlgTitle = LocaleManager.Instance["InputDialogAddNewProfileTitle"];
+            var dlgMainText = LocaleManager.Instance["InputDialogAddNewProfileHeader"];
+            var dlgSubText = string.Format(LocaleManager.Instance["InputDialogAddNewProfileSubtext"],
+                MaxProfileNameLength);
+
+            _tempUserName =
+                await ContentDialogHelper.CreateInputDialog(dlgTitle, dlgMainText, dlgSubText, _owner,
+                    MaxProfileNameLength);
+
+            if (!string.IsNullOrWhiteSpace(_tempUserName))
+            {
+                await SelectProfileImage(true);
+            }
+
+            _tempUserName = String.Empty;
+        }
+
+        public async void DeleteUser()
+        {
+            if (_selectedProfile != null)
+            {
+                var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
+
+                if (_selectedProfile.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)
+                    {
+                        ContentDialogHelper.CreateErrorDialog(_owner,
+                            LocaleManager.Instance["DialogUserProfileDeletionWarningMessage"]);
+                        return;
+                    }
+
+                    _owner.AccountManager.OpenUser(profile.UserId);
+                }
+
+                var result =
+                    await ContentDialogHelper.CreateConfirmationDialog(_owner,
+                        LocaleManager.Instance["DialogUserProfileDeletionConfirmMessage"], "",
+                        LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
+
+                if (result == UserResult.Yes)
+                {
+                    _owner.AccountManager.DeleteUser(_selectedProfile.UserId);
+                }
+            }
+
+            LoadProfiles();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
new file mode 100644
index 0000000000..f91bb3131e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
@@ -0,0 +1,68 @@
+<window:StyleableWindow xmlns="https://github.com/avaloniaui"
+                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                        mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
+                        x:Class="Ryujinx.Ava.Ui.Windows.AmiiboWindow"
+                        xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+                        xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+                        xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+                        CanResize="False"
+                        WindowStartupLocation="CenterOwner"
+                        Width="800" MinHeight="650" Height="650"
+                        SizeToContent="Manual"
+                        MinWidth="600">
+    <Design.DataContext>
+        <viewModels:AmiiboWindowViewModel />
+    </Design.DataContext>
+    <Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <Grid Grid.Row="1" HorizontalAlignment="Stretch">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition />
+                <ColumnDefinition />
+            </Grid.ColumnDefinitions>
+            <StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
+                <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" />
+                <ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" />
+            </StackPanel>
+            <StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
+                <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" />
+                <ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" />
+            </StackPanel>
+        </Grid>
+        <StackPanel Margin="20" Grid.Row="2">
+            <Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" />
+            <ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
+                          Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
+                <TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
+                           TextAlignment="Center" />
+            </ScrollViewer>
+        </StackPanel>
+        <Grid Grid.Row="3">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="*" />
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="Auto" />
+            </Grid.ColumnDefinitions>
+            <CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}"
+                      Content="{locale:Locale AmiiboOptionsShowAllLabel}" />
+            <CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}"
+                      Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" />
+
+            <Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80"
+                    Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton"
+                    Click="ScanButton_Click" />
+            <Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}"
+                    Name="CancelButton"
+                    Click="CancelButton_Click" />
+        </Grid>
+    </Grid>
+</window:StyleableWindow>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
new file mode 100644
index 0000000000..bd0935a9c4
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
@@ -0,0 +1,70 @@
+using Avalonia;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.ViewModels;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class AmiiboWindow : StyleableWindow
+    {
+        public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
+        {
+            ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
+
+            ViewModel.ShowAllAmiibo = showAll;
+
+            DataContext = ViewModel;
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
+        }
+
+        public AmiiboWindow()
+        {
+            ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
+
+            DataContext = ViewModel;
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            if (Program.PreviewerDetached)
+            {
+                Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
+            }
+        }
+
+        public bool IsScanned { get; set; }
+        public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
+        public AmiiboWindowViewModel ViewModel { get; set; }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void ScanButton_Click(object sender, RoutedEventArgs e)
+        {
+            if (ViewModel.AmiiboSelectedIndex > -1)
+            {
+                Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
+                ScannedAmiibo = amiibo;
+                IsScanned = true;
+                Close();
+            }
+        }
+
+        private void CancelButton_Click(object sender, RoutedEventArgs e)
+        {
+            IsScanned = false;
+
+            Close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
new file mode 100644
index 0000000000..6c7576bc0a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
@@ -0,0 +1,53 @@
+<Window 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.Windows.AvatarWindow"
+        CanResize="False"
+        xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+        xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+        xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+        WindowStartupLocation="CenterOwner"
+        x:CompileBindings="True"
+        x:DataType="viewModels:AvatarProfileViewModel"
+        SizeToContent="WidthAndHeight">
+    <Design.DataContext>
+        <viewModels:AvatarProfileViewModel />
+    </Design.DataContext>
+    <Window.Resources>
+        <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+    </Window.Resources>
+    <Grid Margin="5" 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}" Width="600" Height="500"
+                 Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
+            <ListBox.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <WrapPanel Orientation="Horizontal" MaxWidth="600" Margin="0" HorizontalAlignment="Center" />
+                </ItemsPanelTemplate>
+            </ListBox.ItemsPanel>
+            <ListBox.ItemTemplate>
+                <DataTemplate>
+                    <Image Margin="5" Height="96" Width="96"
+                           Source="{Binding Data, Converter={StaticResource ByteImage}}" />
+                </DataTemplate>
+            </ListBox.ItemTemplate>
+        </ListBox>
+        <ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
+                     Maximum="{Binding ImageCount}" Minimum="0" />
+        <StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
+            <Button Content="{Locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
+            <ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
+            <Button HorizontalAlignment="Right" Content="{Locale:Locale AvatarClose}" Click="CloseButton_OnClick"
+                    Name="CloseButton"
+                    Width="200" />
+        </StackPanel>
+    </Grid>
+</Window>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
new file mode 100644
index 0000000000..25e89846c8
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
@@ -0,0 +1,71 @@
+using Avalonia;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using System;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class AvatarWindow : StyleableWindow
+    {
+        public AvatarWindow(ContentManager contentManager)
+        {
+            ContentManager = contentManager;
+            ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages());
+
+            DataContext = ViewModel;
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
+        }
+
+        public AvatarWindow()
+        {
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            if (Program.PreviewerDetached)
+            {
+                Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
+            }
+        }
+
+        public ContentManager ContentManager { get; }
+
+        public byte[] SelectedImage { get; set; }
+
+        internal AvatarProfileViewModel ViewModel { get; set; }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        protected override void OnClosed(EventArgs e)
+        {
+            ViewModel.Dispose();
+            base.OnClosed(e);
+        }
+
+        private void CloseButton_OnClick(object sender, RoutedEventArgs e)
+        {
+            Close();
+        }
+
+        private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
+        {
+            if (ViewModel.SelectedIndex > -1)
+            {
+                SelectedImage = ViewModel.SelectedImage;
+
+                Close();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
new file mode 100644
index 0000000000..1685ee807a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
@@ -0,0 +1,90 @@
+<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
+                        xmlns="https://github.com/avaloniaui"
+                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                        xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+                        xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
+                        xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+                        mc:Ignorable="d"
+                        SizeToContent="Height"
+                        Width="500" MinHeight="500" Height="500"
+                        WindowStartupLocation="CenterOwner"
+                        MinWidth="500">
+    <Window.Styles>
+        <Style Selector="TreeViewItem">
+            <Setter Property="IsExpanded" Value="True" />
+        </Style>
+    </Window.Styles>
+    <Grid Name="DlcGrid" Margin="15">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <TextBlock
+            Grid.Row="1"
+            Margin="20,15,20,20"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            MaxWidth="500"
+            LineHeight="18"
+            TextWrapping="Wrap"
+            Text="{Binding Heading}"
+            TextAlignment="Center" />
+        <Border
+            Grid.Row="2"
+            Margin="5"
+            HorizontalAlignment="Stretch"
+            VerticalAlignment="Stretch"
+            BorderBrush="Gray"
+            BorderThickness="1">
+            <TreeView Items="{Binding LoadedCheats}"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Name="CheatsView"
+                      MinHeight="300">
+                <TreeView.DataTemplates>
+                    <TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
+                        <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
+                            <CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
+                            <TextBlock Width="150"
+                                       Text="{Binding BuildId}" />
+                            <TextBlock
+                                Text="{Binding Path}" />
+                        </StackPanel>
+                    </TreeDataTemplate>
+                    <DataTemplate x:DataType="model:CheatModel">
+                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
+                            <CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
+                            <TextBlock Text="{Binding CleanName}" />
+                        </StackPanel>
+                    </DataTemplate>
+                </TreeView.DataTemplates>
+            </TreeView>
+        </Border>
+        <DockPanel
+            Grid.Row="3"
+            Margin="0"
+            HorizontalAlignment="Stretch">
+            <DockPanel Margin="0" HorizontalAlignment="Right">
+                <Button
+                    Name="SaveButton"
+                    MinWidth="90"
+                    Margin="5"
+                    IsVisible="{Binding !NoCheatsFound}"
+                    Command="{Binding Save}">
+                    <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+                </Button>
+                <Button
+                    Name="CancelButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Close}">
+                    <TextBlock Text="{locale:Locale InputDialogCancel}" />
+                </Button>
+            </DockPanel>
+        </DockPanel>
+    </Grid>
+</window:StyleableWindow>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
new file mode 100644
index 0000000000..33abeb8d7a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
@@ -0,0 +1,137 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class CheatWindow : StyleableWindow
+    {
+        private readonly string _enabledCheatsPath;
+        public bool NoCheatsFound { get; }
+
+        private AvaloniaList<CheatsList> LoadedCheats { get; }
+
+        public string Heading { get; }
+
+        public CheatWindow()
+        {
+            DataContext = this;
+
+            InitializeComponent();
+            AttachDebugDevTools();
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
+        }
+
+        public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
+        {
+            LoadedCheats = new AvaloniaList<CheatsList>();
+
+            Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+
+            string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
+            string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
+            ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
+
+            _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
+
+            string[] enabled = { };
+
+            if (File.Exists(_enabledCheatsPath))
+            {
+                enabled = File.ReadAllLines(_enabledCheatsPath);
+            }
+
+            int cheatAdded = 0;
+
+            var mods = new ModLoader.ModCache();
+
+            ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue);
+
+            string currentCheatFile = string.Empty;
+            string buildId = string.Empty;
+            string parentPath = string.Empty;
+
+            CheatsList currentGroup = null;
+
+            foreach (var cheat in mods.Cheats)
+            {
+                if (cheat.Path.FullName != currentCheatFile)
+                {
+                    currentCheatFile = cheat.Path.FullName;
+                    parentPath = currentCheatFile.Replace(titleModsPath, "");
+
+                    buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
+                    currentGroup = new CheatsList(buildId, parentPath);
+
+                    LoadedCheats.Add(currentGroup);
+                }
+
+                var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}"));
+                currentGroup?.Add(model);
+
+                cheatAdded++;
+            }
+
+            if (cheatAdded == 0)
+            {
+                NoCheatsFound = true;
+            }
+
+            DataContext = this;
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
+        }
+
+        [Conditional("DEBUG")]
+        private void AttachDebugDevTools()
+        {
+            this.AttachDevTools();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        public void Save()
+        {
+            if (NoCheatsFound)
+            {
+                return;
+            }
+
+            List<string> enabledCheats = new List<string>();
+
+            foreach (var cheats in LoadedCheats)
+            {
+                foreach (var cheat in cheats)
+                {
+                    if (cheat.IsEnabled)
+                    {
+                        enabledCheats.Add(cheat.BuildIdKey);
+                    }
+                }
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath));
+
+            File.WriteAllLines(_enabledCheatsPath, enabledCheats);
+
+            Close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
new file mode 100644
index 0000000000..94b3895ef4
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
@@ -0,0 +1,132 @@
+<window:StyleableWindow
+    x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+    xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+    SizeToContent="Height"
+    Width="600" MinHeight="500" Height="500"
+    WindowStartupLocation="CenterOwner"
+    MinWidth="600"
+    mc:Ignorable="d">
+    <Grid Name="DlcGrid" Margin="15">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <TextBlock
+            Grid.Row="1"
+            Margin="20,15,20,20"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            MaxWidth="500"
+            LineHeight="18"
+            TextWrapping="Wrap"
+            Text="{Binding Heading}"
+            TextAlignment="Center" />
+        <Border
+            Grid.Row="2"
+            Margin="5"
+            HorizontalAlignment="Stretch"
+            VerticalAlignment="Stretch"
+            BorderBrush="Gray"
+            BorderThickness="1">
+            <DataGrid
+                MinHeight="200"
+                HorizontalAlignment="Stretch"
+                VerticalAlignment="Stretch"
+                HorizontalScrollBarVisibility="Auto"
+                Items="{Binding Dlcs}"
+                VerticalScrollBarVisibility="Auto">
+                <DataGrid.Columns>
+                    <DataGridTemplateColumn Width="90">
+                        <DataGridTemplateColumn.CellTemplate>
+                            <DataTemplate>
+                                <CheckBox
+                                    Width="50"
+                                    MinWidth="40"
+                                    HorizontalAlignment="Right"
+                                    IsChecked="{Binding IsEnabled}" />
+                            </DataTemplate>
+                        </DataGridTemplateColumn.CellTemplate>
+                        <DataGridTemplateColumn.Header>
+                            <TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
+                        </DataGridTemplateColumn.Header>
+                    </DataGridTemplateColumn>
+                    <DataGridTextColumn
+                        Width="190"
+                        Binding="{Binding TitleId}"
+                        CanUserResize="True">
+                        <DataGridTextColumn.Header>
+                            <TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
+                        </DataGridTextColumn.Header>
+                    </DataGridTextColumn>
+                    <DataGridTextColumn
+                        Width="*"
+                        Binding="{Binding ContainerPath}"
+                        CanUserResize="True">
+                        <DataGridTextColumn.Header>
+                            <TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
+                        </DataGridTextColumn.Header>
+                    </DataGridTextColumn>
+                    <DataGridTextColumn
+                        Width="*"
+                        Binding="{Binding FullPath}"
+                        CanUserResize="True">
+                        <DataGridTextColumn.Header>
+                            <TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
+                        </DataGridTextColumn.Header>
+                    </DataGridTextColumn>
+                </DataGrid.Columns>
+            </DataGrid>
+        </Border>
+        <DockPanel
+            Grid.Row="3"
+            Margin="0"
+            HorizontalAlignment="Stretch">
+            <DockPanel Margin="0" HorizontalAlignment="Left">
+                <Button
+                    Name="AddButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Add}">
+                    <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
+                </Button>
+                <Button
+                    Name="RemoveButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding RemoveSelected}">
+                    <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
+                </Button>
+                <Button
+                    Name="RemoveAllButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding RemoveAll}">
+                    <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
+                </Button>
+            </DockPanel>
+            <DockPanel Margin="0" HorizontalAlignment="Right">
+                <Button
+                    Name="SaveButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Save}">
+                    <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+                </Button>
+                <Button
+                    Name="CancelButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Close}">
+                    <TextBlock Text="{locale:Locale InputDialogCancel}" />
+                </Button>
+            </DockPanel>
+        </DockPanel>
+    </Grid>
+</window:StyleableWindow>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
new file mode 100644
index 0000000000..cb2ed32404
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
@@ -0,0 +1,261 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Path = System.IO.Path;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class DlcManagerWindow : StyleableWindow
+    {
+        private readonly List<DlcContainer> _dlcContainerList;
+        private readonly string _dlcJsonPath;
+
+        public VirtualFileSystem VirtualFileSystem { get; }
+
+        public AvaloniaList<DlcModel> Dlcs { get; set; }
+        public Grid DlcGrid { get; private set; }
+        public ulong TitleId { get; }
+        public string TitleName { get; }
+
+        public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
+
+        public DlcManagerWindow()
+        {
+            DataContext = this;
+
+            InitializeComponent();
+            AttachDebugDevTools();
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
+        }
+
+        public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+        {
+            VirtualFileSystem = virtualFileSystem;
+            TitleId = titleId;
+            TitleName = titleName;
+
+            _dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
+
+            try
+            {
+                _dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
+            }
+            catch
+            {
+                _dlcContainerList = new List<DlcContainer>();
+            }
+
+            DataContext = this;
+
+            InitializeComponent();
+            AttachDebugDevTools();
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
+
+            LoadDlcs();
+        }
+
+        [Conditional("DEBUG")]
+        private void AttachDebugDevTools()
+        {
+            this.AttachDevTools();
+        }
+
+        private void InitializeComponent()
+        {
+            Dlcs = new AvaloniaList<DlcModel>();
+
+            AvaloniaXamlLoader.Load(this);
+
+            DlcGrid = this.FindControl<Grid>("DlcGrid");
+        }
+
+        private void LoadDlcs()
+        {
+            foreach (DlcContainer dlcContainer in _dlcContainerList)
+            {
+                using FileStream containerFile = File.OpenRead(dlcContainer.Path);
+
+                PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+
+                VirtualFileSystem.ImportTickets(pfs);
+
+                foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
+                {
+                    using var ncaFile = new UniqueRef<IFile>();
+                    pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+                    Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
+
+                    if (nca != null)
+                    {
+                        Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
+                            dlcNca.Enabled));
+                    }
+                }
+            }
+        }
+
+        private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
+        {
+            try
+            {
+                return new Nca(VirtualFileSystem.KeySet, ncaStorage);
+            }
+            catch (Exception ex)
+            {
+                ContentDialogHelper.CreateErrorDialog(this,
+                    string.Format(LocaleManager.Instance[
+                        "DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
+            }
+
+            return null;
+        }
+
+        private void AddDlc(string path)
+        {
+            if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
+            {
+                return;
+            }
+
+            using (FileStream containerFile = File.OpenRead(path))
+            {
+                PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+                bool containsDlc = false;
+
+                VirtualFileSystem.ImportTickets(pfs);
+
+                foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+                {
+                    using var ncaFile = new UniqueRef<IFile>();
+
+                    pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+                    Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
+
+                    if (nca == null)
+                    {
+                        continue;
+                    }
+
+                    if (nca.Header.ContentType == NcaContentType.PublicData)
+                    {
+                        if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
+                        {
+                            break;
+                        }
+
+                        Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
+
+                        containsDlc = true;
+                    }
+                }
+
+                if (!containsDlc)
+                {
+                    ContentDialogHelper.CreateErrorDialog(this, LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
+                }
+            }
+        }
+
+        private void RemoveDlcs(bool removeSelectedOnly = false)
+        {
+            if (removeSelectedOnly)
+            {
+               Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
+            }
+            else
+            {
+                Dlcs.Clear();
+            }
+        }
+
+        public void RemoveSelected()
+        {
+            RemoveDlcs(true);
+        }
+
+        public void RemoveAll()
+        {
+            RemoveDlcs();
+        }
+
+        public async void Add()
+        {
+            OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
+
+            dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
+
+            string[] files = await dialog.ShowAsync(this);
+
+            if (files != null)
+            {
+                foreach (string file in files)
+                {
+                    AddDlc(file);
+                }
+            }
+        }
+
+        public void Save()
+        {
+            _dlcContainerList.Clear();
+
+            DlcContainer container = default;
+
+            foreach (DlcModel dlc in Dlcs)
+            {
+                if (container.Path != dlc.ContainerPath)
+                {
+                    if (!string.IsNullOrWhiteSpace(container.Path))
+                    {
+                        _dlcContainerList.Add(container);
+                    }
+
+                    container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
+                }
+
+                container.DlcNcaList.Add(new DlcNca
+                {
+                    Enabled = dlc.IsEnabled,
+                    TitleId = Convert.ToUInt64(dlc.TitleId, 16),
+                    Path = dlc.FullPath
+                });
+            }
+
+            if (!string.IsNullOrWhiteSpace(container.Path))
+            {
+                _dlcContainerList.Add(container);
+            }
+
+            using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
+            {
+                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
+            }
+
+            Close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
new file mode 100644
index 0000000000..347c2cf527
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
@@ -0,0 +1,104 @@
+<window:StyleableWindow
+    x:Class="Ryujinx.Ava.Ui.Windows.TitleUpdateWindow"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+    xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+    SizeToContent="Height"
+    Width="600" MinHeight="500" Height="500"
+    WindowStartupLocation="CenterOwner"
+    MinWidth="600"
+    mc:Ignorable="d">
+    <Grid Margin="15">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <TextBlock
+            Grid.Row="1"
+            Margin="20,15,20,20"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            MaxWidth="500"
+            LineHeight="18"
+            TextWrapping="Wrap"
+            Text="{Binding Heading}"
+            TextAlignment="Center" />
+        <Border
+            Grid.Row="2"
+            Margin="5"
+            HorizontalAlignment="Stretch"
+            VerticalAlignment="Stretch"
+            BorderBrush="Gray"
+            BorderThickness="1">
+            <ScrollViewer
+                Width="550"
+                MinHeight="200"
+                VerticalAlignment="Stretch"
+                HorizontalScrollBarVisibility="Auto"
+                VerticalScrollBarVisibility="Auto">
+                <ItemsControl
+                    Margin="10"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Items="{Binding TitleUpdates}">
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate>
+                            <RadioButton Padding="8, 0" VerticalContentAlignment="Center" GroupName="Update" IsChecked="{Binding IsEnabled, Mode=TwoWay}">
+                                <Label Margin="0" VerticalAlignment="Center" Content="{Binding Label}" />
+                            </RadioButton>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </ScrollViewer>
+        </Border>
+        <DockPanel
+            Grid.Row="3"
+            Margin="0"
+            HorizontalAlignment="Stretch">
+            <DockPanel Margin="0" HorizontalAlignment="Left">
+                <Button
+                    Name="AddButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Add}">
+                    <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
+                </Button>
+                <Button
+                    Name="RemoveButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding RemoveSelected}">
+                    <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
+                </Button>
+                <Button
+                    Name="RemoveAllButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding RemoveAll}">
+                    <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
+                </Button>
+            </DockPanel>
+            <DockPanel Margin="0" HorizontalAlignment="Right">
+                <Button
+                    Name="SaveButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Save}">
+                    <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+                </Button>
+                <Button
+                    Name="CancelButton"
+                    MinWidth="90"
+                    Margin="5"
+                    Command="{Binding Close}">
+                    <TextBlock Text="{locale:Locale InputDialogCancel}" />
+                </Button>
+            </DockPanel>
+        </DockPanel>
+    </Grid>
+</window:StyleableWindow>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
new file mode 100644
index 0000000000..edc1abcd98
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
@@ -0,0 +1,265 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Ns;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Path = System.IO.Path;
+using SpanHelpers = LibHac.Common.SpanHelpers;
+using LibHac.Tools.FsSystem;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class TitleUpdateWindow : StyleableWindow
+    {
+        private readonly string _updateJsonPath;
+        private TitleUpdateMetadata _titleUpdateWindowData;
+
+        public VirtualFileSystem VirtualFileSystem { get; }
+
+        internal AvaloniaList<TitleUpdateModel> TitleUpdates { get; set; }
+        public string TitleId { get; }
+        public string TitleName { get; }
+
+        public string Heading => string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], TitleName, TitleId.ToUpper());
+
+        public TitleUpdateWindow()
+        {
+            DataContext = this;
+
+            InitializeComponent();
+            AttachDebugDevTools();
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
+        }
+
+        public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
+        {
+            VirtualFileSystem = virtualFileSystem;
+            TitleId = titleId;
+            TitleName = titleName;
+
+            _updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
+
+            try
+            {
+                _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
+            }
+            catch
+            {
+                _titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
+            }
+
+            DataContext = this;
+
+            InitializeComponent();
+            AttachDebugDevTools();
+            
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
+
+            LoadUpdates();
+        }
+
+        [Conditional("DEBUG")]
+        private void AttachDebugDevTools()
+        {
+            this.AttachDevTools();
+        }
+
+        private void InitializeComponent()
+        {
+            TitleUpdates = new AvaloniaList<TitleUpdateModel>();
+
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void LoadUpdates()
+        {
+            TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
+
+            foreach (string path in _titleUpdateWindowData.Paths)
+            {
+                AddUpdate(path);
+            }
+
+            if (_titleUpdateWindowData.Selected == "")
+            {
+                TitleUpdates[0].IsEnabled = true;
+            }
+            else
+            {
+                TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
+                List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
+
+                foreach (TitleUpdateModel update in enabled)
+                {
+                    update.IsEnabled = false;
+                }
+
+                if (selected != null)
+                {
+                    selected.IsEnabled = true;
+                }
+            }
+
+            SortUpdates();
+        }
+
+        private void AddUpdate(string path)
+        {
+            if (File.Exists(path) && !TitleUpdates.Any(x => x.Path == path))
+            {
+                using (FileStream file = new(path, FileMode.Open, FileAccess.Read))
+                {
+                    PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
+
+                    try
+                    {
+                        (Nca patchNca, Nca controlNca) =
+                            ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
+
+                        if (controlNca != null && patchNca != null)
+                        {
+                            ApplicationControlProperty controlData = new ApplicationControlProperty();
+
+                            using var nacpFile = new UniqueRef<IFile>();
+
+                            controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
+                                .OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
+                                .ThrowIfFailure();
+                            nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
+                                .ThrowIfFailure();
+
+                            TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+                        }
+                        else
+                        {
+                            ContentDialogHelper.CreateErrorDialog(this,
+                                LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        ContentDialogHelper.CreateErrorDialog(this,
+                            string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
+                    }
+                }
+            }
+        }
+
+        private void RemoveUpdates(bool removeSelectedOnly = false)
+        {
+            if (removeSelectedOnly)
+            {
+                TitleUpdates.RemoveAll(TitleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
+            }
+            else
+            {
+                TitleUpdates.RemoveAll(TitleUpdates.Where(x => !x.IsNoUpdate).ToList());
+            }
+
+            TitleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
+
+            SortUpdates();
+        }
+
+        public void RemoveSelected()
+        {
+            RemoveUpdates(true);
+        }
+
+        public void RemoveAll()
+        {
+            RemoveUpdates();
+        }
+
+        public async void Add()
+        {
+            OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
+
+            dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
+
+            string[] files = await dialog.ShowAsync(this);
+
+            if (files != null)
+            {
+                foreach (string file in files)
+                {
+                    AddUpdate(file);
+                }
+            }
+
+            SortUpdates();
+        }
+
+        private void SortUpdates()
+        {
+            var list = TitleUpdates.ToList();
+
+            list.Sort((first, second) =>
+            {
+                if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
+                {
+                    return -1;
+                }
+                else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
+                {
+                    return 1;
+                }
+
+                return Version.Parse(first.Control.DisplayVersionString.ToString())
+                    .CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
+            });
+
+            TitleUpdates.Clear();
+
+            TitleUpdates.AddRange(list);
+        }
+
+        public void Save()
+        {
+            _titleUpdateWindowData.Paths.Clear();
+
+            _titleUpdateWindowData.Selected = "";
+
+            foreach (TitleUpdateModel update in TitleUpdates)
+            {
+                _titleUpdateWindowData.Paths.Add(update.Path);
+
+                if (update.IsEnabled)
+                {
+                    _titleUpdateWindowData.Selected = update.Path;
+                }
+            }
+
+            using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
+            {
+                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
+            }
+
+            if (Owner is MainWindow window)
+            {
+                window.ViewModel.LoadApplications();
+            }
+
+            Close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
new file mode 100644
index 0000000000..4b004206c8
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
@@ -0,0 +1,107 @@
+<window:StyleableWindow 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.Windows.UserProfileWindow"
+                        xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+                        xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+                        xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+                        xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+                        CanResize="False"
+                        Width="850" MinHeight="550" Height="550"
+                        WindowStartupLocation="CenterOwner"
+                        SizeToContent="Manual"
+                        MinWidth="600">
+    <Design.DataContext>
+        <viewModels:UserProfileViewModel />
+    </Design.DataContext>
+    <Window.Resources>
+        <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+    </Window.Resources>
+    <Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <Grid Grid.Row="1">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition />
+            </Grid.RowDefinitions>
+            <ContentControl
+                Focusable="False"
+                IsVisible="False"
+                KeyboardNavigation.IsTabStop="False">
+                <ui:ContentDialog Name="ContentDialog"
+                                  IsPrimaryButtonEnabled="True"
+                                  IsSecondaryButtonEnabled="True"
+                                  IsVisible="False" />
+            </ContentControl>
+            <TextBlock Text="{Locale:Locale UserProfilesSelectedUserProfile}" />
+            <Grid Grid.Row="1" Margin="10">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="Auto" />
+                    <ColumnDefinition />
+                    <ColumnDefinition Width="Auto" />
+                </Grid.ColumnDefinitions>
+                <Image Height="96" Width="96"
+                       Source="{Binding SelectedProfile.Image, Converter={StaticResource ByteImage}}" />
+                <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
+                            Margin="5, 10">
+                    <TextBox Name="NameBox" Text="{Binding SelectedProfile.Name, Mode=OneWay}"
+                             HorizontalAlignment="Stretch" />
+                    <TextBlock Text="{Binding SelectedProfile.UserId}" />
+                </StackPanel>
+                <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="2" Spacing="10"
+                            Margin="5">
+                    <Button Content="{Locale:Locale UserProfilesSaveProfileName}" Name="SetNameButton"
+                            Click="SetNameButton_OnClick" />
+                    <Button Name="SelectProfileImage" Command="{Binding ChooseProfileImage}"
+                            Content="{Locale:Locale UserProfilesChangeProfileImage}" />
+                </StackPanel>
+            </Grid>
+        </Grid>
+        <Grid Grid.Row="2">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition />
+            </Grid.RowDefinitions>
+            <TextBlock Text="{Locale:Locale UserProfilesAvailableUserProfiles}" />
+            <ListBox Grid.Row="1" Margin="10" Name="ProfilesList" DoubleTapped="ProfilesList_DoubleTapped"
+                     Items="{Binding Profiles}">
+                <ListBox.ItemTemplate>
+                    <DataTemplate>
+                        <Grid HorizontalAlignment="Stretch">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" />
+                                <ColumnDefinition />
+                            </Grid.ColumnDefinitions>
+                            <Grid Grid.Column="0" Background="{DynamicResource ThemeAccentColorBrush}"
+                                  Grid.ColumnSpan="2"
+                                  HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="5" MinWidth="5"
+                                  IsVisible="{Binding IsOpened}" />
+                            <Image Grid.Column="0" Height="96" Width="96"
+                                   Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+                            <StackPanel Margin="10" Orientation="Vertical" HorizontalAlignment="Stretch"
+                                        VerticalAlignment="Center" Grid.Column="1">
+                                <TextBlock Text="{Binding Name}" />
+                                <TextBlock Text="{Binding  UserId}" />
+                            </StackPanel>
+                        </Grid>
+                    </DataTemplate>
+                </ListBox.ItemTemplate>
+            </ListBox>
+        </Grid>
+        <StackPanel Grid.Row="3" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Stretch">
+            <Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
+            <Button IsEnabled="{Binding IsSelectedProfileDeletable}"
+                    Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
+            <Button HorizontalAlignment="Right" Content="{Locale:Locale UserProfilesClose}" Click="CloseButton_OnClick"
+                    Name="CloseButton" />
+        </StackPanel>
+    </Grid>
+</window:StyleableWindow>
\ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
new file mode 100644
index 0000000000..e78e0384f3
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
@@ -0,0 +1,102 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Threading.Tasks;
+using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+    public class UserProfileWindow : StyleableWindow
+    {
+        private TextBox _nameBox;
+
+        public UserProfileWindow(AccountManager accountManager, ContentManager contentManager,
+            VirtualFileSystem virtualFileSystem)
+        {
+            AccountManager = accountManager;
+            ContentManager = contentManager;
+            ViewModel = new UserProfileViewModel(this);
+
+            DataContext = ViewModel;
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            if (contentManager.GetCurrentFirmwareVersion() != null)
+            {
+                Task.Run(() =>
+                {
+                    AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
+                });
+            }
+
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
+        }
+
+        public UserProfileWindow()
+        {
+            ViewModel = new UserProfileViewModel();
+
+            DataContext = ViewModel;
+
+            InitializeComponent();
+#if DEBUG
+            this.AttachDevTools();
+#endif
+            Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
+        }
+
+        public AccountManager AccountManager { get; }
+        public ContentManager ContentManager { get; }
+
+        public UserProfileViewModel ViewModel { get; set; }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+            _nameBox = this.FindControl<TextBox>("NameBox");
+        }
+
+        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];
+
+                    AccountManager.OpenUser(ViewModel.SelectedProfile.UserId);
+
+                    ViewModel.LoadProfiles();
+
+                    foreach (UserProfile profile in ViewModel.Profiles)
+                    {
+                        profile.UpdateState();
+                    }
+                }
+            }
+        }
+
+        private void CloseButton_OnClick(object sender, RoutedEventArgs e)
+        {
+            Close();
+        }
+
+        private void SetNameButton_OnClick(object sender, RoutedEventArgs e)
+        {
+            if (!string.IsNullOrWhiteSpace(_nameBox.Text))
+            {
+                ViewModel.SelectedProfile.Name = _nameBox.Text;
+                AccountManager.SetUserName(ViewModel.SelectedProfile.UserId, _nameBox.Text);
+            }
+        }
+    }
+}
\ No newline at end of file