Begin extracting updates to match DLC refactors

This commit is contained in:
Jimmy Reichley 2024-08-18 23:29:34 -04:00
parent 7f854c3527
commit 3d7ede533f
No known key found for this signature in database
GPG key ID: 67715DC5A329803C
10 changed files with 393 additions and 106 deletions

View file

@ -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();

View 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");
}
}
}

View file

@ -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.",

View file

@ -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);
} }
} }

View file

@ -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))
{ {

View file

@ -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;
{
if (!ignoreNotFound)
{
Dispatcher.UIThread.InvokeAsync(() =>
ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
} }
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) return true;
{
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate);
}
}
}
catch (Exception ex)
{
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
}
} }
public void RemoveUpdate(TitleUpdateModel update) public void RemoveUpdate(TitleUpdateModel update)
{
if (!update.IsBundled)
{ {
TitleUpdates.Remove(update); 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]);
} }
SortUpdates(); totalUpdatesAdded += newUpdatesAdded;
}
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)
{ {
TitleUpdateWindowData.Paths.Add(update.Path); var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
return Dispatcher.UIThread.InvokeAsync(async () =>
if (update == SelectedUpdate as TitleUpdateModel)
{ {
TitleUpdateWindowData.Selected = update.Path; await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
} });
}
JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
} }
} }
} }

View file

@ -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"

View file

@ -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),
}; };

View file

@ -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"

View file

@ -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),
}; };