AutoLoad DLC/updates (#12)

* Add hooks to ApplicationLibrary for loading DLC/updates

* Trigger DLC/update load on games refresh

* Initial moving of DLC/updates to UI.Common

* Use new models in ApplicationLibrary

* Make dlc/updates records; use ApplicationLibrary for loading logic

* Fix a bug with DLC window; rework some logic

* Auto-load bundled DLC on startup

* Autoload DLC

* Add setting for autoloading dlc/updates

* Remove dead code; bind to AppLibrary apps directly in mainwindow

* Stub out bulk dlc menu item

* Add localization; stub out bulk load updates

* Set autoload dirs explicitly

* Begin extracting updates to match DLC refactors

* Add title update autoloading

* Reduce size of settings sections

* Better cache lookup for apps

* Dont reload entire library on game version change

* Remove ApplicationAdded event; always enumerate nsp when autoloading
This commit is contained in:
Jimmy Reichley 2024-10-07 21:08:41 -04:00 committed by GitHub
parent 9a1863c752
commit 565acec468
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1509 additions and 459 deletions

View file

@ -6,22 +6,44 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:models="clr-namespace:Ryujinx.UI.Common.Models;assembly=Ryujinx.UI.Common"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Width="500"
Height="380"
mc:Ignorable="d"
x:DataType="viewModels:DownloadableContentManagerViewModel"
Focusable="True">
<UserControl.Resources>
<helpers:DownloadableContentLabelConverter x:Key="DownloadableContentLabel" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0 0 0 10"
Spacing="5"
Orientation="Horizontal"
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 DlcWindowBundledContentNotice}" />
</StackPanel>
<Panel
Margin="0 0 0 10"
Grid.Row="0">
Grid.Row="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -60,7 +82,7 @@
</Grid>
</Panel>
<Border
Grid.Row="1"
Grid.Row="2"
Margin="0 0 0 24"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@ -73,7 +95,7 @@
SelectionMode="Multiple, Toggle"
Background="Transparent"
SelectionChanged="OnSelectionChanged"
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
SelectedItems="{Binding SelectedDownloadableContents, Mode=OneWay}"
ItemsSource="{Binding Views}">
<ListBox.DataTemplates>
<DataTemplate
@ -96,8 +118,14 @@
VerticalAlignment="Center"
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{Binding Label}" />
TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DownloadableContentLabel}">
<Binding Path="FileName" />
<Binding Path="IsBundled" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="1"
Margin="10 0"
@ -147,7 +175,7 @@
</ListBox>
</Border>
<Panel
Grid.Row="2"
Grid.Row="3"
HorizontalAlignment="Stretch">
<StackPanel
Orientation="Horizontal"

View file

@ -3,11 +3,10 @@ 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;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
@ -23,21 +22,21 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
}
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
public DownloadableContentManagerWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
DataContext = ViewModel = new DownloadableContentManagerViewModel(applicationLibrary, applicationData);
InitializeComponent();
}
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = "",
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
Content = new DownloadableContentManagerWindow(applicationLibrary, applicationData),
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString),
};
@ -88,12 +87,7 @@ namespace Ryujinx.Ava.UI.Windows
{
if (content is DownloadableContentModel model)
{
var index = ViewModel.DownloadableContents.IndexOf(model);
if (index != -1)
{
ViewModel.DownloadableContents[index].Enabled = true;
}
ViewModel.Enable(model);
}
}
@ -101,12 +95,7 @@ namespace Ryujinx.Ava.UI.Windows
{
if (content is DownloadableContentModel model)
{
var index = ViewModel.DownloadableContents.IndexOf(model);
if (index != -1)
{
ViewModel.DownloadableContents[index].Enabled = false;
}
ViewModel.Disable(model);
}
}
}

View file

@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
using DynamicData;
using FluentAvalonia.UI.Controls;
using LibHac.Tools.FsSystem;
using Ryujinx.Ava.Common;
@ -26,6 +27,7 @@ using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@ -45,6 +47,7 @@ namespace Ryujinx.Ava.UI.Windows
private static string _launchApplicationId;
private static bool _startFullscreen;
internal readonly AvaHostUIHandler UiHandler;
private IDisposable _appLibraryAppsSubscription;
public VirtualFileSystem VirtualFileSystem { get; private set; }
public ContentManager ContentManager { get; private set; }
@ -136,14 +139,6 @@ namespace Ryujinx.Ava.UI.Windows
Program.DesktopScaleFactor = this.RenderScaling;
}
private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
ViewModel.Applications.Add(e.AppData);
});
}
private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e)
{
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound);
@ -472,7 +467,12 @@ namespace Ryujinx.Ava.UI.Windows
this);
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
_appLibraryAppsSubscription?.Dispose();
_appLibraryAppsSubscription = ApplicationLibrary.Applications
.Connect()
.ObserveOn(SynchronizationContext.Current)
.Bind(ViewModel.Applications)
.Subscribe();
ViewModel.RefreshFirmwareStatus();
@ -575,6 +575,7 @@ namespace Ryujinx.Ava.UI.Windows
ApplicationLibrary.CancelLoading();
InputManager.Dispose();
_appLibraryAppsSubscription?.Dispose();
Program.Exit();
base.OnClosing(e);
@ -596,7 +597,6 @@ namespace Ryujinx.Ava.UI.Windows
public void LoadApplications()
{
_applicationsLoadedOnce = true;
ViewModel.Applications.Clear();
StatusBarView.LoadProgressBar.IsVisible = true;
ViewModel.StatusBarProgressMaximum = 0;
@ -638,8 +638,18 @@ namespace Ryujinx.Ava.UI.Windows
Thread applicationLibraryThread = new(() =>
{
ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language;
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value;
if (autoloadDirs.Count > 0)
{
var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs);
var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs);
ShowNewContentAddedDialog(dlcLoaded, updatesLoaded);
}
_isLoading = false;
})
{
@ -648,5 +658,33 @@ namespace Ryujinx.Ava.UI.Windows
};
applicationLibraryThread.Start();
}
private Task ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded)
{
var msg = "";
if (numDlcAdded > 0 && numUpdatesAdded > 0)
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded);
}
else if (numDlcAdded > 0)
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded);
}
else if (numUpdatesAdded > 0)
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded);
}
else
{
return Task.CompletedTask;
}
return Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle],
msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
});
}
}
}

View file

@ -39,7 +39,7 @@ namespace Ryujinx.Ava.UI.Windows
{
InputPage.InputView?.SaveCurrentProfile();
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
if (Owner is MainWindow window && (ViewModel.GameDirectoryChanged || ViewModel.AutoloadDirectoryChanged))
{
window.LoadApplications();
}

View file

@ -6,20 +6,42 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:models="clr-namespace:Ryujinx.UI.Common.Models;assembly=Ryujinx.UI.Common"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Width="500"
Height="300"
mc:Ignorable="d"
x:DataType="viewModels:TitleUpdateViewModel"
Focusable="True">
<UserControl.Resources>
<helpers:TitleUpdateLabelConverter x:Key="TitleUpdateLabel" />
</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"
@ -38,8 +60,14 @@
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{Binding Label}" />
TextWrapping="Wrap">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TitleUpdateLabel}">
<Binding Path="DisplayVersion" />
<Binding Path="IsBundled" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<StackPanel
Spacing="10"
Orientation="Horizontal"
@ -72,7 +100,7 @@
</Panel>
</DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
DataType="viewModels:TitleUpdateViewNoUpdateSentinal">
<Panel
Height="33"
Margin="10">
@ -92,7 +120,7 @@
</ListBox>
</Border>
<Panel
Grid.Row="1"
Grid.Row="2"
HorizontalAlignment="Stretch">
<StackPanel
Orientation="Horizontal"

View file

@ -5,11 +5,10 @@ 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;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
@ -25,21 +24,21 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
}
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
public TitleUpdateWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData);
DataContext = ViewModel = new TitleUpdateViewModel(applicationLibrary, applicationData);
InitializeComponent();
}
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = "",
Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
Content = new TitleUpdateWindow(applicationLibrary, applicationData),
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString),
};
@ -60,17 +59,6 @@ namespace Ryujinx.Ava.UI.Windows
{
ViewModel.Save();
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
{
foreach (Window window in al.Windows)
{
if (window is MainWindow mainWindow)
{
mainWindow.LoadApplications();
}
}
}
((ContentDialog)Parent).Hide();
}