diff options
Diffstat (limited to 'src/Ryujinx/UI/Controls')
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml | 95 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs | 371 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationGridView.axaml | 102 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs | 51 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationListView.axaml | 160 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs | 51 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/NavigationDialogHost.axaml | 17 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs | 217 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/SliderScroll.axaml.cs | 31 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml | 42 | ||||
-rw-r--r-- | src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs | 31 |
11 files changed, 1168 insertions, 0 deletions
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml new file mode 100644 index 00000000..dd0926fc --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -0,0 +1,95 @@ +<MenuFlyout + x:Class="Ryujinx.Ava.UI.Controls.ApplicationContextMenu" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + x:DataType="viewModels:MainWindowViewModel"> + <MenuItem + Click="RunApplication_Click" + Header="{locale:Locale GameListContextMenuRunApplication}" /> + <MenuItem + Click="ToggleFavorite_Click" + Header="{locale:Locale GameListContextMenuToggleFavorite}" + ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> + <MenuItem + Click="CreateApplicationShortcut_Click" + Header="{locale:Locale GameListContextMenuCreateShortcut}" + IsEnabled="{Binding CreateShortcutEnabled}" + ToolTip.Tip="{OnPlatform Default={locale:Locale GameListContextMenuCreateShortcutToolTip}, macOS={locale:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> + <Separator /> + <MenuItem + Click="OpenUserSaveDirectory_Click" + Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}" + IsEnabled="{Binding OpenUserSaveDirectoryEnabled}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" /> + <MenuItem + Click="OpenDeviceSaveDirectory_Click" + Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}" + IsEnabled="{Binding OpenDeviceSaveDirectoryEnabled}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" /> + <MenuItem + Click="OpenBcatSaveDirectory_Click" + Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}" + IsEnabled="{Binding OpenBcatSaveDirectoryEnabled}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" /> + <Separator /> + <MenuItem + Click="OpenTitleUpdateManager_Click" + Header="{locale:Locale GameListContextMenuManageTitleUpdates}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" /> + <MenuItem + Click="OpenDownloadableContentManager_Click" + Header="{locale:Locale GameListContextMenuManageDlc}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" /> + <MenuItem + Click="OpenCheatManager_Click" + Header="{locale:Locale GameListContextMenuManageCheat}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" /> + <MenuItem + Click="OpenModManager_Click" + Header="{locale:Locale GameListContextMenuManageMod}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageModToolTip}" /> + <Separator /> + <MenuItem + Click="OpenModsDirectory_Click" + Header="{locale:Locale GameListContextMenuOpenModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" /> + <MenuItem + Click="OpenSdModsDirectory_Click" + Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" /> + <Separator /> + <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}"> + <MenuItem + Click="PurgePtcCache_Click" + Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" /> + <MenuItem + Click="PurgeShaderCache_Click" + Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" /> + <MenuItem + Click="OpenPtcDirectory_Click" + Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" /> + <MenuItem + Click="OpenShaderCacheDirectory_Click" + Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" /> + </MenuItem> + <MenuItem Header="{locale:Locale GameListContextMenuExtractData}"> + <MenuItem + Click="ExtractApplicationExeFs_Click" + Header="{locale:Locale GameListContextMenuExtractDataExeFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" /> + <MenuItem + Click="ExtractApplicationRomFs_Click" + Header="{locale:Locale GameListContextMenuExtractDataRomFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" /> + <MenuItem + Click="ExtractApplicationLogo_Click" + Header="{locale:Locale GameListContextMenuExtractDataLogo}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> + </MenuItem> +</MenuFlyout> diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs new file mode 100644 index 00000000..894ac6c1 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -0,0 +1,371 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using LibHac.Fs; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.HOS; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.UI.Controls +{ + public class ApplicationContextMenu : MenuFlyout + { + public ApplicationContextMenu() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void ToggleFavorite_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; + + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => + { + appMetadata.Favorite = viewModel.SelectedApplication.Favorite; + }); + + viewModel.RefreshView(); + } + } + + public void OpenUserSaveDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel 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) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + OpenSaveDirectory(viewModel, SaveDataType.Device, default); + } + + public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + OpenSaveDirectory(viewModel, SaveDataType.Bcat, default); + } + + private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId) + { + if (viewModel?.SelectedApplication != null) + { + if (!ulong.TryParse(viewModel.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; + } + + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); + + ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); + } + } + + public async void OpenTitleUpdateManager_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + } + } + + public async void OpenDownloadableContentManager_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + } + } + + public async void OpenCheatManager_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await new CheatWindow( + viewModel.VirtualFileSystem, + viewModel.SelectedApplication.TitleId, + viewModel.SelectedApplication.TitleName, + viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); + } + } + + public void OpenModsDirectory_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.TitleId); + + OpenHelper.OpenFolder(titleModsPath); + } + } + + public void OpenSdModsDirectory_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); + + OpenHelper.OpenFolder(titleModsPath); + } + } + + public async void OpenModManager_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + } + } + + public async void PurgePtcCache_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); + + List<FileInfo> cacheFiles = new(); + + 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 async void PurgeShaderCache_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); + + List<DirectoryInfo> oldCacheDirectories = new(); + List<FileInfo> newCacheFiles = new(); + + 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 void OpenPtcDirectory_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "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 void OpenShaderCacheDirectory_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); + + if (!Directory.Exists(shaderCacheDir)) + { + Directory.CreateDirectory(shaderCacheDir); + } + + OpenHelper.OpenFolder(shaderCacheDir); + } + } + + public async void ExtractApplicationExeFs_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Code, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.TitleName); + } + } + + public async void ExtractApplicationRomFs_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Data, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.TitleName); + } + } + + public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Logo, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.TitleName); + } + } + + public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + ApplicationData selectedApplication = viewModel.SelectedApplication; + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + } + } + + public async void RunApplication_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await viewModel.LoadApplication(viewModel.SelectedApplication.Path); + } + } + } +} diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml new file mode 100644 index 00000000..2dc95662 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml @@ -0,0 +1,102 @@ +<UserControl + x:Class="Ryujinx.Ava.UI.Controls.ApplicationGridView" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + d:DesignHeight="450" + d:DesignWidth="800" + Focusable="True" + mc:Ignorable="d" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + x:DataType="viewModels:MainWindowViewModel"> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + <controls:ApplicationContextMenu x:Key="ApplicationContextMenu" /> + </UserControl.Resources> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <ListBox + Grid.Row="0" + Padding="8" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + ContextFlyout="{StaticResource ApplicationContextMenu}" + DoubleTapped="GameList_DoubleTapped" + ItemsSource="{Binding AppsObservableList}" + SelectionChanged="GameList_SelectionChanged"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <WrapPanel + HorizontalAlignment="Center" + VerticalAlignment="Top" + Orientation="Horizontal" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Margin" Value="5" /> + <Setter Property="CornerRadius" Value="4" /> + </Style> + <Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> + <Setter Property="MinHeight" Value="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).GridItemSelectorSize}" /> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate> + <Grid> + <Border + Margin="10" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}" + Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}" + Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}" + Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}" + ClipToBounds="True" + CornerRadius="4"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image + Grid.Row="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + <Panel + Grid.Row="1" + Height="50" + Margin="0,10,0,0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}"> + <TextBlock + HorizontalAlignment="Center" + VerticalAlignment="Center" + Text="{Binding TitleName}" + TextAlignment="Center" + TextWrapping="Wrap" /> + </Panel> + </Grid> + </Border> + <ui:SymbolIcon + Margin="5,5,0,0" + HorizontalAlignment="Left" + VerticalAlignment="Top" + FontSize="16" + Foreground="{DynamicResource SystemAccentColor}" + IsVisible="{Binding Favorite}" + Symbol="StarFilled" /> + </Grid> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + </Grid> +</UserControl> diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs new file mode 100644 index 00000000..ee15bc8d --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationGridView : UserControl + { + public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = + RoutedEvent.Register<ApplicationGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public ApplicationGridView() + { + InitializeComponent(); + } + + public void GameList_DoubleTapped(object sender, TappedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + (DataContext as MainWindowViewModel).GridSelectedApplication = listBox.SelectedItem as ApplicationData; + } + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs args) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + } +} diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml new file mode 100644 index 00000000..fecf0888 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -0,0 +1,160 @@ +<UserControl + x:Class="Ryujinx.Ava.UI.Controls.ApplicationListView" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + d:DesignHeight="450" + d:DesignWidth="800" + Focusable="True" + mc:Ignorable="d" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + x:DataType="viewModels:MainWindowViewModel"> + <UserControl.Resources> + <helpers:BitmapArrayValueConverter x:Key="ByteImage" /> + <controls:ApplicationContextMenu x:Key="ApplicationContextMenu" /> + </UserControl.Resources> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <ListBox + Name="GameListBox" + Grid.Row="0" + Padding="8" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + ContextFlyout="{StaticResource ApplicationContextMenu}" + DoubleTapped="GameList_DoubleTapped" + ItemsSource="{Binding AppsObservableList}" + SelectionChanged="GameList_SelectionChanged"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <StackPanel + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Orientation="Vertical" + Spacing="2" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> + <Setter Property="MinHeight" Value="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ListItemSelectorSize}" /> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate> + <Grid> + <Border + Margin="0" + Padding="10" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + ClipToBounds="True" + CornerRadius="5"> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="10" /> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="150" /> + <ColumnDefinition Width="100" /> + </Grid.ColumnDefinitions> + <Image + Grid.RowSpan="3" + Grid.Column="0" + Margin="0" + Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}" + Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}" + Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}" + Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}" + Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + <Border + Grid.Column="2" + Margin="0,0,5,0" + BorderBrush="{DynamicResource ThemeControlBorderColor}" + BorderThickness="0,0,1,0"> + <StackPanel + HorizontalAlignment="Left" + VerticalAlignment="Top" + Orientation="Vertical" + Spacing="5"> + <TextBlock + HorizontalAlignment="Stretch" + FontWeight="Bold" + Text="{Binding TitleName}" + TextAlignment="Start" + TextWrapping="Wrap" /> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding Developer}" + TextAlignment="Start" + TextWrapping="Wrap" /> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding Version}" + TextAlignment="Start" + TextWrapping="Wrap" /> + </StackPanel> + </Border> + <StackPanel + Grid.Column="3" + Margin="10,0,0,0" + HorizontalAlignment="Left" + VerticalAlignment="Top" + Orientation="Vertical" + Spacing="5"> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding TitleId}" + TextAlignment="Start" + TextWrapping="Wrap" /> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding FileExtension}" + TextAlignment="Start" + TextWrapping="Wrap" /> + </StackPanel> + <StackPanel + Grid.Column="4" + HorizontalAlignment="Right" + VerticalAlignment="Top" + Orientation="Vertical" + Spacing="5"> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding TimePlayedString}" + TextAlignment="End" + TextWrapping="Wrap" /> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding LastPlayedString, Converter={helpers:LocalizedNeverConverter}}" + TextAlignment="End" + TextWrapping="Wrap" /> + <TextBlock + HorizontalAlignment="Stretch" + Text="{Binding FileSizeString}" + TextAlignment="End" + TextWrapping="Wrap" /> + </StackPanel> + <ui:SymbolIcon + Grid.Row="0" + Grid.Column="0" + Margin="-5,-5,0,0" + HorizontalAlignment="Left" + VerticalAlignment="Top" + FontSize="16" + Foreground="{DynamicResource SystemAccentColor}" + IsVisible="{Binding Favorite}" + Symbol="StarFilled" /> + </Grid> + </Border> + </Grid> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + </Grid> +</UserControl> diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs new file mode 100644 index 00000000..8681158f --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationListView : UserControl + { + public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = + RoutedEvent.Register<ApplicationListView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public ApplicationListView() + { + InitializeComponent(); + } + + public void GameList_DoubleTapped(object sender, TappedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + (DataContext as MainWindowViewModel).ListSelectedApplication = listBox.SelectedItem as ApplicationData; + } + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs args) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + } +} diff --git a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml new file mode 100644 index 00000000..bf34b303 --- /dev/null +++ b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml @@ -0,0 +1,17 @@ +<UserControl + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" + x:Class="Ryujinx.Ava.UI.Controls.NavigationDialogHost" + Focusable="True"> + <ui:Frame + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + x:Name="ContentFrame"> + </ui:Frame> +</UserControl>
\ No newline at end of file diff --git a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs new file mode 100644 index 00000000..a32c052b --- /dev/null +++ b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs @@ -0,0 +1,217 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class NavigationDialogHost : UserControl + { + public AccountManager AccountManager { get; } + public ContentManager ContentManager { get; } + public VirtualFileSystem VirtualFileSystem { get; } + public HorizonClient HorizonClient { get; } + public UserProfileViewModel ViewModel { get; set; } + + public NavigationDialogHost() + { + InitializeComponent(); + } + + public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, + VirtualFileSystem virtualFileSystem, HorizonClient horizonClient) + { + AccountManager = accountManager; + ContentManager = contentManager; + VirtualFileSystem = virtualFileSystem; + HorizonClient = horizonClient; + ViewModel = new UserProfileViewModel(); + LoadProfiles(); + + if (contentManager.GetCurrentFirmwareVersion() != null) + { + Task.Run(() => + { + UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem); + }); + } + InitializeComponent(); + } + + public void GoBack() + { + if (ContentFrame.BackStack.Count > 0) + { + ContentFrame.GoBack(); + } + + LoadProfiles(); + } + + public void Navigate(Type sourcePageType, object parameter) + { + ContentFrame.Navigate(sourcePageType, parameter); + } + + public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) + { + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); + ContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = content, + Padding = new Thickness(0), + }; + + contentDialog.Closed += (sender, args) => + { + content.ViewModel.Dispose(); + }; + + Style footer = new(x => x.Name("DialogSpace").Child().OfType<Border>()); + footer.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(footer); + + await contentDialog.ShowAsync(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Navigate(typeof(UserSelectorViews), this); + } + + public void LoadProfiles() + { + ViewModel.Profiles.Clear(); + ViewModel.LostProfiles.Clear(); + + var profiles = AccountManager.GetAllUsers().OrderBy(x => x.Name); + + foreach (var profile in profiles) + { + ViewModel.Profiles.Add(new UserProfile(profile, this)); + } + + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, saveDataId: default, index: default); + + using var saveDataIterator = new UniqueRef<SaveDataIterator>(); + + HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10]; + + HashSet<UserId> lostAccounts = new(); + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + var save = saveDataInfo[i]; + var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High); + if (ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault(x => x.UserId == id) == null) + { + lostAccounts.Add(id); + } + } + } + + foreach (var account in lostAccounts) + { + ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), this)); + } + + ViewModel.Profiles.Add(new BaseModel()); + } + + public async void DeleteUser(UserProfile userProfile) + { + var lastUserId = AccountManager.LastOpenedUser.UserId; + + if (userProfile.UserId == lastUserId) + { + // If we are deleting the currently open profile, then we must open something else before deleting. + var profile = ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault(x => x.UserId != lastUserId); + + if (profile == null) + { + static async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + AccountManager.OpenUser(profile.UserId); + } + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], + "", + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + ""); + + if (result == UserResult.Yes) + { + GoBack(); + AccountManager.DeleteUser(userProfile.UserId); + } + + LoadProfiles(); + } + + public void AddUser() + { + Navigate(typeof(UserEditorView), (this, (UserProfile)null, true)); + } + + public void EditUser(UserProfile userProfile) + { + Navigate(typeof(UserEditorView), (this, userProfile, false)); + } + + public void RecoverLostAccounts() + { + Navigate(typeof(UserRecovererView), this); + } + + public void ManageSaves() + { + Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem)); + } + } +} diff --git a/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs new file mode 100644 index 00000000..b57c6f0a --- /dev/null +++ b/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Avalonia.Input; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public class SliderScroll : Slider + { + protected override Type StyleKeyOverride => typeof(Slider); + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + var newValue = Value + e.Delta.Y * TickFrequency; + + if (newValue < Minimum) + { + Value = Minimum; + } + else if (newValue > Maximum) + { + Value = Maximum; + } + else + { + Value = newValue; + } + + e.Handled = true; + } + } +} diff --git a/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml new file mode 100644 index 00000000..09fa0404 --- /dev/null +++ b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml @@ -0,0 +1,42 @@ +<Window + x:Class="Ryujinx.Ava.UI.Controls.UpdateWaitWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + Title="Ryujinx - Waiting" + SizeToContent="WidthAndHeight" + WindowStartupLocation="CenterOwner" + mc:Ignorable="d" + Focusable="True"> + <Grid + Margin="20" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Image + Grid.Row="1" + Height="70" + MinWidth="50" + Margin="5,10,20,10" + Source="resm:Ryujinx.UI.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.UI.Common" /> + <StackPanel + Grid.Row="1" + Grid.Column="1" + VerticalAlignment="Center" + Orientation="Vertical"> + <TextBlock Name="PrimaryText" Margin="5" /> + <TextBlock + Name="SecondaryText" + Margin="5" + VerticalAlignment="Center" /> + </StackPanel> + </Grid> +</Window> diff --git a/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs new file mode 100644 index 00000000..7ad1ee33 --- /dev/null +++ b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.Windows; +using System.Threading; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class UpdateWaitWindow : StyleableWindow + { + public UpdateWaitWindow(string primaryText, string secondaryText, CancellationTokenSource cancellationToken) : this(primaryText, secondaryText) + { + SystemDecorations = SystemDecorations.Full; + ShowInTaskbar = true; + + Closing += (_, _) => cancellationToken.Cancel(); + } + + public UpdateWaitWindow(string primaryText, string secondaryText) : this() + { + PrimaryText.Text = primaryText; + SecondaryText.Text = secondaryText; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + SystemDecorations = SystemDecorations.BorderOnly; + ShowInTaskbar = false; + } + + public UpdateWaitWindow() + { + InitializeComponent(); + } + } +} |