* dotnet format style --severity info Some changes were manually reverted. * dotnet format analyzers --serverity info Some changes have been minimally adapted. * Restore a few unused methods and variables * Silence dotnet format IDE0060 warnings * Silence dotnet format IDE0052 warnings * Silence dotnet format IDE0059 warnings * Address or silence dotnet format IDE1006 warnings * Address dotnet format CA1816 warnings * Address dotnet format CA1822 warnings * Address or silence dotnet format CA1069 warnings * Make dotnet format succeed in style mode * Address dotnet format CA1401 warnings * Address remaining dotnet format analyzer warnings * Address review comments * dotnet-format fixes after rebase * Address most dotnet format whitespace warnings * Apply dotnet format whitespace formatting A few of them have been manually reverted and the corresponding warning was silenced * Format if-blocks correctly * Another rebase, another dotnet format run * Run dotnet format whitespace after rebase * Run dotnet format style after rebase * Run dotnet format after rebase and remove unused usings - analyzers - style - whitespace * Add comments to disabled warnings * Remove a few unused parameters * Simplify properties and array initialization, Use const when possible, Remove trailing commas * Start working on disabled warnings * Fix and silence a few dotnet-format warnings again * Address IDE0260 warnings * Address a few disabled IDE0060 warnings * Silence IDE0060 in .editorconfig * Revert "Simplify properties and array initialization, Use const when possible, Remove trailing commas" This reverts commit 9462e4136c0a2100dc28b20cf9542e06790aa67e. * dotnet format whitespace after rebase * dotnet format pass with new editorconfig * Fix naming style issues * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Revert one suggestion * Second dotnet format pass and fix build issues * Final pass of dotnet format * Add trailing commas * Fix formatting issues * Keep unnecessary assignment in IconColorPicker.cs * Use using declarations and extend resource lifetimes * Fix rebase issues * Adjust comment spacing * Fix typo * Fix naming issues * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Revert unintentional change * Remove unused file * Remove static keyword from ViewModels Binding of static members doesn't work and is silently ignored. --------- Co-authored-by: Ac_K <Acoustik666@gmail.com>
338 lines
11 KiB
C#
338 lines
11 KiB
C#
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.Threading.Tasks;
|
|
using Application = Avalonia.Application;
|
|
using Path = System.IO.Path;
|
|
|
|
namespace Ryujinx.Ava.UI.ViewModels
|
|
{
|
|
public class DownloadableContentManagerViewModel : BaseModel
|
|
{
|
|
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
|
private readonly string _downloadableContentJsonPath;
|
|
|
|
private readonly VirtualFileSystem _virtualFileSystem;
|
|
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
|
|
private AvaloniaList<DownloadableContentModel> _views = new();
|
|
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
|
|
|
private string _search;
|
|
private readonly ulong _titleId;
|
|
|
|
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
|
|
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)
|
|
{
|
|
_virtualFileSystem = virtualFileSystem;
|
|
|
|
_titleId = titleId;
|
|
|
|
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
|
|
|
try
|
|
{
|
|
_downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, _serializerContext.ListDownloadableContentContainer);
|
|
}
|
|
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()
|
|
{
|
|
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
|
|
AllowMultiple = true,
|
|
};
|
|
|
|
dialog.Filters.Add(new FileDialogFilter
|
|
{
|
|
Name = "NSP",
|
|
Extensions = { "nsp" },
|
|
});
|
|
|
|
if (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);
|
|
}
|
|
|
|
JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
|
|
}
|
|
|
|
}
|
|
}
|