aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Windows
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-07-08 18:47:11 +0000
committerGitHub <noreply@github.com>2022-07-08 15:47:11 -0300
commit3af42d6c7e9e71c504b87a7b0f7f960fe83418fb (patch)
tree2b27f12ee0273d5316229d31383619d915c3210d /Ryujinx.Ava/Ui/Windows
parentbccf5e8b5a8f3870dbf03bedb0eb46b85b78d5f4 (diff)
UI - Avalonia Part 3 (#3441)
* Add all other windows * addreesed review * Prevent "No Update" option from being deleted * Select no update is the current update is removed from the title update window * fix amiibo crash
Diffstat (limited to 'Ryujinx.Ava/Ui/Windows')
-rw-r--r--Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml68
-rw-r--r--Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs70
-rw-r--r--Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml53
-rw-r--r--Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs71
-rw-r--r--Ryujinx.Ava/Ui/Windows/CheatWindow.axaml90
-rw-r--r--Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs137
-rw-r--r--Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml132
-rw-r--r--Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs261
-rw-r--r--Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml104
-rw-r--r--Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs265
-rw-r--r--Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml107
-rw-r--r--Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs102
12 files changed, 1460 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
new file mode 100644
index 00000000..f91bb313
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
@@ -0,0 +1,68 @@
+<window:StyleableWindow 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"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
+ x:Class="Ryujinx.Ava.Ui.Windows.AmiiboWindow"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ CanResize="False"
+ WindowStartupLocation="CenterOwner"
+ Width="800" MinHeight="650" Height="650"
+ SizeToContent="Manual"
+ MinWidth="600">
+ <Design.DataContext>
+ <viewModels:AmiiboWindowViewModel />
+ </Design.DataContext>
+ <Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid Grid.Row="1" HorizontalAlignment="Stretch">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
+ <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" />
+ <ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" />
+ </StackPanel>
+ <StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
+ <TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" />
+ <ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" />
+ </StackPanel>
+ </Grid>
+ <StackPanel Margin="20" Grid.Row="2">
+ <Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" />
+ <ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
+ Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
+ <TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
+ TextAlignment="Center" />
+ </ScrollViewer>
+ </StackPanel>
+ <Grid Grid.Row="3">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="*" />
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}"
+ Content="{locale:Locale AmiiboOptionsShowAllLabel}" />
+ <CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}"
+ Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" />
+
+ <Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80"
+ Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton"
+ Click="ScanButton_Click" />
+ <Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}"
+ Name="CancelButton"
+ Click="CancelButton_Click" />
+ </Grid>
+ </Grid>
+</window:StyleableWindow> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
new file mode 100644
index 00000000..bd0935a9
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
@@ -0,0 +1,70 @@
+using Avalonia;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.ViewModels;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class AmiiboWindow : StyleableWindow
+ {
+ public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
+ {
+ ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
+
+ ViewModel.ShowAllAmiibo = showAll;
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
+ }
+
+ public AmiiboWindow()
+ {
+ ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ if (Program.PreviewerDetached)
+ {
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
+ }
+ }
+
+ public bool IsScanned { get; set; }
+ public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
+ public AmiiboWindowViewModel ViewModel { get; set; }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void ScanButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (ViewModel.AmiiboSelectedIndex > -1)
+ {
+ Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
+ ScannedAmiibo = amiibo;
+ IsScanned = true;
+ Close();
+ }
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ IsScanned = false;
+
+ Close();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
new file mode 100644
index 00000000..6c7576bc
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
@@ -0,0 +1,53 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
+ x:Class="Ryujinx.Ava.Ui.Windows.AvatarWindow"
+ CanResize="False"
+ xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+ WindowStartupLocation="CenterOwner"
+ x:CompileBindings="True"
+ x:DataType="viewModels:AvatarProfileViewModel"
+ SizeToContent="WidthAndHeight">
+ <Design.DataContext>
+ <viewModels:AvatarProfileViewModel />
+ </Design.DataContext>
+ <Window.Resources>
+ <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+ </Window.Resources>
+ <Grid Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Width="600" Height="500"
+ Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <WrapPanel Orientation="Horizontal" MaxWidth="600" Margin="0" HorizontalAlignment="Center" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Image Margin="5" Height="96" Width="96"
+ Source="{Binding Data, Converter={StaticResource ByteImage}}" />
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ <ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
+ Maximum="{Binding ImageCount}" Minimum="0" />
+ <StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
+ <Button Content="{Locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
+ <ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
+ <Button HorizontalAlignment="Right" Content="{Locale:Locale AvatarClose}" Click="CloseButton_OnClick"
+ Name="CloseButton"
+ Width="200" />
+ </StackPanel>
+ </Grid>
+</Window> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
new file mode 100644
index 00000000..25e89846
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
@@ -0,0 +1,71 @@
+using Avalonia;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using System;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class AvatarWindow : StyleableWindow
+ {
+ public AvatarWindow(ContentManager contentManager)
+ {
+ ContentManager = contentManager;
+ ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages());
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
+ }
+
+ public AvatarWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ if (Program.PreviewerDetached)
+ {
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
+ }
+ }
+
+ public ContentManager ContentManager { get; }
+
+ public byte[] SelectedImage { get; set; }
+
+ internal AvatarProfileViewModel ViewModel { get; set; }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ ViewModel.Dispose();
+ base.OnClosed(e);
+ }
+
+ private void CloseButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (ViewModel.SelectedIndex > -1)
+ {
+ SelectedImage = ViewModel.SelectedImage;
+
+ Close();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
new file mode 100644
index 00000000..1685ee80
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
@@ -0,0 +1,90 @@
+<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
+ 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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ mc:Ignorable="d"
+ SizeToContent="Height"
+ Width="500" MinHeight="500" Height="500"
+ WindowStartupLocation="CenterOwner"
+ MinWidth="500">
+ <Window.Styles>
+ <Style Selector="TreeViewItem">
+ <Setter Property="IsExpanded" Value="True" />
+ </Style>
+ </Window.Styles>
+ <Grid Name="DlcGrid" Margin="15">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock
+ Grid.Row="1"
+ Margin="20,15,20,20"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ MaxWidth="500"
+ LineHeight="18"
+ TextWrapping="Wrap"
+ Text="{Binding Heading}"
+ TextAlignment="Center" />
+ <Border
+ Grid.Row="2"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ BorderBrush="Gray"
+ BorderThickness="1">
+ <TreeView Items="{Binding LoadedCheats}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Name="CheatsView"
+ MinHeight="300">
+ <TreeView.DataTemplates>
+ <TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
+ <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
+ <CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
+ <TextBlock Width="150"
+ Text="{Binding BuildId}" />
+ <TextBlock
+ Text="{Binding Path}" />
+ </StackPanel>
+ </TreeDataTemplate>
+ <DataTemplate x:DataType="model:CheatModel">
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
+ <CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
+ <TextBlock Text="{Binding CleanName}" />
+ </StackPanel>
+ </DataTemplate>
+ </TreeView.DataTemplates>
+ </TreeView>
+ </Border>
+ <DockPanel
+ Grid.Row="3"
+ Margin="0"
+ HorizontalAlignment="Stretch">
+ <DockPanel Margin="0" HorizontalAlignment="Right">
+ <Button
+ Name="SaveButton"
+ MinWidth="90"
+ Margin="5"
+ IsVisible="{Binding !NoCheatsFound}"
+ Command="{Binding Save}">
+ <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+ </Button>
+ <Button
+ Name="CancelButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Close}">
+ <TextBlock Text="{locale:Locale InputDialogCancel}" />
+ </Button>
+ </DockPanel>
+ </DockPanel>
+ </Grid>
+</window:StyleableWindow> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
new file mode 100644
index 00000000..33abeb8d
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
@@ -0,0 +1,137 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class CheatWindow : StyleableWindow
+ {
+ private readonly string _enabledCheatsPath;
+ public bool NoCheatsFound { get; }
+
+ private AvaloniaList<CheatsList> LoadedCheats { get; }
+
+ public string Heading { get; }
+
+ public CheatWindow()
+ {
+ DataContext = this;
+
+ InitializeComponent();
+ AttachDebugDevTools();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
+ }
+
+ public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
+ {
+ LoadedCheats = new AvaloniaList<CheatsList>();
+
+ Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+
+ string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
+ string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
+ ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
+
+ _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
+
+ string[] enabled = { };
+
+ if (File.Exists(_enabledCheatsPath))
+ {
+ enabled = File.ReadAllLines(_enabledCheatsPath);
+ }
+
+ int cheatAdded = 0;
+
+ var mods = new ModLoader.ModCache();
+
+ ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue);
+
+ string currentCheatFile = string.Empty;
+ string buildId = string.Empty;
+ string parentPath = string.Empty;
+
+ CheatsList currentGroup = null;
+
+ foreach (var cheat in mods.Cheats)
+ {
+ if (cheat.Path.FullName != currentCheatFile)
+ {
+ currentCheatFile = cheat.Path.FullName;
+ parentPath = currentCheatFile.Replace(titleModsPath, "");
+
+ buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
+ currentGroup = new CheatsList(buildId, parentPath);
+
+ LoadedCheats.Add(currentGroup);
+ }
+
+ var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}"));
+ currentGroup?.Add(model);
+
+ cheatAdded++;
+ }
+
+ if (cheatAdded == 0)
+ {
+ NoCheatsFound = true;
+ }
+
+ DataContext = this;
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
+ }
+
+ [Conditional("DEBUG")]
+ private void AttachDebugDevTools()
+ {
+ this.AttachDevTools();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public void Save()
+ {
+ if (NoCheatsFound)
+ {
+ return;
+ }
+
+ List<string> enabledCheats = new List<string>();
+
+ foreach (var cheats in LoadedCheats)
+ {
+ foreach (var cheat in cheats)
+ {
+ if (cheat.IsEnabled)
+ {
+ enabledCheats.Add(cheat.BuildIdKey);
+ }
+ }
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath));
+
+ File.WriteAllLines(_enabledCheatsPath, enabledCheats);
+
+ Close();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
new file mode 100644
index 00000000..94b3895e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
@@ -0,0 +1,132 @@
+<window:StyleableWindow
+ x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
+ 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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ SizeToContent="Height"
+ Width="600" MinHeight="500" Height="500"
+ WindowStartupLocation="CenterOwner"
+ MinWidth="600"
+ mc:Ignorable="d">
+ <Grid Name="DlcGrid" Margin="15">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock
+ Grid.Row="1"
+ Margin="20,15,20,20"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ MaxWidth="500"
+ LineHeight="18"
+ TextWrapping="Wrap"
+ Text="{Binding Heading}"
+ TextAlignment="Center" />
+ <Border
+ Grid.Row="2"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ BorderBrush="Gray"
+ BorderThickness="1">
+ <DataGrid
+ MinHeight="200"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ HorizontalScrollBarVisibility="Auto"
+ Items="{Binding Dlcs}"
+ VerticalScrollBarVisibility="Auto">
+ <DataGrid.Columns>
+ <DataGridTemplateColumn Width="90">
+ <DataGridTemplateColumn.CellTemplate>
+ <DataTemplate>
+ <CheckBox
+ Width="50"
+ MinWidth="40"
+ HorizontalAlignment="Right"
+ IsChecked="{Binding IsEnabled}" />
+ </DataTemplate>
+ </DataGridTemplateColumn.CellTemplate>
+ <DataGridTemplateColumn.Header>
+ <TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
+ </DataGridTemplateColumn.Header>
+ </DataGridTemplateColumn>
+ <DataGridTextColumn
+ Width="190"
+ Binding="{Binding TitleId}"
+ CanUserResize="True">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn
+ Width="*"
+ Binding="{Binding ContainerPath}"
+ CanUserResize="True">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn
+ Width="*"
+ Binding="{Binding FullPath}"
+ CanUserResize="True">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ </DataGrid.Columns>
+ </DataGrid>
+ </Border>
+ <DockPanel
+ Grid.Row="3"
+ Margin="0"
+ HorizontalAlignment="Stretch">
+ <DockPanel Margin="0" HorizontalAlignment="Left">
+ <Button
+ Name="AddButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Add}">
+ <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
+ </Button>
+ <Button
+ Name="RemoveButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding RemoveSelected}">
+ <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
+ </Button>
+ <Button
+ Name="RemoveAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding RemoveAll}">
+ <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
+ </Button>
+ </DockPanel>
+ <DockPanel Margin="0" HorizontalAlignment="Right">
+ <Button
+ Name="SaveButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Save}">
+ <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+ </Button>
+ <Button
+ Name="CancelButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Close}">
+ <TextBlock Text="{locale:Locale InputDialogCancel}" />
+ </Button>
+ </DockPanel>
+ </DockPanel>
+ </Grid>
+</window:StyleableWindow> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
new file mode 100644
index 00000000..cb2ed324
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
@@ -0,0 +1,261 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Path = System.IO.Path;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class DlcManagerWindow : StyleableWindow
+ {
+ private readonly List<DlcContainer> _dlcContainerList;
+ private readonly string _dlcJsonPath;
+
+ public VirtualFileSystem VirtualFileSystem { get; }
+
+ public AvaloniaList<DlcModel> Dlcs { get; set; }
+ public Grid DlcGrid { get; private set; }
+ public ulong TitleId { get; }
+ public string TitleName { get; }
+
+ public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
+
+ public DlcManagerWindow()
+ {
+ DataContext = this;
+
+ InitializeComponent();
+ AttachDebugDevTools();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
+ }
+
+ public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+ {
+ VirtualFileSystem = virtualFileSystem;
+ TitleId = titleId;
+ TitleName = titleName;
+
+ _dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
+
+ try
+ {
+ _dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
+ }
+ catch
+ {
+ _dlcContainerList = new List<DlcContainer>();
+ }
+
+ DataContext = this;
+
+ InitializeComponent();
+ AttachDebugDevTools();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
+
+ LoadDlcs();
+ }
+
+ [Conditional("DEBUG")]
+ private void AttachDebugDevTools()
+ {
+ this.AttachDevTools();
+ }
+
+ private void InitializeComponent()
+ {
+ Dlcs = new AvaloniaList<DlcModel>();
+
+ AvaloniaXamlLoader.Load(this);
+
+ DlcGrid = this.FindControl<Grid>("DlcGrid");
+ }
+
+ private void LoadDlcs()
+ {
+ foreach (DlcContainer dlcContainer in _dlcContainerList)
+ {
+ using FileStream containerFile = File.OpenRead(dlcContainer.Path);
+
+ PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+
+ VirtualFileSystem.ImportTickets(pfs);
+
+ foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+ pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
+
+ if (nca != null)
+ {
+ Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
+ dlcNca.Enabled));
+ }
+ }
+ }
+ }
+
+ private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
+ {
+ try
+ {
+ return new Nca(VirtualFileSystem.KeySet, ncaStorage);
+ }
+ catch (Exception ex)
+ {
+ ContentDialogHelper.CreateErrorDialog(this,
+ string.Format(LocaleManager.Instance[
+ "DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
+ }
+
+ return null;
+ }
+
+ private void AddDlc(string path)
+ {
+ if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
+ {
+ return;
+ }
+
+ using (FileStream containerFile = File.OpenRead(path))
+ {
+ PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+ bool containsDlc = false;
+
+ VirtualFileSystem.ImportTickets(pfs);
+
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
+
+ if (nca == null)
+ {
+ continue;
+ }
+
+ if (nca.Header.ContentType == NcaContentType.PublicData)
+ {
+ if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
+ {
+ break;
+ }
+
+ Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
+
+ containsDlc = true;
+ }
+ }
+
+ if (!containsDlc)
+ {
+ ContentDialogHelper.CreateErrorDialog(this, LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
+ }
+ }
+ }
+
+ private void RemoveDlcs(bool removeSelectedOnly = false)
+ {
+ if (removeSelectedOnly)
+ {
+ Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
+ }
+ else
+ {
+ Dlcs.Clear();
+ }
+ }
+
+ public void RemoveSelected()
+ {
+ RemoveDlcs(true);
+ }
+
+ public void RemoveAll()
+ {
+ RemoveDlcs();
+ }
+
+ public async void Add()
+ {
+ OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
+
+ dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
+
+ string[] files = await dialog.ShowAsync(this);
+
+ if (files != null)
+ {
+ foreach (string file in files)
+ {
+ AddDlc(file);
+ }
+ }
+ }
+
+ public void Save()
+ {
+ _dlcContainerList.Clear();
+
+ DlcContainer container = default;
+
+ foreach (DlcModel dlc in Dlcs)
+ {
+ if (container.Path != dlc.ContainerPath)
+ {
+ if (!string.IsNullOrWhiteSpace(container.Path))
+ {
+ _dlcContainerList.Add(container);
+ }
+
+ container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
+ }
+
+ container.DlcNcaList.Add(new DlcNca
+ {
+ Enabled = dlc.IsEnabled,
+ TitleId = Convert.ToUInt64(dlc.TitleId, 16),
+ Path = dlc.FullPath
+ });
+ }
+
+ if (!string.IsNullOrWhiteSpace(container.Path))
+ {
+ _dlcContainerList.Add(container);
+ }
+
+ using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
+ {
+ dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
+ }
+
+ Close();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
new file mode 100644
index 00000000..347c2cf5
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
@@ -0,0 +1,104 @@
+<window:StyleableWindow
+ x:Class="Ryujinx.Ava.Ui.Windows.TitleUpdateWindow"
+ 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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ SizeToContent="Height"
+ Width="600" MinHeight="500" Height="500"
+ WindowStartupLocation="CenterOwner"
+ MinWidth="600"
+ mc:Ignorable="d">
+ <Grid Margin="15">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock
+ Grid.Row="1"
+ Margin="20,15,20,20"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ MaxWidth="500"
+ LineHeight="18"
+ TextWrapping="Wrap"
+ Text="{Binding Heading}"
+ TextAlignment="Center" />
+ <Border
+ Grid.Row="2"
+ Margin="5"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ BorderBrush="Gray"
+ BorderThickness="1">
+ <ScrollViewer
+ Width="550"
+ MinHeight="200"
+ VerticalAlignment="Stretch"
+ HorizontalScrollBarVisibility="Auto"
+ VerticalScrollBarVisibility="Auto">
+ <ItemsControl
+ Margin="10"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Items="{Binding TitleUpdates}">
+ <ItemsControl.ItemTemplate>
+ <DataTemplate>
+ <RadioButton Padding="8, 0" VerticalContentAlignment="Center" GroupName="Update" IsChecked="{Binding IsEnabled, Mode=TwoWay}">
+ <Label Margin="0" VerticalAlignment="Center" Content="{Binding Label}" />
+ </RadioButton>
+ </DataTemplate>
+ </ItemsControl.ItemTemplate>
+ </ItemsControl>
+ </ScrollViewer>
+ </Border>
+ <DockPanel
+ Grid.Row="3"
+ Margin="0"
+ HorizontalAlignment="Stretch">
+ <DockPanel Margin="0" HorizontalAlignment="Left">
+ <Button
+ Name="AddButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Add}">
+ <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
+ </Button>
+ <Button
+ Name="RemoveButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding RemoveSelected}">
+ <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
+ </Button>
+ <Button
+ Name="RemoveAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding RemoveAll}">
+ <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
+ </Button>
+ </DockPanel>
+ <DockPanel Margin="0" HorizontalAlignment="Right">
+ <Button
+ Name="SaveButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Save}">
+ <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+ </Button>
+ <Button
+ Name="CancelButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Close}">
+ <TextBlock Text="{locale:Locale InputDialogCancel}" />
+ </Button>
+ </DockPanel>
+ </DockPanel>
+ </Grid>
+</window:StyleableWindow> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
new file mode 100644
index 00000000..edc1abcd
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
@@ -0,0 +1,265 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Ns;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Path = System.IO.Path;
+using SpanHelpers = LibHac.Common.SpanHelpers;
+using LibHac.Tools.FsSystem;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class TitleUpdateWindow : StyleableWindow
+ {
+ private readonly string _updateJsonPath;
+ private TitleUpdateMetadata _titleUpdateWindowData;
+
+ public VirtualFileSystem VirtualFileSystem { get; }
+
+ internal AvaloniaList<TitleUpdateModel> TitleUpdates { get; set; }
+ public string TitleId { get; }
+ public string TitleName { get; }
+
+ public string Heading => string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], TitleName, TitleId.ToUpper());
+
+ public TitleUpdateWindow()
+ {
+ DataContext = this;
+
+ InitializeComponent();
+ AttachDebugDevTools();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
+ }
+
+ public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
+ {
+ VirtualFileSystem = virtualFileSystem;
+ TitleId = titleId;
+ TitleName = titleName;
+
+ _updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
+
+ try
+ {
+ _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
+ }
+ catch
+ {
+ _titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
+ }
+
+ DataContext = this;
+
+ InitializeComponent();
+ AttachDebugDevTools();
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
+
+ LoadUpdates();
+ }
+
+ [Conditional("DEBUG")]
+ private void AttachDebugDevTools()
+ {
+ this.AttachDevTools();
+ }
+
+ private void InitializeComponent()
+ {
+ TitleUpdates = new AvaloniaList<TitleUpdateModel>();
+
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void LoadUpdates()
+ {
+ TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
+
+ foreach (string path in _titleUpdateWindowData.Paths)
+ {
+ AddUpdate(path);
+ }
+
+ if (_titleUpdateWindowData.Selected == "")
+ {
+ TitleUpdates[0].IsEnabled = true;
+ }
+ else
+ {
+ TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
+ List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
+
+ foreach (TitleUpdateModel update in enabled)
+ {
+ update.IsEnabled = false;
+ }
+
+ if (selected != null)
+ {
+ selected.IsEnabled = true;
+ }
+ }
+
+ SortUpdates();
+ }
+
+ private void AddUpdate(string path)
+ {
+ if (File.Exists(path) && !TitleUpdates.Any(x => x.Path == path))
+ {
+ using (FileStream file = new(path, FileMode.Open, FileAccess.Read))
+ {
+ PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
+
+ try
+ {
+ (Nca patchNca, Nca controlNca) =
+ ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
+
+ if (controlNca != null && patchNca != null)
+ {
+ ApplicationControlProperty controlData = new ApplicationControlProperty();
+
+ using var nacpFile = new UniqueRef<IFile>();
+
+ controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
+ .OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
+ .ThrowIfFailure();
+ nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
+ .ThrowIfFailure();
+
+ TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+ }
+ else
+ {
+ ContentDialogHelper.CreateErrorDialog(this,
+ LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
+ }
+ }
+ catch (Exception ex)
+ {
+ ContentDialogHelper.CreateErrorDialog(this,
+ string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
+ }
+ }
+ }
+ }
+
+ private void RemoveUpdates(bool removeSelectedOnly = false)
+ {
+ if (removeSelectedOnly)
+ {
+ TitleUpdates.RemoveAll(TitleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
+ }
+ else
+ {
+ TitleUpdates.RemoveAll(TitleUpdates.Where(x => !x.IsNoUpdate).ToList());
+ }
+
+ TitleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
+
+ SortUpdates();
+ }
+
+ public void RemoveSelected()
+ {
+ RemoveUpdates(true);
+ }
+
+ public void RemoveAll()
+ {
+ RemoveUpdates();
+ }
+
+ public async void Add()
+ {
+ OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
+
+ dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
+
+ string[] files = await dialog.ShowAsync(this);
+
+ if (files != null)
+ {
+ foreach (string file in files)
+ {
+ AddUpdate(file);
+ }
+ }
+
+ SortUpdates();
+ }
+
+ private void SortUpdates()
+ {
+ var list = TitleUpdates.ToList();
+
+ list.Sort((first, second) =>
+ {
+ if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
+ {
+ return -1;
+ }
+ else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
+ {
+ return 1;
+ }
+
+ return Version.Parse(first.Control.DisplayVersionString.ToString())
+ .CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
+ });
+
+ TitleUpdates.Clear();
+
+ TitleUpdates.AddRange(list);
+ }
+
+ public void Save()
+ {
+ _titleUpdateWindowData.Paths.Clear();
+
+ _titleUpdateWindowData.Selected = "";
+
+ foreach (TitleUpdateModel update in TitleUpdates)
+ {
+ _titleUpdateWindowData.Paths.Add(update.Path);
+
+ if (update.IsEnabled)
+ {
+ _titleUpdateWindowData.Selected = update.Path;
+ }
+ }
+
+ using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
+ {
+ dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
+ }
+
+ if (Owner is MainWindow window)
+ {
+ window.ViewModel.LoadApplications();
+ }
+
+ Close();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
new file mode 100644
index 00000000..4b004206
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
@@ -0,0 +1,107 @@
+<window:StyleableWindow xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
+ x:Class="Ryujinx.Ava.Ui.Windows.UserProfileWindow"
+ xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ CanResize="False"
+ Width="850" MinHeight="550" Height="550"
+ WindowStartupLocation="CenterOwner"
+ SizeToContent="Manual"
+ MinWidth="600">
+ <Design.DataContext>
+ <viewModels:UserProfileViewModel />
+ </Design.DataContext>
+ <Window.Resources>
+ <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+ </Window.Resources>
+ <Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid Grid.Row="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition />
+ </Grid.RowDefinitions>
+ <ContentControl
+ Focusable="False"
+ IsVisible="False"
+ KeyboardNavigation.IsTabStop="False">
+ <ui:ContentDialog Name="ContentDialog"
+ IsPrimaryButtonEnabled="True"
+ IsSecondaryButtonEnabled="True"
+ IsVisible="False" />
+ </ContentControl>
+ <TextBlock Text="{Locale:Locale UserProfilesSelectedUserProfile}" />
+ <Grid Grid.Row="1" Margin="10">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <Image Height="96" Width="96"
+ Source="{Binding SelectedProfile.Image, Converter={StaticResource ByteImage}}" />
+ <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
+ Margin="5, 10">
+ <TextBox Name="NameBox" Text="{Binding SelectedProfile.Name, Mode=OneWay}"
+ HorizontalAlignment="Stretch" />
+ <TextBlock Text="{Binding SelectedProfile.UserId}" />
+ </StackPanel>
+ <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="2" Spacing="10"
+ Margin="5">
+ <Button Content="{Locale:Locale UserProfilesSaveProfileName}" Name="SetNameButton"
+ Click="SetNameButton_OnClick" />
+ <Button Name="SelectProfileImage" Command="{Binding ChooseProfileImage}"
+ Content="{Locale:Locale UserProfilesChangeProfileImage}" />
+ </StackPanel>
+ </Grid>
+ </Grid>
+ <Grid Grid.Row="2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition />
+ </Grid.RowDefinitions>
+ <TextBlock Text="{Locale:Locale UserProfilesAvailableUserProfiles}" />
+ <ListBox Grid.Row="1" Margin="10" Name="ProfilesList" DoubleTapped="ProfilesList_DoubleTapped"
+ Items="{Binding Profiles}">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid HorizontalAlignment="Stretch">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Grid Grid.Column="0" Background="{DynamicResource ThemeAccentColorBrush}"
+ Grid.ColumnSpan="2"
+ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="5" MinWidth="5"
+ IsVisible="{Binding IsOpened}" />
+ <Image Grid.Column="0" Height="96" Width="96"
+ Source="{Binding Image, Converter={StaticResource ByteImage}}" />
+ <StackPanel Margin="10" Orientation="Vertical" HorizontalAlignment="Stretch"
+ VerticalAlignment="Center" Grid.Column="1">
+ <TextBlock Text="{Binding Name}" />
+ <TextBlock Text="{Binding UserId}" />
+ </StackPanel>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+ <StackPanel Grid.Row="3" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Stretch">
+ <Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
+ <Button IsEnabled="{Binding IsSelectedProfileDeletable}"
+ Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
+ <Button HorizontalAlignment="Right" Content="{Locale:Locale UserProfilesClose}" Click="CloseButton_OnClick"
+ Name="CloseButton" />
+ </StackPanel>
+ </Grid>
+</window:StyleableWindow> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
new file mode 100644
index 00000000..e78e0384
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
@@ -0,0 +1,102 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Threading.Tasks;
+using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
+
+namespace Ryujinx.Ava.Ui.Windows
+{
+ public class UserProfileWindow : StyleableWindow
+ {
+ private TextBox _nameBox;
+
+ public UserProfileWindow(AccountManager accountManager, ContentManager contentManager,
+ VirtualFileSystem virtualFileSystem)
+ {
+ AccountManager = accountManager;
+ ContentManager = contentManager;
+ ViewModel = new UserProfileViewModel(this);
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ if (contentManager.GetCurrentFirmwareVersion() != null)
+ {
+ Task.Run(() =>
+ {
+ AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
+ });
+ }
+
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
+ }
+
+ public UserProfileWindow()
+ {
+ ViewModel = new UserProfileViewModel();
+
+ DataContext = ViewModel;
+
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
+ }
+
+ public AccountManager AccountManager { get; }
+ public ContentManager ContentManager { get; }
+
+ public UserProfileViewModel ViewModel { get; set; }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ _nameBox = this.FindControl<TextBox>("NameBox");
+ }
+
+ private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
+ {
+ if (sender is ListBox listBox)
+ {
+ int selectedIndex = listBox.SelectedIndex;
+
+ if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
+ {
+ ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
+
+ AccountManager.OpenUser(ViewModel.SelectedProfile.UserId);
+
+ ViewModel.LoadProfiles();
+
+ foreach (UserProfile profile in ViewModel.Profiles)
+ {
+ profile.UpdateState();
+ }
+ }
+ }
+ }
+
+ private void CloseButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void SetNameButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (!string.IsNullOrWhiteSpace(_nameBox.Text))
+ {
+ ViewModel.SelectedProfile.Name = _nameBox.Text;
+ AccountManager.SetUserName(ViewModel.SelectedProfile.UserId, _nameBox.Text);
+ }
+ }
+ }
+} \ No newline at end of file