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.Kernel;
|
||||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
|
@ -741,6 +742,8 @@ namespace Ryujinx.UI.App.Common
|
|||
foreach (var application in applications)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
_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})",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"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}]",
|
||||
"BuildId": "BuildId:",
|
||||
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
|
||||
|
|
|
@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||
|
||||
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)
|
||||
{
|
||||
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> _selectedDownloadableContents = new();
|
||||
private AvaloniaList<DownloadableContentModel> _views = new();
|
||||
private bool _showBundledContentNotice = false;
|
||||
|
||||
private string _search;
|
||||
private readonly ApplicationData _applicationData;
|
||||
|
@ -76,7 +77,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
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;
|
||||
|
||||
|
@ -94,9 +105,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
{
|
||||
var dlcs = _applicationLibrary.DownloadableContents.Items
|
||||
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
||||
{
|
||||
DownloadableContents.Add(dlc);
|
||||
hasBundledContent = hasBundledContent || dlc.IsBundled;
|
||||
|
||||
if (isEnabled)
|
||||
{
|
||||
|
@ -106,6 +120,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
OnPropertyChanged(nameof(UpdateCount));
|
||||
}
|
||||
|
||||
ShowBundledContentNotice = hasBundledContent;
|
||||
|
||||
Sort();
|
||||
}
|
||||
|
||||
|
@ -187,12 +203,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs))
|
||||
if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs) || dlcs.Count == 0)
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
|
|
@ -2,22 +2,17 @@ using Avalonia.Collections;
|
|||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData.Kernel;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.UI.App.Common;
|
||||
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
|
||||
{
|
||||
|
@ -25,18 +20,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
|
||||
public class TitleUpdateViewModel : BaseModel
|
||||
{
|
||||
|
||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
||||
public readonly string TitleUpdateJsonPath;
|
||||
private VirtualFileSystem VirtualFileSystem { get; }
|
||||
private ApplicationLibrary ApplicationLibrary { get; }
|
||||
private ApplicationData ApplicationData { get; }
|
||||
|
||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||
private AvaloniaList<object> _views = new();
|
||||
private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
||||
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private bool _showBundledContentNotice = false;
|
||||
|
||||
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 TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
public TitleUpdateViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
ApplicationLibrary = applicationLibrary;
|
||||
|
||||
ApplicationData = applicationData;
|
||||
|
@ -82,43 +81,29 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
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();
|
||||
}
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
// Try to load updates from PFS first
|
||||
AddUpdate(ApplicationData.Path, true);
|
||||
var updates = ApplicationLibrary.TitleUpdates.Items
|
||||
.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);
|
||||
SelectedUpdate = selected.HasValue ? selected.Value : new TitleUpdateViewNoUpdateSentinal();
|
||||
ShowBundledContentNotice = hasBundledContent;
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
SortUpdates();
|
||||
}
|
||||
|
||||
|
@ -126,65 +111,76 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
{
|
||||
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.Add(new TitleUpdateViewNoUpdateSentinal());
|
||||
Views.AddRange(sortedUpdates);
|
||||
|
||||
SelectedUpdate = selected;
|
||||
|
||||
if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal)
|
||||
{
|
||||
SelectedUpdate = Views[0];
|
||||
}
|
||||
// this is mainly to handle a scenario where the user removes the selected update
|
||||
else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate))
|
||||
{
|
||||
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))
|
||||
{
|
||||
if (!ignoreNotFound)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||
return false;
|
||||
}
|
||||
|
||||
return;
|
||||
var updatesForThisGame = updates.Where(it => it.TitleIdBase == ApplicationData.Id);
|
||||
if (!updatesForThisGame.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var titleUpdate in titleUpdates)
|
||||
foreach (var update in updatesForThisGame)
|
||||
{
|
||||
if (titleUpdate.TitleIdBase != ApplicationData.Id)
|
||||
if (!TitleUpdates.Contains(update))
|
||||
{
|
||||
continue;
|
||||
TitleUpdates.Add(update);
|
||||
SelectedUpdate = update;
|
||||
|
||||
numUpdatesAdded++;
|
||||
}
|
||||
}
|
||||
|
||||
TitleUpdates.Add(titleUpdate);
|
||||
if (numUpdatesAdded > 0)
|
||||
{
|
||||
SortUpdates();
|
||||
}
|
||||
|
||||
if (selected)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveUpdate(TitleUpdateModel update)
|
||||
{
|
||||
if (!update.IsBundled)
|
||||
{
|
||||
TitleUpdates.Remove(update);
|
||||
}
|
||||
else if (update == SelectedUpdate as TitleUpdateModel)
|
||||
{
|
||||
SelectedUpdate = new TitleUpdateViewNoUpdateSentinal();
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
}
|
||||
|
@ -205,30 +201,36 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
},
|
||||
});
|
||||
|
||||
var totalUpdatesAdded = 0;
|
||||
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]);
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
totalUpdatesAdded += newUpdatesAdded;
|
||||
}
|
||||
|
||||
if (totalUpdatesAdded > 0)
|
||||
{
|
||||
await ShowNewUpdatesAddedDialog(totalUpdatesAdded);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
TitleUpdateWindowData.Paths.Clear();
|
||||
TitleUpdateWindowData.Selected = "";
|
||||
var updates = TitleUpdates.Select(it => (it, it == SelectedUpdate as TitleUpdateModel)).ToList();
|
||||
ApplicationLibrary.SaveTitleUpdatesForGame(ApplicationData, updates);
|
||||
}
|
||||
|
||||
foreach (TitleUpdateModel update in TitleUpdates)
|
||||
private Task ShowNewUpdatesAddedDialog(int numAdded)
|
||||
{
|
||||
TitleUpdateWindowData.Paths.Add(update.Path);
|
||||
|
||||
if (update == SelectedUpdate as TitleUpdateModel)
|
||||
var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
|
||||
return Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
TitleUpdateWindowData.Selected = update.Path;
|
||||
}
|
||||
}
|
||||
|
||||
JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
|
||||
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
Grid.Row="0"
|
||||
Margin="0 0 0 10"
|
||||
Spacing="5"
|
||||
Orientation="Horizontal">
|
||||
Orientation="Horizontal"
|
||||
IsVisible="{Binding ShowBundledContentNotice}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
|
|
@ -3,9 +3,7 @@ using Avalonia.Interactivity;
|
|||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using Ryujinx.UI.Common.Models;
|
||||
|
@ -24,21 +22,21 @@ namespace Ryujinx.Ava.UI.Windows
|
|||
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();
|
||||
}
|
||||
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationLibrary, applicationData),
|
||||
Content = new DownloadableContentManagerWindow(applicationLibrary, applicationData),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString),
|
||||
};
|
||||
|
||||
|
|
|
@ -19,11 +19,29 @@
|
|||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
<StackPanel
|
||||
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"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
|
@ -102,7 +120,7 @@
|
|||
</ListBox>
|
||||
</Border>
|
||||
<Panel
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
|
|
|
@ -5,9 +5,7 @@ using Avalonia.Interactivity;
|
|||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using Ryujinx.UI.Common.Models;
|
||||
|
@ -26,21 +24,21 @@ namespace Ryujinx.Ava.UI.Windows
|
|||
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();
|
||||
}
|
||||
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new TitleUpdateWindow(virtualFileSystem, applicationLibrary, applicationData),
|
||||
Content = new TitleUpdateWindow(applicationLibrary, applicationData),
|
||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue