diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 64b30e211..fee589297 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -6,140 +6,166 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" x:DataType="viewModels:MainWindowViewModel"> diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index 64377ba51..6f93676e2 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Input; using LibHac.Fs; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common; @@ -26,7 +26,6 @@ namespace Ryujinx.Ava.UI.Controls { public class ApplicationContextMenu : MenuFlyout { - public ApplicationContextMenu() { InitializeComponent(); @@ -36,39 +35,35 @@ namespace Ryujinx.Ava.UI.Controls { AvaloniaXamlLoader.Load(this); } + + public static RelayCommand ToggleFavorite { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - public void ToggleFavorite_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => + { + appMetadata.Favorite = viewModel.SelectedApplication.Favorite; + }); - viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => - { - appMetadata.Favorite = viewModel.SelectedApplication.Favorite; - }); - - viewModel.RefreshView(); - } - - public void OpenUserSaveDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - OpenSaveDirectory(viewModel, SaveDataType.Account, new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low)); - } - - public void OpenDeviceSaveDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - OpenSaveDirectory(viewModel, SaveDataType.Device, default); - } - - public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - OpenSaveDirectory(viewModel, SaveDataType.Bcat, default); - } + viewModel.RefreshView(); + } + ); + + public static RelayCommand OpenUserSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + OpenSaveDirectory(viewModel, SaveDataType.Account, new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low)) + ); + + public static RelayCommand OpenDeviceSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => OpenSaveDirectory(viewModel, SaveDataType.Device, default)); + + public static RelayCommand OpenBcatSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => OpenSaveDirectory(viewModel, SaveDataType.Bcat, default)); private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId) { @@ -76,159 +71,151 @@ namespace Ryujinx.Ava.UI.Controls ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); } - - public async void OpenTitleUpdateManager_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await TitleUpdateManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); - } - - public async void OpenDownloadableContentManager_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await DownloadableContentManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); - } - - public async void OpenCheatManager_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await StyleableAppWindow.ShowAsync( + + public static AsyncRelayCommand OpenTitleUpdateManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => TitleUpdateManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication) + ); + + public static AsyncRelayCommand OpenDownloadableContentManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => DownloadableContentManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication) + ); + + public static AsyncRelayCommand OpenCheatManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => StyleableAppWindow.ShowAsync( new CheatWindow( viewModel.VirtualFileSystem, viewModel.SelectedApplication.IdString, viewModel.SelectedApplication.Name, viewModel.SelectedApplication.Path ) - ); - } + )); + + public static RelayCommand OpenModsDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString); - public void OpenModsDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; + OpenHelper.OpenFolder(titleModsPath); + }); + + public static RelayCommand OpenSdModsDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString); - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString); + OpenHelper.OpenFolder(titleModsPath); + }); - OpenHelper.OpenFolder(titleModsPath); - } - - public void OpenSdModsDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString); - - OpenHelper.OpenFolder(titleModsPath); - } - - public async void OpenModManager_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + public static AsyncRelayCommand OpenModManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { await ModManagerView.Show( - viewModel.SelectedApplication.Id, - viewModel.SelectedApplication.IdBase, - viewModel.ApplicationLibrary, + viewModel.SelectedApplication.Id, + viewModel.SelectedApplication.IdBase, + viewModel.ApplicationLibrary, viewModel.SelectedApplication.Name); - } - - public async void PurgePtcCache_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name) - ); - - if (result == UserResult.Yes) + }); + + public static AsyncRelayCommand PurgePtcCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name) + ); - List cacheFiles = []; - - if (mainDir.Exists) + if (result == UserResult.Yes) { - cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); - } + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); - if (backupDir.Exists) - { - cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); - } + List cacheFiles = []; - if (cacheFiles.Count > 0) - { - foreach (FileInfo file in cacheFiles) + if (mainDir.Exists) { - try + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) { - file.Delete(); - } - catch (Exception ex) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } } } } - } - } - - public async void NukePtcCache_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCNukeMessage, viewModel.SelectedApplication.Name) - ); - - if (result == UserResult.Yes) + }); + + public static AsyncRelayCommand NukePtcCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCNukeMessage, viewModel.SelectedApplication.Name) + ); - List cacheFiles = []; - - if (mainDir.Exists) + if (result == UserResult.Yes) { - cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); - cacheFiles.AddRange(mainDir.EnumerateFiles("*.info")); - } + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); - if (backupDir.Exists) - { - cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); - cacheFiles.AddRange(backupDir.EnumerateFiles("*.info")); - } + List cacheFiles = []; - if (cacheFiles.Count > 0) - { - foreach (FileInfo file in cacheFiles) + if (mainDir.Exists) { - try + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(mainDir.EnumerateFiles("*.info")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(backupDir.EnumerateFiles("*.info")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) { - file.Delete(); - } - catch (Exception ex) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } } } } - } - } - - public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - + }); + + public static AsyncRelayCommand PurgeShaderCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name) @@ -274,158 +261,143 @@ namespace Ryujinx.Ava.UI.Controls } } } - } - } - - public void OpenPtcDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); - string mainDir = Path.Combine(ptcDir, "0"); - string backupDir = Path.Combine(ptcDir, "1"); - - if (!Directory.Exists(ptcDir)) + } + }); + + public static RelayCommand OpenPtcDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => { - Directory.CreateDirectory(ptcDir); - Directory.CreateDirectory(mainDir); - Directory.CreateDirectory(backupDir); - } + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); + string mainDir = Path.Combine(ptcDir, "0"); + string backupDir = Path.Combine(ptcDir, "1"); - OpenHelper.OpenFolder(ptcDir); - } - - public void OpenShaderCacheDirectory_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString.ToLower(), "cache", "shader"); - - if (!Directory.Exists(shaderCacheDir)) + if (!Directory.Exists(ptcDir)) { - Directory.CreateDirectory(shaderCacheDir); + Directory.CreateDirectory(ptcDir); + Directory.CreateDirectory(mainDir); + Directory.CreateDirectory(backupDir); } - OpenHelper.OpenFolder(shaderCacheDir); - } - } - - public async void ExtractApplicationExeFs_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - { - await ApplicationHelper.ExtractSection( - viewModel.StorageProvider, - NcaSectionType.Code, - viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); - } - } - - public async void ExtractApplicationRomFs_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await ApplicationHelper.ExtractSection( - viewModel.StorageProvider, - NcaSectionType.Data, - viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); - } - - public async void ExtractAocRomFs_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary); - - if (selectedDlc is not null) - { - await ApplicationHelper.ExtractAoc( - viewModel.StorageProvider, - selectedDlc.ContainerPath, - selectedDlc.FileName); - } - } - - public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args) - { - if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - return; - - IReadOnlyList result = await viewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], - AllowMultiple = false, + OpenHelper.OpenFolder(ptcDir); }); + + public static RelayCommand OpenShaderCacheDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString.ToLower(), "cache", "shader"); - if (result.Count == 0) - return; + if (!Directory.Exists(shaderCacheDir)) + { + Directory.CreateDirectory(shaderCacheDir); + } - ApplicationHelper.ExtractSection( - result[0].Path.LocalPath, - NcaSectionType.Logo, - viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + OpenHelper.OpenFolder(shaderCacheDir); + }); + + public static AsyncRelayCommand ExtractApplicationExeFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Code, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + }); + + public static AsyncRelayCommand ExtractApplicationRomFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Data, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + }); + + public static AsyncRelayCommand ExtractApplicationAocRomFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary); + + if (selectedDlc is not null) + { + await ApplicationHelper.ExtractAoc( + viewModel.StorageProvider, + selectedDlc.ContainerPath, + selectedDlc.FileName); + } + }); + + public static AsyncRelayCommand ExtractApplicationLogo { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + IReadOnlyList result = await viewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], + AllowMultiple = false, + }); - IStorageFile iconFile = await result[0].CreateFileAsync($"{viewModel.SelectedApplication.IdString}.png"); - await using Stream fileStream = await iconFile.OpenWriteAsync(); + if (result.Count == 0) + return; - using SKBitmap bitmap = SKBitmap.Decode(viewModel.SelectedApplication.Icon) - .Resize(new SKSizeI(512, 512), SKFilterQuality.High); + ApplicationHelper.ExtractSection( + result[0].Path.LocalPath, + NcaSectionType.Logo, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); - using SKData png = bitmap.Encode(SKEncodedImageFormat.Png, 100); + IStorageFile iconFile = await result[0].CreateFileAsync($"{viewModel.SelectedApplication.IdString}.png"); + await using Stream fileStream = await iconFile.OpenWriteAsync(); - png.SaveTo(fileStream); - } + using SKBitmap bitmap = SKBitmap.Decode(viewModel.SelectedApplication.Icon) + .Resize(new SKSizeI(512, 512), SKFilterQuality.High); - public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - ShortcutHelper.CreateAppShortcut( + using SKData png = bitmap.Encode(SKEncodedImageFormat.Png, 100); + + png.SaveTo(fileStream); + }); + + public static RelayCommand CreateApplicationShortcut { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => ShortcutHelper.CreateAppShortcut( viewModel.SelectedApplication.Path, viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString, viewModel.SelectedApplication.Icon - ); - } + )); + + public static AsyncRelayCommand EditGameConfiguration { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await StyleableAppWindow.ShowAsync(new GameSpecificSettingsWindow(viewModel)); - public async void EditGameConfiguration_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - { - await StyleableAppWindow.ShowAsync(new GameSpecificSettingsWindow(viewModel)); + // just checking for file presence + viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false)); - // just checking for file presence - viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false)); - - viewModel.RefreshView(); - } - } - - public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await CompatibilityListWindow.Show(viewModel.SelectedApplication.IdString); - } - - public async void OpenApplicationData_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await ApplicationDataView.Show(viewModel.SelectedApplication); - } - - public async void RunApplication_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await viewModel.LoadApplication(viewModel.SelectedApplication); - } - - public async void TrimXCI_Click(object sender, RoutedEventArgs args) - { - if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) - await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path); - } + viewModel.RefreshView(); + }); + + public static AsyncRelayCommand OpenApplicationCompatibility { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => CompatibilityListWindow.Show(viewModel.SelectedApplication.IdString)); + + public static AsyncRelayCommand OpenApplicationData { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => ApplicationDataView.Show(viewModel.SelectedApplication)); + + public static AsyncRelayCommand RunApplication { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => viewModel.LoadApplication(viewModel.SelectedApplication)); + + public static AsyncRelayCommand TrimXci { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => viewModel.TrimXCIFile(viewModel.SelectedApplication.Path)); } } diff --git a/src/Ryujinx/UI/Helpers/Commands.cs b/src/Ryujinx/UI/Helpers/Commands.cs index 868b49158..7a65c61a1 100644 --- a/src/Ryujinx/UI/Helpers/Commands.cs +++ b/src/Ryujinx/UI/Helpers/Commands.cs @@ -9,12 +9,12 @@ namespace Ryujinx.Ava.UI.Helpers { public static RelayCommand Create(Action action) => new(action); - public static RelayCommand CreateConditional(Action action, Func canExecute) + public static RelayCommand CreateConditional(Func canExecute, Action action) => new(action, canExecute); public static RelayCommand Create(Action action) => new(action); - public static RelayCommand CreateConditional(Action action, Predicate canExecute) + public static RelayCommand CreateConditional(Predicate canExecute, Action action) => new(action, canExecute); public static AsyncRelayCommand Create(Func action) @@ -31,18 +31,18 @@ namespace Ryujinx.Ava.UI.Helpers public static AsyncRelayCommand CreateSilentFail(Func action) => new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); - public static AsyncRelayCommand CreateConditional(Func action, Func canExecute) + public static AsyncRelayCommand CreateConditional(Func canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.None); - public static AsyncRelayCommand CreateConcurrentConditional(Func action, Func canExecute) + public static AsyncRelayCommand CreateConcurrentConditional(Func canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions); - public static AsyncRelayCommand CreateSilentFailConditional(Func action, Func canExecute) + public static AsyncRelayCommand CreateSilentFailConditional(Func canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); - public static AsyncRelayCommand CreateConditional(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateConditional(Predicate canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.None); - public static AsyncRelayCommand CreateConcurrentConditional(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateConcurrentConditional(Predicate canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions); - public static AsyncRelayCommand CreateSilentFailConditional(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateSilentFailConditional(Predicate canExecute, Func action) => new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f265f4c06..61af0db34 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -12,9 +12,12 @@ using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Gommon; using LibHac.Common; +using LibHac.Fs; using LibHac.Ns; +using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Common.Models; using Ryujinx.Ava.Input; using Ryujinx.Ava.Systems; using Ryujinx.Ava.UI.Controls; @@ -25,6 +28,7 @@ using Ryujinx.Ava.UI.Renderer; using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Systems.AppLibrary; using Ryujinx.Ava.Systems.Configuration; +using Ryujinx.Ava.UI.Views.Dialog; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -51,7 +55,9 @@ using System.Threading; using System.Threading.Tasks; using Key = Ryujinx.Input.Key; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; +using Path = System.IO.Path; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; +using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; namespace Ryujinx.Ava.UI.ViewModels { @@ -1955,5 +1961,373 @@ namespace Ryujinx.Ava.UI.ViewModels } #endregion + + #region Context Menu commands + + public static RelayCommand ToggleFavorite { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; + + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => + { + appMetadata.Favorite = viewModel.SelectedApplication.Favorite; + }); + + viewModel.RefreshView(); + } + ); + + public static RelayCommand OpenUserSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + OpenSaveDirectory(viewModel, SaveDataType.Account, new LibHac.Fs.UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low)) + ); + + public static RelayCommand OpenDeviceSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => OpenSaveDirectory(viewModel, SaveDataType.Device, default)); + + public static RelayCommand OpenBcatSaveDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => OpenSaveDirectory(viewModel, SaveDataType.Bcat, default)); + + private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, LibHac.Fs.UserId userId) + { + SaveDataFilter saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); + + ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); + } + + public static AsyncRelayCommand OpenTitleUpdateManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => TitleUpdateManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication) + ); + + public static AsyncRelayCommand OpenDownloadableContentManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => DownloadableContentManagerView.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication) + ); + + public static AsyncRelayCommand OpenCheatManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => StyleableAppWindow.ShowAsync( + new CheatWindow( + viewModel.VirtualFileSystem, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.Path + ) + )); + + public static RelayCommand OpenModsDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString); + + OpenHelper.OpenFolder(titleModsPath); + }); + + public static RelayCommand OpenSdModsDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString); + + OpenHelper.OpenFolder(titleModsPath); + }); + + public static AsyncRelayCommand OpenModManager { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await ModManagerView.Show( + viewModel.SelectedApplication.Id, + viewModel.SelectedApplication.IdBase, + viewModel.ApplicationLibrary, + viewModel.SelectedApplication.Name); + }); + + public static AsyncRelayCommand PurgePtcCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name) + ); + + if (result == UserResult.Yes) + { + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + + List cacheFiles = []; + + if (mainDir.Exists) + { + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } + } + } + } + }); + + public static AsyncRelayCommand NukePtcCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCNukeMessage, viewModel.SelectedApplication.Name) + ); + + if (result == UserResult.Yes) + { + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + + List cacheFiles = []; + + if (mainDir.Exists) + { + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(mainDir.EnumerateFiles("*.info")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(backupDir.EnumerateFiles("*.info")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } + } + } + } + }); + + public static AsyncRelayCommand PurgeShaderCache { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name) + ); + + if (result == UserResult.Yes) + { + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); + + List oldCacheDirectories = []; + List newCacheFiles = []; + + if (shaderCacheDir.Exists) + { + oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*")); + newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc")); + newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data")); + } + + if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0)) + { + foreach (DirectoryInfo directory in oldCacheDirectories) + { + try + { + directory.Delete(true); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, directory.Name, ex)); + } + } + + foreach (FileInfo file in newCacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.ShaderCachePurgeError, file.Name, ex)); + } + } + } + } + }); + + public static RelayCommand OpenPtcDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); + string mainDir = Path.Combine(ptcDir, "0"); + string backupDir = Path.Combine(ptcDir, "1"); + + if (!Directory.Exists(ptcDir)) + { + Directory.CreateDirectory(ptcDir); + Directory.CreateDirectory(mainDir); + Directory.CreateDirectory(backupDir); + } + + OpenHelper.OpenFolder(ptcDir); + }); + + public static RelayCommand OpenShaderCacheDirectory { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => + { + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString.ToLower(), "cache", "shader"); + + if (!Directory.Exists(shaderCacheDir)) + { + Directory.CreateDirectory(shaderCacheDir); + } + + OpenHelper.OpenFolder(shaderCacheDir); + }); + + public static AsyncRelayCommand ExtractApplicationExeFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Code, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + }); + + public static AsyncRelayCommand ExtractApplicationRomFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Data, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + }); + + public static AsyncRelayCommand ExtractApplicationAocRomFs { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary); + + if (selectedDlc is not null) + { + await ApplicationHelper.ExtractAoc( + viewModel.StorageProvider, + selectedDlc.ContainerPath, + selectedDlc.FileName); + } + }); + + public static AsyncRelayCommand ExtractApplicationLogo { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + IReadOnlyList result = await viewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], + AllowMultiple = false, + }); + + if (result.Count == 0) + return; + + ApplicationHelper.ExtractSection( + result[0].Path.LocalPath, + NcaSectionType.Logo, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + + IStorageFile iconFile = await result[0].CreateFileAsync($"{viewModel.SelectedApplication.IdString}.png"); + await using Stream fileStream = await iconFile.OpenWriteAsync(); + + using SKBitmap bitmap = SKBitmap.Decode(viewModel.SelectedApplication.Icon) + .Resize(new SKSizeI(512, 512), SKFilterQuality.High); + + using SKData png = bitmap.Encode(SKEncodedImageFormat.Png, 100); + + png.SaveTo(fileStream); + }); + + public static RelayCommand CreateApplicationShortcut { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => ShortcutHelper.CreateAppShortcut( + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Icon + )); + + public static AsyncRelayCommand EditGameConfiguration { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + async viewModel => + { + await StyleableAppWindow.ShowAsync(new GameSpecificSettingsWindow(viewModel)); + + // just checking for file presence + viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false)); + + viewModel.RefreshView(); + }); + + public static AsyncRelayCommand OpenApplicationCompatibility { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => CompatibilityListWindow.Show(viewModel.SelectedApplication.IdString)); + + public static AsyncRelayCommand OpenApplicationData { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => ApplicationDataView.Show(viewModel.SelectedApplication)); + + public static AsyncRelayCommand RunApplication { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => viewModel.LoadApplication(viewModel.SelectedApplication)); + + public static AsyncRelayCommand TrimXci { get; } = + Commands.CreateConditional(vm => vm?.SelectedApplication != null, + viewModel => viewModel.TrimXCIFile(viewModel.SelectedApplication.Path)); + + #endregion } }