Refactor more logic out of DLC manager VM

This commit is contained in:
Jimmy Reichley 2024-08-17 14:17:21 -04:00
parent 1eb7146b90
commit 57de6a7dc5
No known key found for this signature in database
GPG key ID: 67715DC5A329803C
8 changed files with 87 additions and 130 deletions

View file

@ -22,8 +22,7 @@ namespace Ryujinx.UI.Common.Helper
public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase) public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase)
{ {
// _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
if (!File.Exists(downloadableContentJsonPath)) if (!File.Exists(downloadableContentJsonPath))
{ {
@ -77,9 +76,7 @@ namespace Ryujinx.UI.Common.Helper
downloadableContentContainerList.Add(container); downloadableContentContainerList.Add(container);
} }
// _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
// var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
} }
@ -102,11 +99,9 @@ namespace Ryujinx.UI.Common.Helper
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
// Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage()); Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage());
if (nca == null) if (nca == null)
{ {
// result.Add((content, downloadableContentNca.Enabled));
continue; continue;
} }
@ -115,13 +110,6 @@ namespace Ryujinx.UI.Common.Helper
downloadableContentNca.FullPath); downloadableContentNca.FullPath);
result.Add((content, downloadableContentNca.Enabled)); result.Add((content, downloadableContentNca.Enabled));
// if (downloadableContentNca.Enabled)
// {
// SelectedDownloadableContents.Add(content);
// }
// OnPropertyChanged(nameof(UpdateCount));
} }
} }
@ -136,6 +124,7 @@ namespace Ryujinx.UI.Common.Helper
} }
catch (Exception) catch (Exception)
{ {
// TODO(jpr): emit failure
// Dispatcher.UIThread.InvokeAsync(async () => // Dispatcher.UIThread.InvokeAsync(async () =>
// { // {
// await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath)); // await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath));
@ -144,5 +133,10 @@ namespace Ryujinx.UI.Common.Helper
return null; return null;
} }
private static string PathToGameDLCJson(ulong applicationIdBase)
{
return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
}
} }
} }

View file

@ -1,5 +1,6 @@
namespace Ryujinx.UI.Common.Models namespace Ryujinx.UI.Common.Models
{ {
// NOTE: most consuming code relies on this model being value-comparable
public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath) public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath)
{ {
public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci"; public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";

View file

@ -1,5 +1,6 @@
namespace Ryujinx.UI.Common.Models namespace Ryujinx.UI.Common.Models
{ {
// NOTE: most consuming code relies on this model being value-comparable
public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path) public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path)
{ {
public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci"; public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci";

View file

@ -711,7 +711,9 @@
"UpdateWindowTitle": "Title Update Manager", "UpdateWindowTitle": "Title Update Manager",
"CheatWindowHeading": "Cheats Available for {0} [{1}]", "CheatWindowHeading": "Cheats Available for {0} [{1}]",
"BuildId": "BuildId:", "BuildId": "BuildId:",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"DlcWindowHeading": "{0} Downloadable Content(s)", "DlcWindowHeading": "{0} Downloadable Content(s)",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"ModWindowHeading": "{0} Mod(s)", "ModWindowHeading": "{0} Mod(s)",
"UserProfilesEditProfile": "Edit Selected", "UserProfilesEditProfile": "Edit Selected",
"Cancel": "Cancel", "Cancel": "Cancel",

View file

@ -5,5 +5,6 @@ namespace Ryujinx.Ava.UI.Helpers
List, List,
Grid, Grid,
Chip, Chip,
Important,
} }
} }

View file

@ -14,6 +14,7 @@ namespace Ryujinx.Ava.UI.Helpers
{ Glyph.List, char.ConvertFromUtf32((int)Symbol.List) }, { Glyph.List, char.ConvertFromUtf32((int)Symbol.List) },
{ Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll) }, { Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll) },
{ Glyph.Chip, char.ConvertFromUtf32(59748) }, { Glyph.Chip, char.ConvertFromUtf32(59748) },
{ Glyph.Important, char.ConvertFromUtf32((int)Symbol.Important) },
}; };
public GlyphValueConverter(string key) public GlyphValueConverter(string key)

View file

@ -3,39 +3,22 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using DynamicData; using DynamicData;
using LibHac.Common; using FluentAvalonia.UI.Controls;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; 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 Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common; using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using Ryujinx.UI.Common.Models; using Ryujinx.UI.Common.Models;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using Path = System.IO.Path;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
public class DownloadableContentManagerViewModel : BaseModel public class DownloadableContentManagerViewModel : BaseModel
{ {
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
private readonly string _downloadableContentJsonPath;
private readonly VirtualFileSystem _virtualFileSystem;
private readonly ApplicationLibrary _applicationLibrary; private readonly ApplicationLibrary _applicationLibrary;
private AvaloniaList<DownloadableContentModel> _downloadableContents = new(); private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
private AvaloniaList<DownloadableContentModel> _views = new(); private AvaloniaList<DownloadableContentModel> _views = new();
@ -45,8 +28,6 @@ namespace Ryujinx.Ava.UI.ViewModels
private readonly ApplicationData _applicationData; private readonly ApplicationData _applicationData;
private readonly IStorageProvider _storageProvider; private readonly IStorageProvider _storageProvider;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AvaloniaList<DownloadableContentModel> DownloadableContents public AvaloniaList<DownloadableContentModel> DownloadableContents
{ {
get => _downloadableContents; get => _downloadableContents;
@ -97,7 +78,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
_virtualFileSystem = virtualFileSystem;
_applicationLibrary = applicationLibrary; _applicationLibrary = applicationLibrary;
_applicationData = applicationData; _applicationData = applicationData;
@ -107,31 +87,14 @@ namespace Ryujinx.Ava.UI.ViewModels
_storageProvider = desktop.MainWindow.StorageProvider; _storageProvider = desktop.MainWindow.StorageProvider;
} }
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json");
if (!File.Exists(_downloadableContentJsonPath))
{
_downloadableContentContainerList = new List<DownloadableContentContainer>();
Save();
}
try
{
_downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, _serializerContext.ListDownloadableContentContainer);
}
catch
{
Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
_downloadableContentContainerList = new List<DownloadableContentContainer>();
}
LoadDownloadableContents(); LoadDownloadableContents();
} }
private void LoadDownloadableContents() private void LoadDownloadableContents()
{ {
foreach ((DownloadableContentModel dlc, bool isEnabled) in _applicationLibrary.DownloadableContents.Items.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase)) var dlcs = _applicationLibrary.DownloadableContents.Items
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
{ {
DownloadableContents.Add(dlc); DownloadableContents.Add(dlc);
@ -144,7 +107,10 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
// NOTE: Try to load downloadable contents from PFS last to preserve enabled state. // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
AddDownloadableContent(_applicationData.Path); if (AddDownloadableContent(_applicationData.Path, out var newDlc) && newDlc > 0)
{
ShowNewDlcAddedDialog(newDlc);
}
// NOTE: Save the list again to remove leftovers. // NOTE: Save the list again to remove leftovers.
Save(); Save();
@ -153,7 +119,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public void Sort() public void Sort()
{ {
DownloadableContents.AsObservableChangeSet() DownloadableContents
// Sort bundled last
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.AsObservableChangeSet()
.Filter(Filter) .Filter(Filter)
.Bind(out var view).AsObservableList(); .Bind(out var view).AsObservableList();
@ -182,23 +152,6 @@ namespace Ryujinx.Ava.UI.ViewModels
return false; 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.DialogLoadFileErrorMessage], ex.Message, containerPath));
});
}
return null;
}
public async void Add() public async void Add()
{ {
var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
@ -216,20 +169,30 @@ namespace Ryujinx.Ava.UI.ViewModels
}, },
}); });
var totalDlcAdded = 0;
foreach (var file in result) foreach (var file in result)
{ {
if (!AddDownloadableContent(file.Path.LocalPath)) if (!AddDownloadableContent(file.Path.LocalPath, out var newDlcAdded))
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
} }
totalDlcAdded += newDlcAdded;
}
if (totalDlcAdded > 0)
{
await ShowNewDlcAddedDialog(0);
} }
} }
private bool AddDownloadableContent(string path) private bool AddDownloadableContent(string path, out int numDlcAdded)
{ {
if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path)) numDlcAdded = 0;
if (!File.Exists(path))
{ {
return true; return false;
} }
if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs)) if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs))
@ -237,41 +200,43 @@ namespace Ryujinx.Ava.UI.ViewModels
return false; return false;
} }
bool success = false; foreach (var dlc in dlcs.Where(dlc => dlc.TitleIdBase == _applicationData.IdBase))
foreach (var dlc in dlcs)
{ {
if (dlc.TitleIdBase != _applicationData.IdBase) if (!DownloadableContents.Contains(dlc))
{ {
continue; DownloadableContents.Add(dlc);
Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc));
numDlcAdded++;
} }
DownloadableContents.Add(dlc);
Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(dlc));
success = true;
} }
if (success) if (numDlcAdded > 0)
{ {
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
} }
return success; return true;
} }
public void Remove(DownloadableContentModel model) public void Remove(DownloadableContentModel model)
{ {
DownloadableContents.Remove(model);
SelectedDownloadableContents.Remove(model); SelectedDownloadableContents.Remove(model);
OnPropertyChanged(nameof(UpdateCount));
Sort(); if (!model.IsBundled)
{
DownloadableContents.Remove(model);
OnPropertyChanged(nameof(UpdateCount));
Sort();
}
} }
public void RemoveAll() public void RemoveAll()
{ {
DownloadableContents.Clear();
SelectedDownloadableContents.Clear(); SelectedDownloadableContents.Clear();
DownloadableContents.RemoveMany(DownloadableContents.Where(it => !it.IsBundled));
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
} }
@ -301,40 +266,15 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
var dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList(); var dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList();
_applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs); _applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs);
// _downloadableContentContainerList.Clear(); }
// DownloadableContentContainer container = default; private Task ShowNewDlcAddedDialog(int numAdded)
// {
// foreach (DownloadableContentModel downloadableContent in DownloadableContents) var msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded);
// { return Dispatcher.UIThread.InvokeAsync(async () =>
// if (container.ContainerPath != downloadableContent.ContainerPath) {
// { await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
// if (!string.IsNullOrWhiteSpace(container.ContainerPath)) });
// {
// _downloadableContentContainerList.Add(container);
// }
//
// container = new DownloadableContentContainer
// {
// ContainerPath = downloadableContent.ContainerPath,
// DownloadableContentNcaList = new List<DownloadableContentNca>(),
// };
// }
//
// container.DownloadableContentNcaList.Add(new DownloadableContentNca
// {
// Enabled = SelectedDownloadableContents.Contains(downloadableContent),
// TitleId = downloadableContent.TitleId,
// FullPath = downloadableContent.FullPath,
// });
// }
//
// if (!string.IsNullOrWhiteSpace(container.ContainerPath))
// {
// _downloadableContentContainerList.Add(container);
// }
//
// JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
} }
} }

View file

@ -19,13 +19,30 @@
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0 0 0 10"
Spacing="5"
Orientation="Horizontal">
<ui:FontIcon
Margin="0"
HorizontalAlignment="Stretch"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter Important}" />
<!-- NOTE: aligning to bottom for better visual alignment with glyph -->
<TextBlock
FontStyle="Italic"
VerticalAlignment="Bottom"
Text="{locale:Locale DlcWindowBundledContentNotice}" />
</StackPanel>
<Panel <Panel
Margin="0 0 0 10" Margin="0 0 0 10"
Grid.Row="0"> Grid.Row="1">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@ -64,7 +81,7 @@
</Grid> </Grid>
</Panel> </Panel>
<Border <Border
Grid.Row="1" Grid.Row="2"
Margin="0 0 0 24" Margin="0 0 0 24"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@ -157,7 +174,7 @@
</ListBox> </ListBox>
</Border> </Border>
<Panel <Panel
Grid.Row="2" Grid.Row="3"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<StackPanel <StackPanel
Orientation="Horizontal" Orientation="Horizontal"