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
This commit is contained in:
Ac_K 2023-01-21 02:57:37 +01:00 committed by GitHub
parent 8474d52778
commit a47824f961
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 329 deletions

View file

@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading; using Avalonia.Threading;
using LibHac; using LibHac;
using LibHac.Account; using LibHac.Account;
@ -12,7 +13,6 @@ using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -44,14 +44,11 @@ namespace Ryujinx.Ava.Common
_accountManager = accountManager; _accountManager = accountManager;
} }
private static bool TryFindSaveData(string titleName, ulong titleId, private static bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
{ {
saveDataId = default; saveDataId = default;
Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter);
SaveDataSpaceId.User, in filter);
if (ResultFs.TargetNotFound.Includes(result)) if (ResultFs.TargetNotFound.Includes(result))
{ {
ref ApplicationControlProperty control = ref controlHolder.Value; ref ApplicationControlProperty control = ref controlHolder.Value;
@ -68,17 +65,15 @@ namespace Ryujinx.Ava.Common
control.UserAccountSaveDataSize = 0x4000; control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000; control.UserAccountSaveDataJournalSize = 0x4000;
Logger.Warning?.Print(LogClass.Application, 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.");
"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); result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
if (result.IsFailure()) if (result.IsFailure())
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageCreateSaveErrorMessage, result.ToStringWithName())); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageCreateSaveErrorMessage, result.ToStringWithName()));
}); });
@ -97,7 +92,7 @@ namespace Ryujinx.Ava.Common
return true; return true;
} }
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageFindSaveErrorMessage, result.ToStringWithName())); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageFindSaveErrorMessage, result.ToStringWithName()));
}); });
@ -105,8 +100,7 @@ namespace Ryujinx.Ava.Common
return false; return false;
} }
public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, BlitStruct<ApplicationControlProperty> controlData, string titleName)
BlitStruct<ApplicationControlProperty> controlData, string titleName)
{ {
if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId)) if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId))
{ {
@ -147,13 +141,14 @@ namespace Ryujinx.Ava.Common
} }
} }
public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0)
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); string destination = await folderDialog.ShowAsync(_owner);
var cancellationToken = new CancellationTokenSource(); var cancellationToken = new CancellationTokenSource();
if (!string.IsNullOrWhiteSpace(destination)) if (!string.IsNullOrWhiteSpace(destination))
@ -175,24 +170,19 @@ namespace Ryujinx.Ava.Common
} }
}); });
Thread.Sleep(1000); using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
using (FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read))
{
Nca mainNca = null; Nca mainNca = null;
Nca patchNca = null; Nca patchNca = null;
string extension = Path.GetExtension(titleFilePath).ToLower(); string extension = Path.GetExtension(titleFilePath).ToLower();
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{ {
PartitionFileSystem pfs; PartitionFileSystem pfs;
if (extension == ".xci") if (extension == ".xci")
{ {
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
pfs = xci.OpenPartition(XciPartitionType.Secure);
} }
else else
{ {
@ -206,11 +196,9 @@ namespace Ryujinx.Ava.Common
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType == NcaContentType.Program) if (nca.Header.ContentType == NcaContentType.Program)
{ {
int dataIndex = int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{ {
patchNca = nca; patchNca = nca;
@ -229,17 +217,17 @@ namespace Ryujinx.Ava.Common
if (mainNca == null) if (mainNca == null)
{ {
Logger.Error?.Print(LogClass.Application, Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file");
"Extraction failure. The main NCA was not present in the selected file");
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
}); });
return; return;
} }
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
if (updatePatchNca != null) if (updatePatchNca != null)
{ {
patchNca = updatePatchNca; patchNca = updatePatchNca;
@ -270,8 +258,8 @@ namespace Ryujinx.Ava.Common
{ {
if (resultCode.Value.IsFailure()) if (resultCode.Value.IsFailure())
{ {
Logger.Error?.Print(LogClass.Application, Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
$"LibHac returned error code: {resultCode.Value.ErrorCode}");
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
@ -279,15 +267,10 @@ namespace Ryujinx.Ava.Common
} }
else if (resultCode.Value.IsSuccess()) else if (resultCode.Value.IsSuccess())
{ {
Dispatcher.UIThread.InvokeAsync(async () => NotificationHelper.Show(
{ LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle],
await ContentDialogHelper.CreateInfoDialog( $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}",
LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage], NotificationType.Information);
"",
LocaleManager.Instance[LocaleKeys.InputDialogOk],
"",
LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle]);
});
} }
} }
@ -296,12 +279,13 @@ namespace Ryujinx.Ava.Common
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
Logger.Error?.Print(LogClass.Application, $"{ex.Message}");
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.CreateErrorDialog(ex.Message); await ContentDialogHelper.CreateErrorDialog(ex.Message);
}); });
} }
}
}); });
extractorThread.Name = "GUI.NcaSectionExtractorThread"; extractorThread.Name = "GUI.NcaSectionExtractorThread";

View file

@ -14,7 +14,7 @@
Focusable="True"> Focusable="True">
<UserControl.Resources> <UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" /> <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened"> <MenuFlyout x:Key="GameContextMenu">
<MenuItem <MenuItem
Command="{Binding ToggleFavorite}" Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}" Header="{locale:Locale GameListContextMenuToggleFavorite}"
@ -22,14 +22,17 @@
<Separator /> <Separator />
<MenuItem <MenuItem
Command="{Binding OpenUserSaveDirectory}" Command="{Binding OpenUserSaveDirectory}"
IsEnabled="{Binding EnabledUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem <MenuItem
Command="{Binding OpenDeviceSaveDirectory}" Command="{Binding OpenDeviceSaveDirectory}"
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
<MenuItem <MenuItem
Command="{Binding OpenBcatSaveDirectory}" Command="{Binding OpenBcatSaveDirectory}"
IsEnabled="{Binding EnabledBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
<Separator /> <Separator />

View file

@ -1,9 +1,7 @@
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using LibHac.Common;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
@ -13,7 +11,6 @@ namespace Ryujinx.Ava.UI.Controls
{ {
public partial class GameGridView : UserControl public partial class GameGridView : UserControl
{ {
private ApplicationData _selectedApplication;
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
@ -23,6 +20,16 @@ namespace Ryujinx.Ava.UI.Controls
remove { RemoveHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); }
} }
public GameGridView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void GameList_DoubleTapped(object sender, RoutedEventArgs args) public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
{ {
if (sender is ListBox listBox) if (sender is ListBox listBox)
@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls
{ {
if (sender is ListBox listBox) if (sender is ListBox listBox)
{ {
_selectedApplication = listBox.SelectedItem as ApplicationData; (DataContext as MainWindowViewModel).GridSelectedApplication = listBox.SelectedItem as ApplicationData;
(DataContext as MainWindowViewModel).GridSelectedApplication = _selectedApplication;
} }
} }
public ApplicationData SelectedApplication => _selectedApplication;
public GameGridView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{ {
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; (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;
}
}
}
} }
} }

View file

@ -13,7 +13,7 @@
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" /> <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened"> <MenuFlyout x:Key="GameContextMenu">
<MenuItem <MenuItem
Command="{Binding ToggleFavorite}" Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}" Header="{locale:Locale GameListContextMenuToggleFavorite}"
@ -21,14 +21,17 @@
<Separator /> <Separator />
<MenuItem <MenuItem
Command="{Binding OpenUserSaveDirectory}" Command="{Binding OpenUserSaveDirectory}"
IsEnabled="{Binding EnabledUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem <MenuItem
Command="{Binding OpenDeviceSaveDirectory}" Command="{Binding OpenDeviceSaveDirectory}"
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
<MenuItem <MenuItem
Command="{Binding OpenBcatSaveDirectory}" Command="{Binding OpenBcatSaveDirectory}"
IsEnabled="{Binding EnabledBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}" Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
<Separator /> <Separator />

View file

@ -1,9 +1,7 @@
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using LibHac.Common;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
@ -13,7 +11,6 @@ namespace Ryujinx.Ava.UI.Controls
{ {
public partial class GameListView : UserControl public partial class GameListView : UserControl
{ {
private ApplicationData _selectedApplication;
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
@ -23,6 +20,16 @@ namespace Ryujinx.Ava.UI.Controls
remove { RemoveHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); }
} }
public GameListView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void GameList_DoubleTapped(object sender, RoutedEventArgs args) public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
{ {
if (sender is ListBox listBox) if (sender is ListBox listBox)
@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls
{ {
if (sender is ListBox listBox) if (sender is ListBox listBox)
{ {
_selectedApplication = listBox.SelectedItem as ApplicationData; (DataContext as MainWindowViewModel).ListSelectedApplication = listBox.SelectedItem as ApplicationData;
(DataContext as MainWindowViewModel).ListSelectedApplication = _selectedApplication;
} }
} }
public ApplicationData SelectedApplication => _selectedApplication;
public GameListView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{ {
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; (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;
}
}
}
} }
} }

View file

@ -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);
}
}
}

View file

@ -5,7 +5,7 @@ using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using LibHac.Bcat; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Tools.Fs; 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 public string LoadHeading
{ {
get => _loadHeading; get => _loadHeading;
@ -735,19 +741,14 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
get get
{ {
switch (ConfigurationState.Instance.Ui.GridSize) return ConfigurationState.Instance.Ui.GridSize.Value switch
{ {
case 1: 1 => 78,
return 78; 2 => 100,
case 2: 3 => 120,
return 100; 4 => 140,
case 3: _ => 16,
return 120; };
case 4:
return 140;
default:
return 16;
}
} }
} }
@ -755,19 +756,14 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
get get
{ {
switch (ConfigurationState.Instance.Ui.GridSize) return ConfigurationState.Instance.Ui.GridSize.Value switch
{ {
case 1: 1 => 120,
return 120; 2 => ShowNames ? 210 : 150,
case 2: 3 => ShowNames ? 240 : 180,
return ShowNames ? 210 : 150; 4 => ShowNames ? 280 : 220,
case 3: _ => 16,
return ShowNames ? 240 : 180; };
case 4:
return ShowNames ? 280 : 220;
default:
return 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() private async void ExtractLogo()
{ {
var selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); await ApplicationHelper.ExtractSection(NcaSectionType.Logo, SelectedApplication.Path, SelectedApplication.TitleName);
} }
} }
private async void ExtractRomFs() private async void ExtractRomFs()
{ {
var selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); await ApplicationHelper.ExtractSection(NcaSectionType.Data, SelectedApplication.Path, SelectedApplication.TitleName);
} }
} }
private async void ExtractExeFs() private async void ExtractExeFs()
{ {
var selection = SelectedApplication; if (SelectedApplication != null)
if (selection != 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() public void ToggleFavorite()
{ {
ApplicationData selection = SelectedApplication; ApplicationData selection = SelectedApplication;
@ -1555,37 +1493,45 @@ namespace Ryujinx.Ava.UI.ViewModels
public void OpenUserSaveDirectory() public void OpenUserSaveDirectory()
{ {
ApplicationData selection = SelectedApplication; OpenSaveDirectory(SaveDataType.Account, userId: new UserId((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low));
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); 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)
{
if (!ulong.TryParse(SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
});
return; return;
} }
UserId userId = new((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low); var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
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() public void OpenModsDirectory()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@ -1593,12 +1539,10 @@ namespace Ryujinx.Ava.UI.ViewModels
public void OpenSdModsDirectory() public void OpenSdModsDirectory()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath(); string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@ -1614,25 +1558,17 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void OpenDownloadableContentManager() public async void OpenDownloadableContentManager()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
{
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
}
} }
} }
public async void OpenCheatManager() public async void OpenCheatManager()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) await new CheatWindow(VirtualFileSystem, SelectedApplication.TitleId, SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
{
await new CheatWindow(VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(desktop.MainWindow);
}
} }
} }

View file

@ -104,6 +104,8 @@ namespace Ryujinx.Ava.UI.Windows
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
ViewModel.ReloadGameList += ReloadGameList; ViewModel.ReloadGameList += ReloadGameList;
NotificationHelper.SetNotificationManager(this);
} }
private void IsActiveChanged(bool obj) private void IsActiveChanged(bool obj)