diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index db8d242418..fb8f800c01 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 3070fc0293..b2ad0d31dd 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 0000000000..e5e4f66b57 --- /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 489dfe6211..a3663af3e1 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 e524d6e4fe..fe446fb3ee 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 2dab1d3528..6dc99fb54a 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; - - DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged; - - Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})"; - - LoadDownloadableContents(); - PrintHeading(); } - private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) { - RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0); - } - - private void PrintHeading() - { - Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16")); - } - - private void LoadDownloadableContents() - { - foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) + ContentDialog contentDialog = new() { - 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) - { - _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), - downloadableContentContainer.ContainerPath, - downloadableContentNca.FullPath, - downloadableContentNca.Enabled)); - } - } - } - } - - // NOTE: Save the list again to remove leftovers. - Save(); - } - - private Nca TryOpenNca(IStorage ncaStorage, string containerPath) - { - try - { - return new Nca(_virtualFileSystem.KeySet, ncaStorage); - } - catch (Exception ex) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, containerPath)); - }); - } - - return null; - } - - 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; - } - - _downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true)); - - containsDownloadableContent = true; - } - } - - if (!containsDownloadableContent) - { - 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) - { - removedItems.Add(item as DownloadableContentModel); - } - - DlcDataGrid.SelectedItems.Clear(); - - 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 + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName), + Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")) }; - dialog.Filters.Add(new FileDialogFilter - { - Name = "NSP", - Extensions = { "nsp" } - }); + Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); - string[] files = await dialog.ShowAsync(this); + contentDialog.Styles.Add(bottomBorder); - if (files != null) - { - foreach (string file in files) - { - await AddDownloadableContent(file); - } - } - - PrintHeading(); + await ContentDialogHelper.ShowAsync(contentDialog); } - public void Save() + private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs) { - _downloadableContentContainerList.Clear(); + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } - DownloadableContentContainer container = default; + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } - foreach (DownloadableContentModel downloadableContent in _downloadableContents) + private void RemoveDLC(object sender, RoutedEventArgs e) + { + if (sender is Button button) { - if (container.ContainerPath != downloadableContent.ContainerPath) + if (button.DataContext is DownloadableContentModel model) { - if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + ViewModel.Remove(model); + } + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is DownloadableContentModel model) + { + OpenHelper.LocateFile(model.ContainerPath); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is DownloadableContentModel model) + { + var index = ViewModel.DownloadableContents.IndexOf(model); + + if (index != -1) { - _downloadableContentContainerList.Add(container); + ViewModel.DownloadableContents[index].Enabled = true; } - - container = new DownloadableContentContainer - { - ContainerPath = downloadableContent.ContainerPath, - DownloadableContentNcaList = new List<DownloadableContentNca>() - }; } + } - container.DownloadableContentNcaList.Add(new DownloadableContentNca + foreach (var content in e.RemovedItems) + { + if (content is DownloadableContentModel model) { - Enabled = downloadableContent.Enabled, - TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16), - FullPath = downloadableContent.FullPath - }); - } + var index = ViewModel.DownloadableContents.IndexOf(model); - if (!string.IsNullOrWhiteSpace(container.ContainerPath)) - { - _downloadableContentContainerList.Add(container); + if (index != -1) + { + ViewModel.DownloadableContents[index].Enabled = false; + } + } } - - 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