Setup Wizard restructuring

- Remove polymorphic base, this only existed because TKMM has a desktop/switch setup prodecure difference and has 2 implementations of the setup wizard. We only need one.
- Remove Systems/UI file split, they're all in Ryujinx.Ava.UI now
- made NotificationHelper instance-based to allow you to encapsulate notifications to a window that magically disappear when the window is closed, instead of switching to showing on the main window.
This commit is contained in:
GreemDev 2025-11-24 03:45:19 -06:00
parent 3202ec72b2
commit 8d9d6b1afc
18 changed files with 123 additions and 84 deletions

View file

@ -1,20 +0,0 @@
using Avalonia.Controls.Presenters;
using Ryujinx.Ava.Common.Locale;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems.SetupWizard
{
public abstract class BaseSetupWizard(ContentPresenter presenter)
{
/// <summary>
/// Define the logic and flow of this <see cref="BaseSetupWizard"/>.
/// </summary>
public abstract Task Start();
protected SetupWizardPage FirstPage()
=> new(presenter, isFirstPage: true);
protected SetupWizardPage NextPage()
=> new(presenter);
}
}

View file

@ -1,10 +0,0 @@
using Gommon;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.Systems.SetupWizard
{
public abstract class SetupWizardPageContext: BaseModel
{
public abstract Result CompleteStep();
}
}

View file

@ -11,22 +11,24 @@ using System.Threading;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Helpers
{ {
public static class NotificationHelper public class NotificationHelper
{ {
public static NotificationHelper Shared { get; set; }
private const int MaxNotifications = 4; private const int MaxNotifications = 4;
private const int NotificationDelayInMs = 5000; private const int NotificationDelayInMs = 5000;
private static WindowNotificationManager _notificationManager; private readonly WindowNotificationManager _notificationManager;
private static readonly BlockingCollection<Notification> _notifications = new(); private readonly BlockingCollection<Notification> _notifications = new();
public static void SetNotificationManager(Window host) public NotificationHelper(Window host)
{ {
_notificationManager = new WindowNotificationManager(host) _notificationManager = new WindowNotificationManager(host)
{ {
Position = NotificationPosition.BottomRight, Position = NotificationPosition.BottomRight,
MaxItems = MaxNotifications, MaxItems = MaxNotifications,
Margin = new Thickness(0, 0, 15, 40), Margin = new Thickness(0, 0, 15, 40)
}; };
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new( Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
@ -49,8 +51,6 @@ namespace Ryujinx.Ava.UI.Helpers
host.Closing += (sender, args) => host.Closing += (sender, args) =>
{ {
if (sender is RyujinxSetupWizardWindow) return;
if (maybeAsyncWorkQueue.IsValueCreated) if (maybeAsyncWorkQueue.IsValueCreated)
{ {
maybeAsyncWorkQueue.Value.Dispose(); maybeAsyncWorkQueue.Value.Dispose();
@ -58,21 +58,75 @@ namespace Ryujinx.Ava.UI.Helpers
}; };
} }
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null) public static void Show(string title, string text, NotificationType type, bool waitingExit = false,
Action onClick = null, Action onClose = null)
=> Shared?.Notify(title, text, type, waitingExit, onClick, onClose);
public void Notify(string title, string text, NotificationType type, bool waitingExit = false,
Action onClick = null, Action onClose = null)
{ {
TimeSpan delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs); TimeSpan delay = waitingExit
? TimeSpan.FromMilliseconds(0)
: TimeSpan.FromMilliseconds(NotificationDelayInMs);
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); _notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
} }
public static void ShowError(string message, bool waitingExit = false) => #region Instance notification senders
ShowError(
public void NotifyInformation(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Notify(
title,
text,
NotificationType.Information,
waitingExit,
onClick,
onClose);
public void NotifySuccess(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Notify(
title,
text,
NotificationType.Success,
waitingExit,
onClick,
onClose);
public void NotifyWarning(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Notify(
title,
text,
NotificationType.Warning,
waitingExit,
onClick,
onClose);
public void NotifyError(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Notify(
title,
text,
NotificationType.Error,
waitingExit,
onClick,
onClose);
public void NotifyError(string message, bool waitingExit = false) =>
NotifyError(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle], LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
waitingExit: waitingExit waitingExit: waitingExit
); );
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => #endregion
#region Static notification senders
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show( Show(
title, title,
text, text,
@ -81,7 +135,8 @@ namespace Ryujinx.Ava.UI.Helpers
onClick, onClick,
onClose); onClose);
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show( Show(
title, title,
text, text,
@ -90,7 +145,8 @@ namespace Ryujinx.Ava.UI.Helpers
onClick, onClick,
onClose); onClose);
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show( Show(
title, title,
text, text,
@ -99,7 +155,8 @@ namespace Ryujinx.Ava.UI.Helpers
onClick, onClick,
onClose); onClose);
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show( Show(
title, title,
text, text,
@ -107,5 +164,14 @@ namespace Ryujinx.Ava.UI.Helpers
waitingExit, waitingExit,
onClick, onClick,
onClose); onClose);
public static void ShowError(string message, bool waitingExit = false) =>
ShowError(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
waitingExit: waitingExit
);
#endregion
} }
} }

View file

@ -4,8 +4,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Gommon; using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
@ -81,14 +79,14 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
SystemVersion installedFwVer = RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion(); SystemVersion installedFwVer = RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion();
if (installedFwVer != null) if (installedFwVer != null)
{ {
NotificationHelper.ShowInformation( Notifications.NotifyInformation(
"Firmware installed", "Firmware installed",
$"Installed firmware version {installedFwVer.VersionString}." $"Installed firmware version {installedFwVer.VersionString}."
); );
} }
else else
{ {
NotificationHelper.ShowError( Notifications.NotifyError(
"Firmware not installed", "Firmware not installed",
$"It seems some error occurred when trying to install the firmware at path '{FirmwareSourcePath}'." + $"It seems some error occurred when trying to install the firmware at path '{FirmwareSourcePath}'." +
"\nDid that folder contain a firmware dump?" "\nDid that folder contain a firmware dump?"
@ -96,9 +94,6 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
} }
RyujinxApp.MainWindow.ViewModel.RefreshFirmwareStatus(installedFwVer, allowNullVersion: true); RyujinxApp.MainWindow.ViewModel.RefreshFirmwareStatus(installedFwVer, allowNullVersion: true);
if (installedFwVer is null)
return Result.Fail;
// Purge Applet Cache. // Purge Applet Cache.
DirectoryInfo miiEditorCacheFolder = new( DirectoryInfo miiEditorCacheFolder = new(
@ -112,7 +107,7 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
} }
catch (Exception e) catch (Exception e)
{ {
NotificationHelper.ShowError(e.Message, waitingExit: true); Notifications.NotifyError(e.Message, waitingExit: true);
return Result.Fail; return Result.Fail;
} }

View file

@ -5,8 +5,6 @@ using CommunityToolkit.Mvvm.Input;
using DynamicData; using DynamicData;
using Gommon; using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -42,7 +40,7 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
} }
} }
private static Result InstallKeys(string directory) private Result InstallKeys(string directory)
{ {
try try
{ {
@ -57,18 +55,18 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
ContentManager.InstallKeys(directory, systemDirectory); ContentManager.InstallKeys(directory, systemDirectory);
NotificationHelper.ShowInformation( Notifications.NotifyInformation(
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo], title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
text: LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage]); text: LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage]);
} }
catch (InvalidFirmwarePackageException ifwpe) catch (InvalidFirmwarePackageException ifwpe)
{ {
NotificationHelper.ShowError(ifwpe.Message, waitingExit: true); Notifications.NotifyError(ifwpe.Message, waitingExit: true);
return Result.Failure(NoKeysFoundInFolder.Shared); return Result.Failure(NoKeysFoundInFolder.Shared);
} }
catch (MissingKeyException ex) catch (MissingKeyException ex)
{ {
NotificationHelper.ShowError(ex.ToString(), waitingExit: true); Notifications.NotifyError(ex.ToString(), waitingExit: true);
return Result.Failure(NoKeysFoundInFolder.Shared); return Result.Failure(NoKeysFoundInFolder.Shared);
} }
catch (Exception ex) catch (Exception ex)
@ -80,7 +78,7 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, directory); LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, directory);
} }
NotificationHelper.ShowError(message, waitingExit: true); Notifications.NotifyError(message, waitingExit: true);
return Result.Failure(new MessageError(message)); return Result.Failure(new MessageError(message));
} }

View file

@ -1,6 +1,5 @@
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.SetupWizard.Pages; using Ryujinx.Ava.UI.SetupWizard.Pages;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
@ -9,17 +8,18 @@ using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public class RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow) public class RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow)
: BaseSetupWizard(wizardWindow.WizardPresenter)
{ {
private readonly MainWindow _mainWindow = RyujinxApp.MainWindow; private readonly MainWindow _mainWindow = RyujinxApp.MainWindow;
private bool _configWasModified; private bool _configWasModified;
public bool HasFirmware => _mainWindow.ContentManager.GetCurrentFirmwareVersion() != null; public bool HasFirmware => _mainWindow.ContentManager.GetCurrentFirmwareVersion() != null;
public NotificationHelper NotificationHelper { get; private set; }
public override async Task Start() public async Task Start()
{ {
NotificationHelper.SetNotificationManager(wizardWindow); NotificationHelper = new NotificationHelper(wizardWindow);
RyujinxSetupWizardWindow.IsOpen = true; RyujinxSetupWizardWindow.IsOpen = true;
Start: Start:
await FirstPage() await FirstPage()
@ -39,7 +39,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
goto Keys; goto Keys;
Return: Return:
NotificationHelper.SetNotificationManager(_mainWindow); NotificationHelper = null;
wizardWindow.Close(); wizardWindow.Close();
RyujinxSetupWizardWindow.IsOpen = false; RyujinxSetupWizardWindow.IsOpen = false;
@ -93,6 +93,10 @@ namespace Ryujinx.Ava.UI.SetupWizard
return true; return true;
} }
private SetupWizardPage FirstPage() => new(wizardWindow.WizardPresenter, this, isFirstPage: true);
private SetupWizardPage NextPage() => new(wizardWindow.WizardPresenter, this);
public void SignalConfigModified() public void SignalConfigModified()
{ {
_configWasModified = true; _configWasModified = true;

View file

@ -1,7 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Gommon;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -31,14 +29,14 @@ namespace Ryujinx.Ava.UI.SetupWizard
return Task.CompletedTask; return Task.CompletedTask;
Task windowTask = ShowAsync( Task windowTask = ShowAsync(
CreateWindow(out BaseSetupWizard wiz), CreateWindow(out RyujinxSetupWizard wiz),
owner owner
); );
_ = wiz.Start(); _ = wiz.Start();
return windowTask; return windowTask;
} }
public static RyujinxSetupWizardWindow CreateWindow(out BaseSetupWizard setupWizard) public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard)
{ {
RyujinxSetupWizardWindow window = new(); RyujinxSetupWizardWindow window = new();
window.DataContext = setupWizard = new RyujinxSetupWizard(window); window.DataContext = setupWizard = new RyujinxSetupWizard(window);

View file

@ -2,10 +2,8 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.SetupWizard;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.Systems.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public partial class SetupWizardPage public partial class SetupWizardPage
{ {
@ -46,11 +44,11 @@ namespace Ryujinx.Ava.Systems.SetupWizard
return this; return this;
} }
public SetupWizardPage WithContent<TControl, TViewModel>(out TViewModel boundViewModel) public SetupWizardPage WithContent<TControl, TViewModel>(out TViewModel boundViewModel)
where TControl : RyujinxControl<TViewModel>, new() where TControl : RyujinxControl<TViewModel>, new()
where TViewModel : BaseModel, new() where TViewModel : SetupWizardPageContext, new()
{ {
boundViewModel = new(); boundViewModel = new() { Notifications = ownerWizard.NotificationHelper };
return WithContent<TControl>(boundViewModel); return WithContent<TControl>(boundViewModel);
} }

View file

@ -2,15 +2,13 @@ using Avalonia.Controls.Presenters;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.SetupWizard;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public partial class SetupWizardPage(ContentPresenter contentPresenter, bool isFirstPage = false) : BaseModel public partial class SetupWizardPage(ContentPresenter contentPresenter, RyujinxSetupWizard ownerWizard, bool isFirstPage = false) : BaseModel
{ {
private bool? _result; private bool? _result;
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();

View file

@ -0,0 +1,13 @@
using Gommon;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.UI.SetupWizard
{
public abstract class SetupWizardPageContext : BaseModel
{
public NotificationHelper Notifications { get; init; }
public abstract Result CompleteStep();
}
}

View file

@ -4,10 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:fa="using:Projektanker.Icons.Avalonia" xmlns:fa="using:Projektanker.Icons.Avalonia"
xmlns:wiz="using:Ryujinx.Ava.Systems.SetupWizard" xmlns:wiz="using:Ryujinx.Ava.UI.SetupWizard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="wiz:SetupWizardPage" x:DataType="wiz:SetupWizardPage"
x:Class="Ryujinx.Ava.Systems.SetupWizard.SetupWizardPageView"> x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView">
<Grid RowDefinitions="*,Auto" Margin="60"> <Grid RowDefinitions="*,Auto" Margin="60">
<ScrollViewer> <ScrollViewer>
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">

View file

@ -1,6 +1,6 @@
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.Systems.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage> public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage>
{ {

View file

@ -15,7 +15,6 @@ using Ryujinx.Ava.Systems;
using Ryujinx.Ava.Systems.AppLibrary; using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.UI; using Ryujinx.Ava.Systems.Configuration.UI;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Applet;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
@ -136,7 +135,7 @@ namespace Ryujinx.Ava.UI.Windows
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
NotificationHelper.SetNotificationManager(this); NotificationHelper.Shared = new NotificationHelper(this);
Executor.ExecuteBackgroundAsync(async () => Executor.ExecuteBackgroundAsync(async () =>
{ {