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

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})",
"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.",

View file

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

View file

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

View file

@ -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))
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)
{
Dispatcher.UIThread.InvokeAsync(() =>
ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
}
TitleUpdates.Add(update);
SelectedUpdate = update;
return;
}
foreach (var titleUpdate in titleUpdates)
{
if (titleUpdate.TitleIdBase != ApplicationData.Id)
{
continue;
}
TitleUpdates.Add(titleUpdate);
if (selected)
{
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate);
}
numUpdatesAdded++;
}
}
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)
{
TitleUpdates.Remove(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]);
}
totalUpdatesAdded += newUpdatesAdded;
}
SortUpdates();
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)
{
var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
return Dispatcher.UIThread.InvokeAsync(async () =>
{
TitleUpdateWindowData.Paths.Add(update.Path);
if (update == SelectedUpdate as TitleUpdateModel)
{
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);
});
}
}
}

View file

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

View file

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

View file

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

View file

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