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)
{
// _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json");
var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
if (!File.Exists(downloadableContentJsonPath))
{
@ -77,9 +76,7 @@ namespace Ryujinx.UI.Common.Helper
downloadableContentContainerList.Add(container);
}
// _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json");
// var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
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();
// Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage());
if (nca == null)
{
// result.Add((content, downloadableContentNca.Enabled));
continue;
}
@ -115,13 +110,6 @@ namespace Ryujinx.UI.Common.Helper
downloadableContentNca.FullPath);
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)
{
// TODO(jpr): emit failure
// Dispatcher.UIThread.InvokeAsync(async () =>
// {
// await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath));
@ -144,5 +133,10 @@ namespace Ryujinx.UI.Common.Helper
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
{
// NOTE: most consuming code relies on this model being value-comparable
public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath)
{
public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";

View file

@ -1,5 +1,6 @@
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 bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci";

View file

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

View file

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

View file

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

View file

@ -3,39 +3,22 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using DynamicData;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using FluentAvalonia.UI.Controls;
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 Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using Ryujinx.UI.Common.Models;
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 readonly ApplicationLibrary _applicationLibrary;
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
private AvaloniaList<DownloadableContentModel> _views = new();
@ -45,8 +28,6 @@ namespace Ryujinx.Ava.UI.ViewModels
private readonly ApplicationData _applicationData;
private readonly IStorageProvider _storageProvider;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AvaloniaList<DownloadableContentModel> DownloadableContents
{
get => _downloadableContents;
@ -97,7 +78,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{
_virtualFileSystem = virtualFileSystem;
_applicationLibrary = applicationLibrary;
_applicationData = applicationData;
@ -107,31 +87,14 @@ namespace Ryujinx.Ava.UI.ViewModels
_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();
}
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);
@ -144,7 +107,10 @@ namespace Ryujinx.Ava.UI.ViewModels
}
// 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.
Save();
@ -153,7 +119,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public void Sort()
{
DownloadableContents.AsObservableChangeSet()
DownloadableContents
// Sort bundled last
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.AsObservableChangeSet()
.Filter(Filter)
.Bind(out var view).AsObservableList();
@ -182,23 +152,6 @@ namespace Ryujinx.Ava.UI.ViewModels
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()
{
var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
@ -216,20 +169,30 @@ namespace Ryujinx.Ava.UI.ViewModels
},
});
var totalDlcAdded = 0;
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]);
}
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))
@ -237,41 +200,43 @@ namespace Ryujinx.Ava.UI.ViewModels
return false;
}
bool success = false;
foreach (var dlc in dlcs)
foreach (var dlc in dlcs.Where(dlc => dlc.TitleIdBase == _applicationData.IdBase))
{
if (dlc.TitleIdBase != _applicationData.IdBase)
if (!DownloadableContents.Contains(dlc))
{
continue;
}
DownloadableContents.Add(dlc);
Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(dlc));
Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc));
success = true;
numDlcAdded++;
}
}
if (success)
if (numDlcAdded > 0)
{
OnPropertyChanged(nameof(UpdateCount));
Sort();
}
return success;
return true;
}
public void Remove(DownloadableContentModel model)
{
DownloadableContents.Remove(model);
SelectedDownloadableContents.Remove(model);
if (!model.IsBundled)
{
DownloadableContents.Remove(model);
OnPropertyChanged(nameof(UpdateCount));
Sort();
}
}
public void RemoveAll()
{
DownloadableContents.Clear();
SelectedDownloadableContents.Clear();
DownloadableContents.RemoveMany(DownloadableContents.Where(it => !it.IsBundled));
OnPropertyChanged(nameof(UpdateCount));
Sort();
}
@ -301,40 +266,15 @@ namespace Ryujinx.Ava.UI.ViewModels
{
var dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList();
_applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs);
// _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 = SelectedDownloadableContents.Contains(downloadableContent),
// TitleId = downloadableContent.TitleId,
// FullPath = downloadableContent.FullPath,
// });
// }
//
// if (!string.IsNullOrWhiteSpace(container.ContainerPath))
// {
// _downloadableContentContainerList.Add(container);
// }
//
// JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
private Task ShowNewDlcAddedDialog(int numAdded)
{
var msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded);
return Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
});
}
}

View file

@ -19,13 +19,30 @@
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</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
Margin="0 0 0 10"
Grid.Row="0">
Grid.Row="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -64,7 +81,7 @@
</Grid>
</Panel>
<Border
Grid.Row="1"
Grid.Row="2"
Margin="0 0 0 24"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@ -157,7 +174,7 @@
</ListBox>
</Border>
<Panel
Grid.Row="2"
Grid.Row="3"
HorizontalAlignment="Stretch">
<StackPanel
Orientation="Horizontal"