Begin extracting updates to match DLC refactors
This commit is contained in:
parent
7f854c3527
commit
3d7ede533f
10 changed files with 393 additions and 106 deletions
|
@ -1,4 +1,5 @@
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
|
using DynamicData.Kernel;
|
||||||
using LibHac;
|
using LibHac;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
|
@ -741,6 +742,8 @@ namespace Ryujinx.UI.App.Common
|
||||||
foreach (var application in applications)
|
foreach (var application in applications)
|
||||||
{
|
{
|
||||||
it.AddOrUpdate(application);
|
it.AddOrUpdate(application);
|
||||||
|
LoadTitleUpdatesForApplication(application);
|
||||||
|
LoadDlcForApplication(application);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -776,6 +779,71 @@ namespace Ryujinx.UI.App.Common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadTitleUpdatesForApplication(ApplicationData application)
|
||||||
|
{
|
||||||
|
_titleUpdates.Edit(it =>
|
||||||
|
{
|
||||||
|
var savedUpdates =
|
||||||
|
TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase);
|
||||||
|
it.AddOrUpdate(savedUpdates);
|
||||||
|
|
||||||
|
var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected);
|
||||||
|
|
||||||
|
if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates))
|
||||||
|
{
|
||||||
|
var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet();
|
||||||
|
|
||||||
|
bool addedNewUpdate = false;
|
||||||
|
foreach (var update in bundledUpdates)
|
||||||
|
{
|
||||||
|
if (!savedUpdateLookup.Contains(update))
|
||||||
|
{
|
||||||
|
addedNewUpdate = true;
|
||||||
|
it.AddOrUpdate((update, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedNewUpdate)
|
||||||
|
{
|
||||||
|
var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList();
|
||||||
|
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDlcForApplication(ApplicationData application)
|
||||||
|
{
|
||||||
|
_downloadableContents.Edit(it =>
|
||||||
|
{
|
||||||
|
var savedDlc =
|
||||||
|
DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase);
|
||||||
|
it.AddOrUpdate(savedDlc);
|
||||||
|
|
||||||
|
if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc))
|
||||||
|
{
|
||||||
|
var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet();
|
||||||
|
|
||||||
|
bool addedNewDlc = false;
|
||||||
|
foreach (var dlc in bundledDlc)
|
||||||
|
{
|
||||||
|
if (!savedDlcLookup.Contains(dlc))
|
||||||
|
{
|
||||||
|
addedNewDlc = true;
|
||||||
|
it.AddOrUpdate((dlc, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedNewDlc)
|
||||||
|
{
|
||||||
|
var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList();
|
||||||
|
DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase,
|
||||||
|
gameDlcs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void LoadTitleUpdates()
|
public void LoadTitleUpdates()
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -833,6 +901,23 @@ namespace Ryujinx.UI.App.Common
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SaveTitleUpdatesForGame(ulong titleIdBase)
|
||||||
|
{
|
||||||
|
var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList();
|
||||||
|
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveTitleUpdatesForGame(ApplicationData application, List<(TitleUpdateModel, bool IsSelected)> updates)
|
||||||
|
{
|
||||||
|
_titleUpdates.Edit(it =>
|
||||||
|
{
|
||||||
|
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, updates);
|
||||||
|
|
||||||
|
it.Remove(it.Items.Where(item => item.TitleUpdate.TitleIdBase == application.IdBase));
|
||||||
|
it.AddOrUpdate(updates);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public int AutoLoadDownloadableContents(List<string> appDirs)
|
public int AutoLoadDownloadableContents(List<string> appDirs)
|
||||||
{
|
{
|
||||||
_cancellationToken = new CancellationTokenSource();
|
_cancellationToken = new CancellationTokenSource();
|
||||||
|
|
162
src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs
Normal file
162
src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Common.Keys;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
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.Common.Configuration;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata;
|
||||||
|
|
||||||
|
namespace Ryujinx.UI.Common.Helper
|
||||||
|
{
|
||||||
|
public static class TitleUpdatesHelper
|
||||||
|
{
|
||||||
|
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
|
public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
|
||||||
|
{
|
||||||
|
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
|
||||||
|
|
||||||
|
if (!File.Exists(titleUpdatesJsonPath))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata);
|
||||||
|
return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
|
||||||
|
{
|
||||||
|
var titleUpdateWindowData = new TitleUpdateMetadata
|
||||||
|
{
|
||||||
|
Selected = "",
|
||||||
|
Paths = [],
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ((TitleUpdateModel update, bool isSelected) in updates)
|
||||||
|
{
|
||||||
|
titleUpdateWindowData.Paths.Add(update.Path);
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected))
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application,
|
||||||
|
$"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
titleUpdateWindowData.Selected = update.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
|
||||||
|
JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
|
||||||
|
{
|
||||||
|
var result = new List<(TitleUpdateModel, bool IsSelected)>();
|
||||||
|
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
|
foreach (string path in titleUpdateMetadata.Paths)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs);
|
||||||
|
|
||||||
|
Dictionary<ulong, ContentMetaData> updates =
|
||||||
|
pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel);
|
||||||
|
|
||||||
|
Nca patchNca = null;
|
||||||
|
Nca controlNca = null;
|
||||||
|
|
||||||
|
if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program);
|
||||||
|
controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control);
|
||||||
|
|
||||||
|
if (controlNca == null || patchNca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationControlProperty controlData = new();
|
||||||
|
|
||||||
|
using UniqueRef<IFile> nacpFile = new();
|
||||||
|
|
||||||
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
|
||||||
|
.OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
|
||||||
|
.ThrowIfFailure();
|
||||||
|
|
||||||
|
var displayVersion = controlData.DisplayVersionString.ToString();
|
||||||
|
var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version,
|
||||||
|
displayVersion, path);
|
||||||
|
|
||||||
|
result.Add((update, path == titleUpdateMetadata.Selected));
|
||||||
|
}
|
||||||
|
catch (MissingKeyException exception)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application,
|
||||||
|
$"Your key set is missing a key with the name: {exception.Name}");
|
||||||
|
}
|
||||||
|
catch (InvalidDataException)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application,
|
||||||
|
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {path}");
|
||||||
|
}
|
||||||
|
catch (IOException exception)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, exception.Message);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application,
|
||||||
|
$"The file encountered was not of a valid type. File: '{path}' Error: {exception}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PathToGameUpdatesJson(ulong applicationIdBase)
|
||||||
|
{
|
||||||
|
return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -717,7 +717,8 @@
|
||||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||||
"UpdateWindowTitle": "Title Update Manager",
|
"UpdateWindowTitle": "Title Update Manager",
|
||||||
"UpdateWindowDlcAddedMessage": "{0} new update(s) added",
|
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||||
|
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||||
"BuildId": "BuildId:",
|
"BuildId": "BuildId:",
|
||||||
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
|
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
|
||||||
|
|
|
@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication);
|
await TitleUpdateWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication);
|
await DownloadableContentManagerWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
|
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
|
||||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||||
private AvaloniaList<DownloadableContentModel> _views = new();
|
private AvaloniaList<DownloadableContentModel> _views = new();
|
||||||
|
private bool _showBundledContentNotice = false;
|
||||||
|
|
||||||
private string _search;
|
private string _search;
|
||||||
private readonly ApplicationData _applicationData;
|
private readonly ApplicationData _applicationData;
|
||||||
|
@ -76,7 +77,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
|
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public bool ShowBundledContentNotice
|
||||||
|
{
|
||||||
|
get => _showBundledContentNotice;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_showBundledContentNotice = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadableContentManagerViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
_applicationLibrary = applicationLibrary;
|
_applicationLibrary = applicationLibrary;
|
||||||
|
|
||||||
|
@ -94,9 +105,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
var dlcs = _applicationLibrary.DownloadableContents.Items
|
var dlcs = _applicationLibrary.DownloadableContents.Items
|
||||||
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
||||||
|
|
||||||
|
bool hasBundledContent = false;
|
||||||
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
||||||
{
|
{
|
||||||
DownloadableContents.Add(dlc);
|
DownloadableContents.Add(dlc);
|
||||||
|
hasBundledContent = hasBundledContent || dlc.IsBundled;
|
||||||
|
|
||||||
if (isEnabled)
|
if (isEnabled)
|
||||||
{
|
{
|
||||||
|
@ -106,6 +120,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
OnPropertyChanged(nameof(UpdateCount));
|
OnPropertyChanged(nameof(UpdateCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShowBundledContentNotice = hasBundledContent;
|
||||||
|
|
||||||
Sort();
|
Sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,12 +203,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs))
|
if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs) || dlcs.Count == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var dlc in dlcs.Where(dlc => dlc.TitleIdBase == _applicationData.IdBase))
|
var dlcsForThisGame = dlcs.Where(it => it.TitleIdBase == _applicationData.IdBase);
|
||||||
|
if (!dlcsForThisGame.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dlc in dlcsForThisGame)
|
||||||
{
|
{
|
||||||
if (!DownloadableContents.Contains(dlc))
|
if (!DownloadableContents.Contains(dlc))
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,22 +2,17 @@ using Avalonia.Collections;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DynamicData.Kernel;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.Common.Logging;
|
|
||||||
using Ryujinx.Common.Utilities;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
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 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
|
||||||
{
|
{
|
||||||
|
@ -25,18 +20,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public class TitleUpdateViewModel : BaseModel
|
public class TitleUpdateViewModel : BaseModel
|
||||||
{
|
{
|
||||||
|
|
||||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
|
||||||
public readonly string TitleUpdateJsonPath;
|
|
||||||
private VirtualFileSystem VirtualFileSystem { get; }
|
|
||||||
private ApplicationLibrary ApplicationLibrary { get; }
|
private ApplicationLibrary ApplicationLibrary { get; }
|
||||||
private ApplicationData ApplicationData { get; }
|
private ApplicationData ApplicationData { get; }
|
||||||
|
|
||||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||||
private AvaloniaList<object> _views = new();
|
private AvaloniaList<object> _views = new();
|
||||||
private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
||||||
|
private bool _showBundledContentNotice = false;
|
||||||
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
||||||
|
|
||||||
public AvaloniaList<TitleUpdateModel> TitleUpdates
|
public AvaloniaList<TitleUpdateModel> TitleUpdates
|
||||||
{
|
{
|
||||||
|
@ -68,11 +58,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShowBundledContentNotice
|
||||||
|
{
|
||||||
|
get => _showBundledContentNotice;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_showBundledContentNotice = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public TitleUpdateViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
VirtualFileSystem = virtualFileSystem;
|
|
||||||
ApplicationLibrary = applicationLibrary;
|
ApplicationLibrary = applicationLibrary;
|
||||||
|
|
||||||
ApplicationData = applicationData;
|
ApplicationData = applicationData;
|
||||||
|
@ -82,43 +81,29 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdBaseString, "updates.json");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TitleUpdateWindowData = JsonHelper.DeserializeFromFile(TitleUpdateJsonPath, _serializerContext.TitleUpdateMetadata);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdBaseString} at {TitleUpdateJsonPath}");
|
|
||||||
|
|
||||||
TitleUpdateWindowData = new TitleUpdateMetadata
|
|
||||||
{
|
|
||||||
Selected = "",
|
|
||||||
Paths = new List<string>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadUpdates();
|
LoadUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
// Try to load updates from PFS first
|
var updates = ApplicationLibrary.TitleUpdates.Items
|
||||||
AddUpdate(ApplicationData.Path, true);
|
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
|
||||||
|
|
||||||
foreach (string path in TitleUpdateWindowData.Paths)
|
bool hasBundledContent = false;
|
||||||
|
SelectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
||||||
|
foreach ((TitleUpdateModel update, bool isSelected) in updates)
|
||||||
{
|
{
|
||||||
AddUpdate(path);
|
TitleUpdates.Add(update);
|
||||||
|
hasBundledContent = hasBundledContent || update.IsBundled;
|
||||||
|
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
SelectedUpdate = update;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected = TitleUpdates.FirstOrOptional(x => x.Path == TitleUpdateWindowData.Selected);
|
ShowBundledContentNotice = hasBundledContent;
|
||||||
SelectedUpdate = selected.HasValue ? selected.Value : new TitleUpdateViewNoUpdateSentinal();
|
|
||||||
|
|
||||||
// NOTE: Save the list again to remove leftovers.
|
|
||||||
Save();
|
|
||||||
SortUpdates();
|
SortUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,65 +111,76 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version);
|
var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version);
|
||||||
|
|
||||||
|
// NOTE(jpr): this works around a bug where calling Views.Clear also clears SelectedUpdate for
|
||||||
|
// some reason. so we save the item here and restore it after
|
||||||
|
var selected = SelectedUpdate;
|
||||||
|
|
||||||
Views.Clear();
|
Views.Clear();
|
||||||
Views.Add(new TitleUpdateViewNoUpdateSentinal());
|
Views.Add(new TitleUpdateViewNoUpdateSentinal());
|
||||||
Views.AddRange(sortedUpdates);
|
Views.AddRange(sortedUpdates);
|
||||||
|
|
||||||
|
SelectedUpdate = selected;
|
||||||
|
|
||||||
if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal)
|
if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal)
|
||||||
{
|
{
|
||||||
SelectedUpdate = Views[0];
|
SelectedUpdate = Views[0];
|
||||||
}
|
}
|
||||||
|
// this is mainly to handle a scenario where the user removes the selected update
|
||||||
else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate))
|
else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate))
|
||||||
{
|
{
|
||||||
SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0];
|
SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUpdate(string path, bool ignoreNotFound = false, bool selected = false)
|
private bool AddUpdate(string path, out int numUpdatesAdded)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path) || TitleUpdates.Any(x => x.Path == path))
|
numUpdatesAdded = 0;
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var updates))
|
||||||
{
|
{
|
||||||
if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var titleUpdates))
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatesForThisGame = updates.Where(it => it.TitleIdBase == ApplicationData.Id);
|
||||||
|
if (!updatesForThisGame.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var update in updatesForThisGame)
|
||||||
|
{
|
||||||
|
if (!TitleUpdates.Contains(update))
|
||||||
{
|
{
|
||||||
if (!ignoreNotFound)
|
TitleUpdates.Add(update);
|
||||||
{
|
SelectedUpdate = update;
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
ContentDialogHelper.CreateErrorDialog(
|
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
numUpdatesAdded++;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var titleUpdate in titleUpdates)
|
|
||||||
{
|
|
||||||
if (titleUpdate.TitleIdBase != ApplicationData.Id)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TitleUpdates.Add(titleUpdate);
|
|
||||||
|
|
||||||
if (selected)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
if (numUpdatesAdded > 0)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
|
SortUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveUpdate(TitleUpdateModel update)
|
public void RemoveUpdate(TitleUpdateModel update)
|
||||||
{
|
{
|
||||||
TitleUpdates.Remove(update);
|
if (!update.IsBundled)
|
||||||
|
{
|
||||||
|
TitleUpdates.Remove(update);
|
||||||
|
}
|
||||||
|
else if (update == SelectedUpdate as TitleUpdateModel)
|
||||||
|
{
|
||||||
|
SelectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
||||||
|
}
|
||||||
|
|
||||||
SortUpdates();
|
SortUpdates();
|
||||||
}
|
}
|
||||||
|
@ -205,30 +201,36 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var totalUpdatesAdded = 0;
|
||||||
foreach (var file in result)
|
foreach (var file in result)
|
||||||
{
|
{
|
||||||
AddUpdate(file.Path.LocalPath, selected: true);
|
if (!AddUpdate(file.Path.LocalPath, out var newUpdatesAdded))
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalUpdatesAdded += newUpdatesAdded;
|
||||||
}
|
}
|
||||||
|
|
||||||
SortUpdates();
|
if (totalUpdatesAdded > 0)
|
||||||
|
{
|
||||||
|
await ShowNewUpdatesAddedDialog(totalUpdatesAdded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
TitleUpdateWindowData.Paths.Clear();
|
var updates = TitleUpdates.Select(it => (it, it == SelectedUpdate as TitleUpdateModel)).ToList();
|
||||||
TitleUpdateWindowData.Selected = "";
|
ApplicationLibrary.SaveTitleUpdatesForGame(ApplicationData, updates);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (TitleUpdateModel update in TitleUpdates)
|
private Task ShowNewUpdatesAddedDialog(int numAdded)
|
||||||
|
{
|
||||||
|
var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
|
||||||
|
return Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
TitleUpdateWindowData.Paths.Add(update.Path);
|
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
|
||||||
|
});
|
||||||
if (update == SelectedUpdate as TitleUpdateModel)
|
|
||||||
{
|
|
||||||
TitleUpdateWindowData.Selected = update.Path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Margin="0 0 0 10"
|
Margin="0 0 0 10"
|
||||||
Spacing="5"
|
Spacing="5"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal"
|
||||||
|
IsVisible="{Binding ShowBundledContentNotice}">
|
||||||
<ui:FontIcon
|
<ui:FontIcon
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
|
|
@ -3,9 +3,7 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using Ryujinx.UI.Common.Models;
|
using Ryujinx.UI.Common.Models;
|
||||||
|
@ -24,21 +22,21 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public DownloadableContentManagerWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationLibrary, applicationData);
|
DataContext = ViewModel = new DownloadableContentManagerViewModel(applicationLibrary, applicationData);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationLibrary, applicationData),
|
Content = new DownloadableContentManagerWindow(applicationLibrary, applicationData),
|
||||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString),
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,29 @@
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Border
|
<StackPanel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
|
Margin="0 0 0 10"
|
||||||
|
Spacing="5"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
IsVisible="{Binding ShowBundledContentNotice}">
|
||||||
|
<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 UpdateWindowBundledContentNotice}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Grid.Row="1"
|
||||||
Margin="0 0 0 24"
|
Margin="0 0 0 24"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
@ -102,7 +120,7 @@
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Border>
|
</Border>
|
||||||
<Panel
|
<Panel
|
||||||
Grid.Row="1"
|
Grid.Row="2"
|
||||||
HorizontalAlignment="Stretch">
|
HorizontalAlignment="Stretch">
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
|
|
|
@ -5,9 +5,7 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using Ryujinx.UI.Common.Models;
|
using Ryujinx.UI.Common.Models;
|
||||||
|
@ -26,21 +24,21 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public TitleUpdateWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationLibrary, applicationData);
|
DataContext = ViewModel = new TitleUpdateViewModel(applicationLibrary, applicationData);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new TitleUpdateWindow(virtualFileSystem, applicationLibrary, applicationData),
|
Content = new TitleUpdateWindow(applicationLibrary, applicationData),
|
||||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString),
|
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue