From a47824f96101a1c1e63b7622f0c4e61ba6345a98 Mon Sep 17 00:00:00 2001 From: Ac_K Date: Sat, 21 Jan 2023 02:57:37 +0100 Subject: [PATCH] Ava UI: Add Notifications and Cleanup (#4275) * Ava UI: Add Notifications and Cleanup * Revert notifications on ErrorDialog * remove unused code from game list views * Fix cast --- Ryujinx.Ava/Common/ApplicationHelper.cs | 240 ++++++++---------- Ryujinx.Ava/UI/Controls/GameGridView.axaml | 5 +- Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs | 50 +--- Ryujinx.Ava/UI/Controls/GameListView.axaml | 5 +- Ryujinx.Ava/UI/Controls/GameListView.axaml.cs | 50 +--- Ryujinx.Ava/UI/Helpers/NotificationHelper.cs | 65 +++++ .../UI/ViewModels/MainWindowViewModel.cs | 182 +++++-------- Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs | 2 + 8 files changed, 270 insertions(+), 329 deletions(-) create mode 100644 Ryujinx.Ava/UI/Helpers/NotificationHelper.cs diff --git a/Ryujinx.Ava/Common/ApplicationHelper.cs b/Ryujinx.Ava/Common/ApplicationHelper.cs index 2dc3d37ff8..0b8bd8da1a 100644 --- a/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Threading; using LibHac; using LibHac.Account; @@ -12,7 +13,6 @@ 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.Helpers; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Logging; @@ -44,14 +44,11 @@ namespace Ryujinx.Ava.Common _accountManager = accountManager; } - private static bool TryFindSaveData(string titleName, ulong titleId, - BlitStruct controlHolder, in SaveDataFilter filter, out ulong saveDataId) + private static bool TryFindSaveData(string titleName, ulong titleId, BlitStruct controlHolder, in SaveDataFilter filter, out ulong saveDataId) { saveDataId = default; - Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, - SaveDataSpaceId.User, in filter); - + Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter); if (ResultFs.TargetNotFound.Includes(result)) { ref ApplicationControlProperty control = ref controlHolder.Value; @@ -68,17 +65,15 @@ namespace Ryujinx.Ava.Common control.UserAccountSaveDataSize = 0x4000; control.UserAccountSaveDataJournalSize = 0x4000; - Logger.Warning?.Print(LogClass.Application, - "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); + Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); } - Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); + Uid user = new((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user); - if (result.IsFailure()) { - Dispatcher.UIThread.Post(async () => + Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageCreateSaveErrorMessage, result.ToStringWithName())); }); @@ -97,7 +92,7 @@ namespace Ryujinx.Ava.Common return true; } - Dispatcher.UIThread.Post(async () => + Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageFindSaveErrorMessage, result.ToStringWithName())); }); @@ -105,8 +100,7 @@ namespace Ryujinx.Ava.Common return false; } - public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, - BlitStruct controlData, string titleName) + public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, BlitStruct controlData, string titleName) { if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId)) { @@ -147,14 +141,15 @@ namespace Ryujinx.Ava.Common } } - public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, - int programIndex = 0) + public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0) { - OpenFolderDialog folderDialog = new() { Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] }; + OpenFolderDialog folderDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] + }; - string destination = await folderDialog.ShowAsync(_owner); - - var cancellationToken = new CancellationTokenSource(); + string destination = await folderDialog.ShowAsync(_owner); + var cancellationToken = new CancellationTokenSource(); if (!string.IsNullOrWhiteSpace(destination)) { @@ -174,133 +169,122 @@ namespace Ryujinx.Ava.Common cancellationToken.Cancel(); } }); + + using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); - Thread.Sleep(1000); + Nca mainNca = null; + Nca patchNca = null; - using (FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read)) + string extension = Path.GetExtension(titleFilePath).ToLower(); + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") { - Nca mainNca = null; - Nca patchNca = null; + PartitionFileSystem pfs; - string extension = Path.GetExtension(titleFilePath).ToLower(); - - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + if (extension == ".xci") { - PartitionFileSystem pfs; + pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + pfs = new PartitionFileSystem(file.AsStorage()); + } - if (extension == ".xci") + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + if (nca.Header.ContentType == NcaContentType.Program) { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - pfs = new PartitionFileSystem(file.AsStorage()); - } - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - - if (nca.Header.ContentType == NcaContentType.Program) + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) { - int dataIndex = - Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } + patchNca = nca; + } + else + { + mainNca = nca; } } } - else if (extension == ".nca") - { - mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); - } + } + else if (extension == ".nca") + { + mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); + } - if (mainNca == null) + if (mainNca == null) + { + Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file"); + + Dispatcher.UIThread.InvokeAsync(async () => { - Logger.Error?.Print(LogClass.Application, - "Extraction failure. The main NCA was not present in the selected file"); - Dispatcher.UIThread.InvokeAsync(async () => + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); + }); + + return; + } + + (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + if (updatePatchNca != null) + { + patchNca = updatePatchNca; + } + + int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); + + try + { + IFileSystem ncaFileSystem = patchNca != null + ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) + : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); + + FileSystemClient fsClient = _horizonClient.Fs; + + string source = DateTime.Now.ToFileTime().ToString()[10..]; + string output = DateTime.Now.ToFileTime().ToString()[10..]; + + using var uniqueSourceFs = new UniqueRef(ncaFileSystem); + using var uniqueOutputFs = new UniqueRef(new LocalFileSystem(destination)); + + fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref()); + fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref()); + + (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token); + + if (!canceled) + { + if (resultCode.Value.IsFailure()) { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); - }); - return; - } + Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); - (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, - mainNca.Header.TitleId.ToString("x16"), programIndex, out _); - if (updatePatchNca != null) - { - patchNca = updatePatchNca; - } - - int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); - - try - { - IFileSystem ncaFileSystem = patchNca != null - ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) - : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); - - FileSystemClient fsClient = _horizonClient.Fs; - - string source = DateTime.Now.ToFileTime().ToString()[10..]; - string output = DateTime.Now.ToFileTime().ToString()[10..]; - - using var uniqueSourceFs = new UniqueRef(ncaFileSystem); - using var uniqueOutputFs = new UniqueRef(new LocalFileSystem(destination)); - - fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref()); - fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref()); - - (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token); - - if (!canceled) - { - if (resultCode.Value.IsFailure()) + Dispatcher.UIThread.InvokeAsync(async () => { - Logger.Error?.Print(LogClass.Application, - $"LibHac returned error code: {resultCode.Value.ErrorCode}"); - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); - }); - } - else if (resultCode.Value.IsSuccess()) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateInfoDialog( - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage], - "", - LocaleManager.Instance[LocaleKeys.InputDialogOk], - "", - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle]); - }); - } + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); + }); } - - fsClient.Unmount(source.ToU8Span()); - fsClient.Unmount(output.ToU8Span()); - } - catch (ArgumentException ex) - { - Dispatcher.UIThread.InvokeAsync(async () => + else if (resultCode.Value.IsSuccess()) { - await ContentDialogHelper.CreateErrorDialog(ex.Message); - }); + NotificationHelper.Show( + LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", + NotificationType.Information); + } } + + fsClient.Unmount(source.ToU8Span()); + fsClient.Unmount(output.ToU8Span()); + } + catch (ArgumentException ex) + { + Logger.Error?.Print(LogClass.Application, $"{ex.Message}"); + + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(ex.Message); + }); } }); diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml b/Ryujinx.Ava/UI/Controls/GameGridView.axaml index 862bc6d30e..32cabfaa83 100644 --- a/Ryujinx.Ava/UI/Controls/GameGridView.axaml +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml @@ -14,7 +14,7 @@ Focusable="True"> - + diff --git a/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs b/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs index 531b54357c..aa76b7c957 100644 --- a/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs +++ b/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs @@ -1,9 +1,7 @@ -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using LibHac.Common; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ui.App.Common; @@ -13,16 +11,25 @@ namespace Ryujinx.Ava.UI.Controls { public partial class GameGridView : UserControl { - private ApplicationData _selectedApplication; public static readonly RoutedEvent ApplicationOpenedEvent = RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); public event EventHandler ApplicationOpened { - add { AddHandler(ApplicationOpenedEvent, value); } + add { AddHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); } } + public GameGridView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) { if (sender is ListBox listBox) @@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls { if (sender is ListBox listBox) { - _selectedApplication = listBox.SelectedItem as ApplicationData; - - (DataContext as MainWindowViewModel).GridSelectedApplication = _selectedApplication; + (DataContext as MainWindowViewModel).GridSelectedApplication = listBox.SelectedItem as ApplicationData; } } - public ApplicationData SelectedApplication => _selectedApplication; - - public GameGridView() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) { (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; } - - private void MenuBase_OnMenuOpened(object sender, EventArgs e) - { - var selection = SelectedApplication; - - if (selection != null) - { - if (sender is ContextMenu menu) - { - bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; - bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; - bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; - - ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; - ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; - ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; - } - } - } } } diff --git a/Ryujinx.Ava/UI/Controls/GameListView.axaml b/Ryujinx.Ava/UI/Controls/GameListView.axaml index 2ba4a204dd..c13eaae80b 100644 --- a/Ryujinx.Ava/UI/Controls/GameListView.axaml +++ b/Ryujinx.Ava/UI/Controls/GameListView.axaml @@ -13,7 +13,7 @@ mc:Ignorable="d"> - + diff --git a/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs b/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs index bded1dec7f..a64497097e 100644 --- a/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs +++ b/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs @@ -1,9 +1,7 @@ -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using LibHac.Common; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ui.App.Common; @@ -13,16 +11,25 @@ namespace Ryujinx.Ava.UI.Controls { public partial class GameListView : UserControl { - private ApplicationData _selectedApplication; public static readonly RoutedEvent ApplicationOpenedEvent = RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); public event EventHandler ApplicationOpened { - add { AddHandler(ApplicationOpenedEvent, value); } + add { AddHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); } } + public GameListView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) { if (sender is ListBox listBox) @@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls { if (sender is ListBox listBox) { - _selectedApplication = listBox.SelectedItem as ApplicationData; - - (DataContext as MainWindowViewModel).ListSelectedApplication = _selectedApplication; + (DataContext as MainWindowViewModel).ListSelectedApplication = listBox.SelectedItem as ApplicationData; } } - public ApplicationData SelectedApplication => _selectedApplication; - - public GameListView() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) { (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; } - - private void MenuBase_OnMenuOpened(object sender, EventArgs e) - { - var selection = SelectedApplication; - - if (selection != null) - { - if (sender is ContextMenu menu) - { - bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; - bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; - bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; - - ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; - ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; - ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; - } - } - } } } diff --git a/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs b/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs new file mode 100644 index 0000000000..7e2afb8bd9 --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs @@ -0,0 +1,65 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class NotificationHelper + { + private const int MaxNotifications = 4; + private const int NotificationDelayInMs = 5000; + + private static WindowNotificationManager _notificationManager; + + private static readonly ManualResetEvent _templateAppliedEvent = new(false); + private static readonly BlockingCollection _notifications = new(); + + public static void SetNotificationManager(Window host) + { + _notificationManager = new WindowNotificationManager(host) + { + Position = NotificationPosition.BottomRight, + MaxItems = MaxNotifications, + Margin = new Thickness(0, 0, 15, 40) + }; + + _notificationManager.TemplateApplied += (sender, args) => + { + _templateAppliedEvent.Set(); + }; + + Task.Run(async () => + { + _templateAppliedEvent.WaitOne(); + + foreach (var notification in _notifications.GetConsumingEnumerable()) + { + Dispatcher.UIThread.Post(() => + { + _notificationManager.Show(notification); + }); + + await Task.Delay(NotificationDelayInMs / MaxNotifications); + } + }); + } + + public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null) + { + var delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs); + + _notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); + } + + public static void ShowError(string message) + { + Show(LocaleManager.Instance[LocaleKeys.DialogErrorTitle], $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", NotificationType.Error); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 8bd146ed8e..6fea1844f3 100644 --- a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -5,7 +5,7 @@ using Avalonia.Media; using Avalonia.Threading; using DynamicData; using DynamicData.Binding; -using LibHac.Bcat; +using LibHac.Common; using LibHac.Fs; using LibHac.FsSystem; using LibHac.Tools.Fs; @@ -344,6 +344,12 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool EnabledUserSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; + + public bool EnabledDeviceSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; + + public bool EnabledBcatSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + public string LoadHeading { get => _loadHeading; @@ -735,19 +741,14 @@ namespace Ryujinx.Ava.UI.ViewModels { get { - switch (ConfigurationState.Instance.Ui.GridSize) + return ConfigurationState.Instance.Ui.GridSize.Value switch { - case 1: - return 78; - case 2: - return 100; - case 3: - return 120; - case 4: - return 140; - default: - return 16; - } + 1 => 78, + 2 => 100, + 3 => 120, + 4 => 140, + _ => 16, + }; } } @@ -755,19 +756,14 @@ namespace Ryujinx.Ava.UI.ViewModels { get { - switch (ConfigurationState.Instance.Ui.GridSize) + return ConfigurationState.Instance.Ui.GridSize.Value switch { - case 1: - return 120; - case 2: - return ShowNames ? 210 : 150; - case 3: - return ShowNames ? 240 : 180; - case 4: - return ShowNames ? 280 : 220; - default: - return 16; - } + 1 => 120, + 2 => ShowNames ? 210 : 150, + 3 => ShowNames ? 240 : 180, + 4 => ShowNames ? 280 : 220, + _ => 16, + }; } } @@ -1091,35 +1087,27 @@ namespace Ryujinx.Ava.UI.ViewModels })); } - private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId) - { - ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName); - } - private async void ExtractLogo() { - var selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); + await ApplicationHelper.ExtractSection(NcaSectionType.Logo, SelectedApplication.Path, SelectedApplication.TitleName); } } private async void ExtractRomFs() { - var selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); + await ApplicationHelper.ExtractSection(NcaSectionType.Data, SelectedApplication.Path, SelectedApplication.TitleName); } } private async void ExtractExeFs() { - var selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path); + await ApplicationHelper.ExtractSection(NcaSectionType.Code, SelectedApplication.Path, SelectedApplication.TitleName); } } @@ -1487,56 +1475,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public void OpenDeviceSaveDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - Task.Run(() => - { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - async void Action() - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - } - - Dispatcher.UIThread.Post(Action); - - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Device, userId: default, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); - } - } - - public void OpenBcatSaveDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - Task.Run(() => - { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - async void Action() - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - } - - Dispatcher.UIThread.Post(Action); - - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); - } - } - public void ToggleFavorite() { ApplicationData selection = SelectedApplication; @@ -1555,37 +1493,45 @@ namespace Ryujinx.Ava.UI.ViewModels public void OpenUserSaveDirectory() { - ApplicationData selection = SelectedApplication; - if (selection != null) + OpenSaveDirectory(SaveDataType.Account, userId: new UserId((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low)); + } + + public void OpenDeviceSaveDirectory() + { + OpenSaveDirectory(SaveDataType.Device, userId: default); + } + + public void OpenBcatSaveDirectory() + { + OpenSaveDirectory(SaveDataType.Bcat, userId: default); + } + + private void OpenSaveDirectory(SaveDataType saveDataType, UserId userId) + { + if (SelectedApplication != null) { - Task.Run(() => + if (!ulong.TryParse(SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + Dispatcher.UIThread.InvokeAsync(async () => { - async void Action() - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - } + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + }); - Dispatcher.UIThread.Post(Action); + return; + } - return; - } + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); - UserId userId = new((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low); - SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); + ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, SelectedApplication.ControlHolder, SelectedApplication.TitleName); } } public void OpenModsDirectory() { - ApplicationData selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath(); - string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -1593,12 +1539,10 @@ namespace Ryujinx.Ava.UI.ViewModels public void OpenSdModsDirectory() { - ApplicationData selection = SelectedApplication; - - if (selection != null) + if (SelectedApplication != null) { string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath(); - string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -1614,25 +1558,17 @@ namespace Ryujinx.Ava.UI.ViewModels public async void OpenDownloadableContentManager() { - ApplicationData selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); - } + await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window); } } public async void OpenCheatManager() { - ApplicationData selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - await new CheatWindow(VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(desktop.MainWindow); - } + await new CheatWindow(VirtualFileSystem, SelectedApplication.TitleId, SelectedApplication.TitleName).ShowDialog(TopLevel as Window); } } diff --git a/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index 921dfbb11a..81e055063f 100644 --- a/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -104,6 +104,8 @@ namespace Ryujinx.Ava.UI.Windows ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; ViewModel.ReloadGameList += ReloadGameList; + + NotificationHelper.SetNotificationManager(this); } private void IsActiveChanged(bool obj)