aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.Ava/Assets/Locales/en_US.json18
-rw-r--r--Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml35
-rw-r--r--Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs105
-rw-r--r--Ryujinx.Ava/Ui/Models/Amiibo.cs72
-rw-r--r--Ryujinx.Ava/Ui/Models/CheatModel.cs37
-rw-r--r--Ryujinx.Ava/Ui/Models/CheatsList.cs51
-rw-r--r--Ryujinx.Ava/Ui/Models/DlcModel.cs18
-rw-r--r--Ryujinx.Ava/Ui/Models/ProfileImageModel.cs2
-rw-r--r--Ryujinx.Ava/Ui/Models/TitleUpdateModel.cs7
-rw-r--r--Ryujinx.Ava/Ui/Models/UserProfile.cs61
-rw-r--r--Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs450
-rw-r--r--Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs363
-rw-r--r--Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs79
-rw-r--r--Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs166
-rw-r--r--Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml68
-rw-r--r--Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs70
-rw-r--r--Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml53
-rw-r--r--Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs71
-rw-r--r--Ryujinx.Ava/Ui/Windows/CheatWindow.axaml90
-rw-r--r--Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs137
-rw-r--r--Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml132
-rw-r--r--Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs261
-rw-r--r--Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml104
-rw-r--r--Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs265
-rw-r--r--Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml107
-rw-r--r--Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs102
26 files changed, 2901 insertions, 23 deletions
diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json
index ebf30df6..d9483db4 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 00000000..c6f43f43
--- /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 00000000..728b8906
--- /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 00000000..8644ab52
--- /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 00000000..cdab27cd
--- /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 00000000..f2b0592e
--- /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 00000000..7e5f4a62
--- /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 a23b55cc..1c9f3b05 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 f864e70a..2bf6dbfa 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 00000000..351ada76
--- /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 00000000..9f411ba2
--- /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 00000000..c2983741
--- /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 dbc19f75..bc8e6450 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);
+
+ await window.ShowDialog(_owner);
+
+ if (window.IsScanned)
+ {
+ _showAll = window.ViewModel.ShowAllAmiibo;
+ _lastScannedAmiiboId = window.ScannedAmiibo.GetId();
- // TODO : Implement Amiibo window
- ContentDialogHelper.ShowNotAvailableMessage(_owner);
+ _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 00000000..d75f65b1
--- /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 00000000..f91bb313
--- /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 00000000..bd0935a9
--- /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 00000000..6c7576bc
--- /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 00000000..25e89846
--- /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 00000000..1685ee80
--- /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 00000000..33abeb8d
--- /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 00000000..94b3895e
--- /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 00000000..cb2ed324
--- /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 00000000..347c2cf5
--- /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 00000000..edc1abcd
--- /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 00000000..4b004206
--- /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 00000000..e78e0384
--- /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