aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx/UI/Controls
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx/UI/Controls')
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml95
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs371
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationGridView.axaml102
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs51
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationListView.axaml160
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs51
-rw-r--r--src/Ryujinx/UI/Controls/NavigationDialogHost.axaml17
-rw-r--r--src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs217
-rw-r--r--src/Ryujinx/UI/Controls/SliderScroll.axaml.cs31
-rw-r--r--src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml42
-rw-r--r--src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs31
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();
+ }
+ }
+}