From a47824f96101a1c1e63b7622f0c4e61ba6345a98 Mon Sep 17 00:00:00 2001
From: Ac_K <Acoustik666@gmail.com>
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<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
+        private static bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> 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<ApplicationControlProperty> controlData, string titleName)
+        public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, BlitStruct<ApplicationControlProperty> 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<IFile>();
+
+                            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<IFile>();
-
-                                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<IFileSystem>(ncaFileSystem);
+                        using var uniqueOutputFs = new UniqueRef<IFileSystem>(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<IFileSystem>(ncaFileSystem);
-                            using var uniqueOutputFs = new UniqueRef<IFileSystem>(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">
     <UserControl.Resources>
         <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
-        <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+        <MenuFlyout x:Key="GameContextMenu">
             <MenuItem
                 Command="{Binding ToggleFavorite}"
                 Header="{locale:Locale GameListContextMenuToggleFavorite}"
@@ -22,14 +22,17 @@
             <Separator />
             <MenuItem
                 Command="{Binding OpenUserSaveDirectory}"
+                IsEnabled="{Binding EnabledUserSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
             <MenuItem
                 Command="{Binding OpenDeviceSaveDirectory}"
+                IsEnabled="{Binding EnabledDeviceSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
             <MenuItem
                 Command="{Binding OpenBcatSaveDirectory}"
+                IsEnabled="{Binding EnabledBcatSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
             <Separator />
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<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
             RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
 
         public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
-                    ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
-                    ((menu.Items as AvaloniaList<object>)[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">
     <UserControl.Resources>
         <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
-        <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+        <MenuFlyout x:Key="GameContextMenu">
             <MenuItem
                 Command="{Binding ToggleFavorite}"
                 Header="{locale:Locale GameListContextMenuToggleFavorite}"
@@ -21,14 +21,17 @@
             <Separator />
             <MenuItem
                 Command="{Binding OpenUserSaveDirectory}"
+                IsEnabled="{Binding EnabledUserSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
             <MenuItem
                 Command="{Binding OpenDeviceSaveDirectory}"
+                IsEnabled="{Binding EnabledDeviceSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
             <MenuItem
                 Command="{Binding OpenBcatSaveDirectory}"
+                IsEnabled="{Binding EnabledBcatSaveDirectory}"
                 Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
                 ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
             <Separator />
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<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
             RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
 
         public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
-                    ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
-                    ((menu.Items as AvaloniaList<object>)[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<Notification> _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)