diff options
| author | Emmanuel Hansen <emmausssss@gmail.com> | 2022-07-08 18:47:11 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-08 15:47:11 -0300 |
| commit | 3af42d6c7e9e71c504b87a7b0f7f960fe83418fb (patch) | |
| tree | 2b27f12ee0273d5316229d31383619d915c3210d /Ryujinx.Ava/Ui/ViewModels | |
| parent | bccf5e8b5a8f3870dbf03bedb0eb46b85b78d5f4 (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.cs | 450 | ||||
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs | 363 | ||||
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs | 79 | ||||
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs | 166 |
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 |
