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 133ac41425
commit dc2aa837b3
18 changed files with 123 additions and 84 deletions

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
# Ryubing Setup Wizard
Directly modified from the code found [here](https://github.com/TKMM-Team/Tkmm/tree/master/src/Tkmm/Wizard).

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
using Avalonia;
using Avalonia.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPage
{
public SetupWizardPage WithTitle(LocaleKeys title) => WithTitle(LocaleManager.Instance[title]);
public SetupWizardPage WithTitle(string title)
{
Title = title;
return this;
}
public SetupWizardPage WithContent(LocaleKeys content) => WithContent(LocaleManager.Instance[content]);
public SetupWizardPage WithContent(object? content)
{
if (content is StyledElement { Parent: ContentControl parent })
{
parent.Content = null;
}
Content = content;
return this;
}
public SetupWizardPage WithHelpContent(LocaleKeys content) =>
WithHelpContent(LocaleManager.Instance[content]);
public SetupWizardPage WithHelpContent(object? content)
{
HelpContent = content;
return this;
}
public SetupWizardPage WithContent<TControl>(object? context = null) where TControl : Control, new()
{
Content = new TControl { DataContext = context };
return this;
}
public SetupWizardPage WithContent<TControl, TViewModel>(out TViewModel boundViewModel)
where TControl : RyujinxControl<TViewModel>, new()
where TViewModel : SetupWizardPageContext, new()
{
boundViewModel = new() { Notifications = ownerWizard.NotificationHelper };
return WithContent<TControl>(boundViewModel);
}
public SetupWizardPage WithActionContent(LocaleKeys content) =>
WithActionContent(LocaleManager.Instance[content]);
public SetupWizardPage WithActionContent(object? content)
{
ActionContent = content;
return this;
}
}
}

View File

@@ -0,0 +1,59 @@
using Avalonia.Controls.Presenters;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPage(ContentPresenter contentPresenter, RyujinxSetupWizard ownerWizard, bool isFirstPage = false) : BaseModel
{
private bool? _result;
private readonly CancellationTokenSource _cts = new();
public bool IsFirstPage { get; } = isFirstPage;
[ObservableProperty]
public partial string? Title { get; set; }
[ObservableProperty]
public partial object? Content { get; set; }
[ObservableProperty] public partial object? HelpContent { get; set; }
[ObservableProperty]
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];
[RelayCommand]
private void MoveBack()
{
_result = false;
_cts.Cancel();
}
[RelayCommand]
private void MoveNext()
{
_result = true;
_cts.Cancel();
}
public async ValueTask<bool> Show()
{
contentPresenter.Content = new SetupWizardPageView { ViewModel = this };
try
{
await Task.Delay(-1, _cts.Token);
}
catch (TaskCanceledException)
{
return _result ?? false;
}
return false;
}
}
}

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

@@ -0,0 +1,72 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:fa="using:Projektanker.Icons.Avalonia"
xmlns:wiz="using:Ryujinx.Ava.UI.SetupWizard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="wiz:SetupWizardPage"
x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView">
<Grid RowDefinitions="*,Auto" Margin="60">
<ScrollViewer>
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
TextWrapping="WrapWithOverflow"
FontSize="46"
Text="{Binding Title}" />
<ContentPresenter Grid.Row="1"
Content="{Binding}"
IsVisible="{Binding !#InfoToggle.IsChecked}"
TextWrapping="WrapWithOverflow"
Margin="0,15,0,0">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
<ContentControl Content="{Binding Content}" VerticalAlignment="Stretch"/>
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*"
IsVisible="{Binding #InfoToggle.IsChecked}">
<StackPanel Spacing="5" VerticalAlignment="Top">
<HyperlinkButton NavigateUri="https://discord.gg/PEuzjrFXUA" Content="Join Discord" />
</StackPanel>
<ContentPresenter Content="{Binding}"
Grid.Column="1"
Margin="20,0,0,0"
TextWrapping="WrapWithOverflow">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
<ContentControl Content="{Binding HelpContent}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
</Grid>
</Grid>
</ScrollViewer>
<Grid ColumnDefinitions="Auto,Auto,*" Grid.Row="1">
<ToggleButton Name="InfoToggle"
Padding="6">
<fa:Icon Value="fa-solid fa-circle-info" />
</ToggleButton>
<Button IsVisible="{Binding !IsFirstPage}"
Grid.Column="1"
Content="{ext:Locale SetupWizardActionBack}"
Margin="10,0,0,0"
Command="{Binding MoveBackCommand}" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Grid.Column="2">
<Button Content="{Binding ActionContent}"
Command="{Binding MoveNextCommand}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage>
{
public SetupWizardPageView()
{
InitializeComponent();
}
}
}