aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/ViewModels
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-07-08 18:47:11 +0000
committerGitHub <noreply@github.com>2022-07-08 15:47:11 -0300
commit3af42d6c7e9e71c504b87a7b0f7f960fe83418fb (patch)
tree2b27f12ee0273d5316229d31383619d915c3210d /Ryujinx.Ava/Ui/ViewModels
parentbccf5e8b5a8f3870dbf03bedb0eb46b85b78d5f4 (diff)
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
Diffstat (limited to 'Ryujinx.Ava/Ui/ViewModels')
-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
4 files changed, 1040 insertions, 18 deletions
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