aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.Ava/Assets/Locales/en_US.json4
-rw-r--r--Ryujinx.Ava/UI/Models/DownloadableContentModel.cs3
-rw-r--r--Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs340
-rw-r--r--Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs2
-rw-r--r--Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml280
-rw-r--r--Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs297
6 files changed, 546 insertions, 380 deletions
diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json
index db8d2424..fb8f800c 100644
--- a/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -583,10 +583,10 @@
"SelectUpdateDialogTitle": "Select update files",
"UserProfileWindowTitle": "User Profiles Manager",
"CheatWindowTitle": "Cheats Manager",
- "DlcWindowTitle": "Downloadable Content Manager",
+ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
"UpdateWindowTitle": "Title Update Manager",
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
- "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
+ "DlcWindowHeading": "{0} Downloadable Content(s)",
"UserProfilesEditProfile": "Edit Selected",
"Cancel": "Cancel",
"Save": "Save",
diff --git a/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs b/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs
index 3070fc02..b2ad0d31 100644
--- a/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs
+++ b/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs
@@ -1,4 +1,5 @@
using Ryujinx.Ava.UI.ViewModels;
+using System.IO;
namespace Ryujinx.Ava.UI.Models
{
@@ -21,6 +22,8 @@ namespace Ryujinx.Ava.UI.Models
public string ContainerPath { get; }
public string FullPath { get; }
+ public string FileName => Path.GetFileName(ContainerPath);
+
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
{
TitleId = titleId;
diff --git a/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
new file mode 100644
index 00000000..e5e4f66b
--- /dev/null
+++ b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -0,0 +1,340 @@
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+using DynamicData;
+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.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Path = System.IO.Path;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+ public class DownloadableContentManagerViewModel : BaseModel
+ {
+ private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
+ private readonly string _downloadableContentJsonPath;
+
+ private VirtualFileSystem _virtualFileSystem;
+ private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
+ private AvaloniaList<DownloadableContentModel> _views = new();
+ private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
+
+ private string _search;
+ private ulong _titleId;
+ private string _titleName;
+
+ public AvaloniaList<DownloadableContentModel> DownloadableContents
+ {
+ get => _downloadableContents;
+ set
+ {
+ _downloadableContents = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
+ }
+ }
+
+ public AvaloniaList<DownloadableContentModel> Views
+ {
+ get => _views;
+ set
+ {
+ _views = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public AvaloniaList<DownloadableContentModel> SelectedDownloadableContents
+ {
+ get => _selectedDownloadableContents;
+ set
+ {
+ _selectedDownloadableContents = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string Search
+ {
+ get => _search;
+ set
+ {
+ _search = value;
+ OnPropertyChanged();
+ Sort();
+ }
+ }
+
+ public string UpdateCount
+ {
+ get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
+ }
+
+ public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+ {
+ _virtualFileSystem = virtualFileSystem;
+
+ _titleId = titleId;
+ _titleName = titleName;
+
+ _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
+
+ try
+ {
+ _downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
+ }
+ catch
+ {
+ Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
+ _downloadableContentContainerList = new List<DownloadableContentContainer>();
+ }
+
+ LoadDownloadableContents();
+ }
+
+ private void LoadDownloadableContents()
+ {
+ foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
+ {
+ if (File.Exists(downloadableContentContainer.ContainerPath))
+ {
+ using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
+
+ PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+
+ _virtualFileSystem.ImportTickets(partitionFileSystem);
+
+ foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
+ {
+ using UniqueRef<IFile> ncaFile = new();
+
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
+ if (nca != null)
+ {
+ var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
+ downloadableContentContainer.ContainerPath,
+ downloadableContentNca.FullPath,
+ downloadableContentNca.Enabled);
+
+ DownloadableContents.Add(content);
+
+ if (content.Enabled)
+ {
+ SelectedDownloadableContents.Add(content);
+ }
+
+ OnPropertyChanged(nameof(UpdateCount));
+ }
+ }
+ }
+ }
+
+ // NOTE: Save the list again to remove leftovers.
+ Save();
+ Sort();
+ }
+
+ public void Sort()
+ {
+ DownloadableContents.AsObservableChangeSet()
+ .Filter(Filter)
+ .Bind(out var view).AsObservableList();
+
+ _views.Clear();
+ _views.AddRange(view);
+ OnPropertyChanged(nameof(Views));
+ }
+
+ private bool Filter(object arg)
+ {
+ if (arg is DownloadableContentModel content)
+ {
+ return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleId.ToLower().Contains(_search.ToLower());
+ }
+
+ return false;
+ }
+
+ private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
+ {
+ try
+ {
+ return new Nca(_virtualFileSystem.KeySet, ncaStorage);
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadNcaErrorMessage], ex.Message, containerPath));
+ });
+ }
+
+ return null;
+ }
+
+ public async void Add()
+ {
+ OpenFileDialog dialog = new OpenFileDialog()
+ {
+ Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
+ AllowMultiple = true
+ };
+
+ dialog.Filters.Add(new FileDialogFilter
+ {
+ Name = "NSP",
+ Extensions = { "nsp" }
+ });
+
+ if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ string[] files = await dialog.ShowAsync(desktop.MainWindow);
+
+ if (files != null)
+ {
+ foreach (string file in files)
+ {
+ await AddDownloadableContent(file);
+ }
+ }
+ }
+ }
+
+ private async Task AddDownloadableContent(string path)
+ {
+ if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
+ {
+ return;
+ }
+
+ using FileStream containerFile = File.OpenRead(path);
+
+ PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+ bool containsDownloadableContent = false;
+
+ _virtualFileSystem.ImportTickets(partitionFileSystem);
+
+ foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
+ if (nca == null)
+ {
+ continue;
+ }
+
+ if (nca.Header.ContentType == NcaContentType.PublicData)
+ {
+ if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
+ {
+ break;
+ }
+
+ var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
+ DownloadableContents.Add(content);
+ SelectedDownloadableContents.Add(content);
+
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
+
+ containsDownloadableContent = true;
+ }
+ }
+
+ if (!containsDownloadableContent)
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
+ }
+ }
+
+ public void Remove(DownloadableContentModel model)
+ {
+ DownloadableContents.Remove(model);
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
+ }
+
+ public void RemoveAll()
+ {
+ DownloadableContents.Clear();
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
+ }
+
+ public void EnableAll()
+ {
+ SelectedDownloadableContents = new(DownloadableContents);
+ }
+
+ public void DisableAll()
+ {
+ SelectedDownloadableContents.Clear();
+ }
+
+ public void Save()
+ {
+ _downloadableContentContainerList.Clear();
+
+ DownloadableContentContainer container = default;
+
+ foreach (DownloadableContentModel downloadableContent in DownloadableContents)
+ {
+ if (container.ContainerPath != downloadableContent.ContainerPath)
+ {
+ if (!string.IsNullOrWhiteSpace(container.ContainerPath))
+ {
+ _downloadableContentContainerList.Add(container);
+ }
+
+ container = new DownloadableContentContainer
+ {
+ ContainerPath = downloadableContent.ContainerPath,
+ DownloadableContentNcaList = new List<DownloadableContentNca>()
+ };
+ }
+
+ container.DownloadableContentNcaList.Add(new DownloadableContentNca
+ {
+ Enabled = downloadableContent.Enabled,
+ TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
+ FullPath = downloadableContent.FullPath
+ });
+ }
+
+ if (!string.IsNullOrWhiteSpace(container.ContainerPath))
+ {
+ _downloadableContentContainerList.Add(container);
+ }
+
+ using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
+ {
+ downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index 489dfe62..a3663af3 100644
--- a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -1564,7 +1564,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (SelectedApplication != null)
{
- await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
+ await DownloadableContentManagerWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
}
}
diff --git a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
index e524d6e4..fe446fb3 100644
--- a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
+++ b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
@@ -1,172 +1,194 @@
-<window:StyleableWindow
+<UserControl
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
- Width="800"
- Height="500"
- MinWidth="800"
- MinHeight="500"
- MaxWidth="800"
- MaxHeight="500"
- SizeToContent="Height"
- WindowStartupLocation="CenterOwner"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ Width="500"
+ Height="380"
mc:Ignorable="d"
+ x:CompileBindings="True"
+ x:DataType="viewModels:DownloadableContentManagerViewModel"
Focusable="True">
- <Grid Name="DownloadableContentGrid" Margin="15">
+ <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
- <RowDefinition Height="Auto" />
- <RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
- <TextBlock
- Name="Heading"
- Grid.Row="1"
- MaxWidth="500"
- Margin="20,15,20,20"
- HorizontalAlignment="Center"
- VerticalAlignment="Center"
- LineHeight="18"
- TextAlignment="Center"
- TextWrapping="Wrap" />
- <DockPanel
- Grid.Row="2"
- Margin="0"
- HorizontalAlignment="Left">
- <Button
- Name="EnableAllButton"
- MinWidth="90"
- Margin="5"
- Command="{Binding EnableAll}">
- <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
- </Button>
- <Button
- Name="DisableAllButton"
- MinWidth="90"
- Margin="5"
- Command="{Binding DisableAll}">
- <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
- </Button>
- </DockPanel>
+ <Panel
+ Margin="0 0 0 10"
+ Grid.Row="0">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="*" />
+ </Grid.ColumnDefinitions>
+ <TextBlock
+ Grid.Column="0"
+ Text="{Binding UpdateCount}" />
+ <StackPanel
+ Margin="10 0"
+ Grid.Column="1"
+ Orientation="Horizontal">
+ <Button
+ Name="EnableAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{ReflectionBinding EnableAll}">
+ <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
+ </Button>
+ <Button
+ Name="DisableAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{ReflectionBinding DisableAll}">
+ <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
+ </Button>
+ </StackPanel>
+ <TextBox
+ Grid.Column="2"
+ MinHeight="27"
+ MaxHeight="27"
+ HorizontalAlignment="Stretch"
+ Watermark="{locale:Locale Search}"
+ Text="{Binding Search}" />
+ </Grid>
+ </Panel>
<Border
- Grid.Row="3"
- Margin="5"
+ Grid.Row="1"
+ Margin="0 0 0 24"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
- BorderBrush="Gray"
- BorderThickness="1">
- <ScrollViewer
- VerticalAlignment="Stretch"
- HorizontalScrollBarVisibility="Auto"
- VerticalScrollBarVisibility="Auto">
- <DataGrid
- Name="DlcDataGrid"
- MinHeight="200"
- HorizontalAlignment="Stretch"
- VerticalAlignment="Stretch"
- CanUserReorderColumns="False"
- CanUserResizeColumns="True"
- CanUserSortColumns="True"
- HorizontalScrollBarVisibility="Auto"
- Items="{Binding _downloadableContents}"
- SelectionMode="Extended"
- VerticalScrollBarVisibility="Auto">
- <DataGrid.Styles>
- <Styles>
- <Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
- <Setter Property="HorizontalAlignment" Value="Left" />
- <Setter Property="HorizontalContentAlignment" Value="Left" />
- </Style>
- </Styles>
- <Styles>
- <Style Selector="DataGridCell:nth-child(1)">
- <Setter Property="HorizontalAlignment" Value="Right" />
- <Setter Property="HorizontalContentAlignment" Value="Right" />
- </Style>
- </Styles>
- </DataGrid.Styles>
- <DataGrid.Columns>
- <DataGridTemplateColumn Width="90">
- <DataGridTemplateColumn.CellTemplate>
- <DataTemplate>
- <CheckBox
- Width="50"
- MinWidth="40"
- HorizontalAlignment="Center"
- IsChecked="{Binding Enabled}" />
- </DataTemplate>
- </DataGridTemplateColumn.CellTemplate>
- <DataGridTemplateColumn.Header>
- <TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
- </DataGridTemplateColumn.Header>
- </DataGridTemplateColumn>
- <DataGridTextColumn Width="140" Binding="{Binding TitleId}">
- <DataGridTextColumn.Header>
- <TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
- </DataGridTextColumn.Header>
- </DataGridTextColumn>
- <DataGridTextColumn Width="280" Binding="{Binding FullPath}">
- <DataGridTextColumn.Header>
- <TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
- </DataGridTextColumn.Header>
- </DataGridTextColumn>
- <DataGridTextColumn Binding="{Binding ContainerPath}">
- <DataGridTextColumn.Header>
- <TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
- </DataGridTextColumn.Header>
- </DataGridTextColumn>
- </DataGrid.Columns>
- </DataGrid>
- </ScrollViewer>
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ BorderThickness="1"
+ CornerRadius="5"
+ Padding="2.5">
+ <ListBox
+ AutoScrollToSelectedItem="False"
+ VirtualizationMode="None"
+ SelectionMode="Multiple, Toggle"
+ Background="Transparent"
+ SelectionChanged="OnSelectionChanged"
+ SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
+ Items="{Binding Views}">
+ <ListBox.DataTemplates>
+ <DataTemplate
+ DataType="models:DownloadableContentModel">
+ <Panel Margin="10">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*" />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <Grid
+ Grid.Column="0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"></ColumnDefinition>
+ <ColumnDefinition Width="Auto"></ColumnDefinition>
+ </Grid.ColumnDefinitions>
+ <TextBlock
+ Grid.Column="0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ MaxLines="2"
+ TextWrapping="Wrap"
+ TextTrimming="CharacterEllipsis"
+ Text="{Binding FileName}" />
+ <TextBlock
+ Grid.Column="1"
+ Margin="10 0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Text="{Binding TitleId}" />
+ </Grid>
+ <StackPanel
+ Grid.Column="1"
+ Spacing="10"
+ Orientation="Horizontal"
+ HorizontalAlignment="Right">
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Click="OpenLocation">
+ <ui:SymbolIcon
+ Symbol="OpenFolder"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Click="RemoveDLC">
+ <ui:SymbolIcon
+ Symbol="Cancel"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ </StackPanel>
+ </Grid>
+ </Panel>
+ </DataTemplate>
+ </ListBox.DataTemplates>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Background" Value="Transparent" />
+ </Style>
+ </ListBox.Styles>
+ </ListBox>
</Border>
- <DockPanel
- Grid.Row="4"
- Margin="0"
+ <Panel
+ Grid.Row="2"
HorizontalAlignment="Stretch">
- <DockPanel Margin="0" HorizontalAlignment="Left">
+ <StackPanel
+ Orientation="Horizontal"
+ Spacing="10"
+ HorizontalAlignment="Left">
<Button
Name="AddButton"
MinWidth="90"
Margin="5"
- Command="{Binding Add}">
+ Command="{ReflectionBinding 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}">
+ Command="{ReflectionBinding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button>
- </DockPanel>
- <DockPanel Margin="0" HorizontalAlignment="Right">
+ </StackPanel>
+ <StackPanel
+ Orientation="Horizontal"
+ Spacing="10"
+ HorizontalAlignment="Right">
<Button
Name="SaveButton"
MinWidth="90"
Margin="5"
- Command="{Binding SaveAndClose}">
+ Click="SaveAndClose">
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button>
<Button
Name="CancelButton"
MinWidth="90"
Margin="5"
- Command="{Binding Close}">
+ Click="Close">
<TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button>
- </DockPanel>
- </DockPanel>
+ </StackPanel>
+ </Panel>
</Grid>
-</window:StyleableWindow> \ No newline at end of file
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs
index 2dab1d35..6dc99fb5 100644
--- a/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs
+++ b/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs
@@ -1,314 +1,115 @@
-using Avalonia.Collections;
using Avalonia.Controls;
-using Avalonia.Threading;
-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 Avalonia.Interactivity;
+using Avalonia.Styling;
+using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
-using Ryujinx.Common.Configuration;
-using Ryujinx.Common.Utilities;
+using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reactive.Linq;
-using System.Text;
+using Ryujinx.Ui.Common.Helper;
using System.Threading.Tasks;
-using Path = System.IO.Path;
+using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows
{
- public partial class DownloadableContentManagerWindow : StyleableWindow
+ public partial class DownloadableContentManagerWindow : UserControl
{
- private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
- private readonly string _downloadableContentJsonPath;
-
- private VirtualFileSystem _virtualFileSystem { get; }
- private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
-
- private ulong _titleId { get; }
- private string _titleName { get; }
+ public DownloadableContentManagerViewModel ViewModel;
public DownloadableContentManagerWindow()
{
DataContext = this;
InitializeComponent();
-
- Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
}
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{
- _virtualFileSystem = virtualFileSystem;
- _downloadableContents = new AvaloniaList<DownloadableContentModel>();
-
- _titleId = titleId;
- _titleName = titleName;
-
- _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
-
- try
- {
- _downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
- }
- catch
- {
- _downloadableContentContainerList = new List<DownloadableContentContainer>();
- }
-
- DataContext = this;
+ DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName);
InitializeComponent();
+ }
- RemoveButton.IsEnabled = false;
+ public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+ {
+ ContentDialog contentDialog = new()
+ {
+ PrimaryButtonText = "",
+ SecondaryButtonText = "",
+ CloseButtonText = "",
+ Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName),
+ Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16"))
+ };
- DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
+ Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
+ bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
- Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
+ contentDialog.Styles.Add(bottomBorder);
- LoadDownloadableContents();
- PrintHeading();
+ await ContentDialogHelper.ShowAsync(contentDialog);
}
- private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs)
{
- RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
+ ViewModel.Save();
+ ((ContentDialog)Parent).Hide();
}
- private void PrintHeading()
+ private void Close(object sender, RoutedEventArgs e)
{
- Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
+ ((ContentDialog)Parent).Hide();
}
- private void LoadDownloadableContents()
+ private void RemoveDLC(object sender, RoutedEventArgs e)
{
- foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
+ if (sender is Button button)
{
- if (File.Exists(downloadableContentContainer.ContainerPath))
+ if (button.DataContext is DownloadableContentModel model)
{
- using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
-
- PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
-
- foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
- {
- using UniqueRef<IFile> ncaFile = new();
-
- partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
- if (nca != null)
- {
- _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
- downloadableContentContainer.ContainerPath,
- downloadableContentNca.FullPath,
- downloadableContentNca.Enabled));
- }
- }
+ ViewModel.Remove(model);
}
}
-
- // NOTE: Save the list again to remove leftovers.
- Save();
}
- private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
+ private void OpenLocation(object sender, RoutedEventArgs e)
{
- try
- {
- return new Nca(_virtualFileSystem.KeySet, ncaStorage);
- }
- catch (Exception ex)
+ if (sender is Button button)
{
- Dispatcher.UIThread.InvokeAsync(async () =>
+ if (button.DataContext is DownloadableContentModel model)
{
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, containerPath));
- });
+ OpenHelper.LocateFile(model.ContainerPath);
+ }
}
-
- return null;
}
- private async Task AddDownloadableContent(string path)
+ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
- if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
+ foreach (var content in e.AddedItems)
{
- return;
- }
-
- using FileStream containerFile = File.OpenRead(path);
-
- PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
- bool containsDownloadableContent = false;
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
-
- foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef<IFile>();
-
- partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
- if (nca == null)
+ if (content is DownloadableContentModel model)
{
- continue;
- }
+ var index = ViewModel.DownloadableContents.IndexOf(model);
- if (nca.Header.ContentType == NcaContentType.PublicData)
- {
- if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
+ if (index != -1)
{
- break;
+ ViewModel.DownloadableContents[index].Enabled = true;
}
-
- _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
-
- containsDownloadableContent = true;
}
}
- if (!containsDownloadableContent)
+ foreach (var content in e.RemovedItems)
{
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
- }
- }
-
- private void RemoveDownloadableContents(bool removeSelectedOnly = false)
- {
- if (removeSelectedOnly)
- {
- AvaloniaList<DownloadableContentModel> removedItems = new();
-
- foreach (var item in DlcDataGrid.SelectedItems)
+ if (content is DownloadableContentModel model)
{
- removedItems.Add(item as DownloadableContentModel);
- }
-
- DlcDataGrid.SelectedItems.Clear();
+ var index = ViewModel.DownloadableContents.IndexOf(model);
- foreach (var item in removedItems)
- {
- _downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
- }
- }
- else
- {
- _downloadableContents.Clear();
- }
-
- PrintHeading();
- }
-
- public void RemoveSelected()
- {
- RemoveDownloadableContents(true);
- }
-
- public void RemoveAll()
- {
- RemoveDownloadableContents();
- }
-
- public void EnableAll()
- {
- foreach(var item in _downloadableContents)
- {
- item.Enabled = true;
- }
- }
-
- public void DisableAll()
- {
- foreach (var item in _downloadableContents)
- {
- item.Enabled = false;
- }
- }
-
- public async void Add()
- {
- OpenFileDialog dialog = new OpenFileDialog()
- {
- Title = LocaleManager.Instance[LocaleKeys.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)
- {
- await AddDownloadableContent(file);
- }
- }
-
- PrintHeading();
- }
-
- public void Save()
- {
- _downloadableContentContainerList.Clear();
-
- DownloadableContentContainer container = default;
-
- foreach (DownloadableContentModel downloadableContent in _downloadableContents)
- {
- if (container.ContainerPath != downloadableContent.ContainerPath)
- {
- if (!string.IsNullOrWhiteSpace(container.ContainerPath))
+ if (index != -1)
{
- _downloadableContentContainerList.Add(container);
+ ViewModel.DownloadableContents[index].Enabled = false;
}
-
- container = new DownloadableContentContainer
- {
- ContainerPath = downloadableContent.ContainerPath,
- DownloadableContentNcaList = new List<DownloadableContentNca>()
- };
}
-
- container.DownloadableContentNcaList.Add(new DownloadableContentNca
- {
- Enabled = downloadableContent.Enabled,
- TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
- FullPath = downloadableContent.FullPath
- });
- }
-
- if (!string.IsNullOrWhiteSpace(container.ContainerPath))
- {
- _downloadableContentContainerList.Add(container);
}
-
- using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
- {
- downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
- }
- }
-
- public void SaveAndClose()
- {
- Save();
- Close();
}
}
} \ No newline at end of file