Initial work on a setup wizard

Setup wizard abstraction & architecture from TKMM
This commit is contained in:
GreemDev
2025-11-21 00:20:15 -06:00
parent 66f339d265
commit b033adbde7
19 changed files with 743 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.UI.SetupWizard;
using System;
using System.Diagnostics;

View File

@@ -0,0 +1,22 @@
<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:pages="clr-namespace:Ryujinx.UI.SetupWizard.Pages"
xmlns:markup="clr-namespace:Ryujinx.Ava.Common.Markup"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.UI.SetupWizard.Pages.SetupKeysPage"
x:DataType="pages:SetupKeysPageViewModel">
<StackPanel>
<TextBlock Text="{markup:Locale SetupWizardKeysPageDescription}" Margin="0,0,0,10"/>
<Grid ColumnDefinitions="*,Auto">
<TextBox Name="KeysFolderPathField" Text="{Binding KeysFolderPath}" IsReadOnly="True" />
<Button Grid.Column="1"
Content="..."
Command="{Binding BrowseCommand}"
CommandParameter="{Binding #KeysFolderPathField}"
Margin="5,0,0,0"/>
</Grid>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.Threading.Tasks;
namespace Ryujinx.UI.SetupWizard.Pages
{
public partial class SetupKeysPageViewModel : BaseModel
{
[ObservableProperty]
public partial string? KeysFolderPath { get; set; }
[RelayCommand]
private static async Task Browse(TextBox tb)
{
var result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions {
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle],
AllowMultiple = false
}) switch {
[var target] => target.TryGetLocalPath(),
_ => null
};
if (result is not null)
{
tb.Text = result;
}
}
}
}

View File

@@ -0,0 +1,78 @@
using Avalonia.Controls.Presenters;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Logging;
using Ryujinx.UI.SetupWizard;
using Ryujinx.UI.SetupWizard.Pages;
using System;
using System.IO;
using System.Threading.Tasks;
using Logger = Ryujinx.Common.Logging.Logger;
namespace Ryujinx.Ava.UI.SetupWizard
{
public class RyujinxSetupWizard(ContentPresenter presenter, MainWindowViewModel mwvm, Action onClose) : BaseSetupWizard(presenter)
{
private bool _configWasModified = false;
public bool HasFirmware => mwvm.ContentManager.GetCurrentFirmwareVersion() != null;
public override async ValueTask Start()
{
RyujinxSetupWizardWindow.IsUsingSetupWizard = true;
Start:
await FirstPage();
Keys:
if (!mwvm.VirtualFileSystem.HasKeySet)
{
Retry:
SetupKeysPageViewModel kpvm = new();
bool result = await NextPage()
.WithTitle(LocaleKeys.SetupWizardKeysPageTitle)
.WithContent<SetupKeysPage>(kpvm)
.Show();
if (!result)
goto Start;
if (!Directory.Exists(kpvm.KeysFolderPath))
goto Retry;
await mwvm.HandleKeysInstallation(kpvm.KeysFolderPath);
}
Firmware:
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
// i know its always false thats the fucking point, its not done
if (!HasFirmware && false)
{
if (!mwvm.VirtualFileSystem.HasKeySet)
goto Keys;
Retry:
SetupKeysPageViewModel kpvm = new();
bool result = await NextPage()
.WithTitle(LocaleKeys.SetupWizardKeysPageTitle)
.WithContent<SetupKeysPage>(kpvm)
.Show();
if (!result)
goto Keys;
if (!Directory.Exists(kpvm.KeysFolderPath))
goto Retry;
await mwvm.HandleKeysInstallation(kpvm.KeysFolderPath);
}
Return:
onClose();
if (_configWasModified)
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
}
}
}

View File

@@ -0,0 +1,20 @@
<windows:StyleableAppWindow 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:markup="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:windows="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
CanResize="False"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.UI.SetupWizard.RyujinxSetupWizardWindow"
x:DataType="setupWizard:RyujinxSetupWizard"
Title="{markup:Locale SetupWizardFirstPageTitle}">
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="Auto,*">
<Grid Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Name="FlushControls">
<controls:RyujinxLogo ToolTip.Tip="{markup:Locale SetupWizardFirstPageTitle}"/>
</Grid>
<ContentPresenter Grid.Row="1" Name="WizardPresenter"/>
</Grid>
</windows:StyleableAppWindow>

View File

@@ -0,0 +1,83 @@
using Ryujinx.Ava;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.SetupWizard;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.IO;
namespace Ryujinx.UI.SetupWizard
{
public partial class RyujinxSetupWizardWindow : StyleableAppWindow
{
public static bool IsUsingSetupWizard { get; set; }
public RyujinxSetupWizardWindow() : base(useCustomTitleBar: true)
{
InitializeComponent();
if (Program.PreviewerDetached)
{
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
}
}
public static RyujinxSetupWizardWindow CreateWindow(MainWindowViewModel mwvm, out RyujinxSetupWizard setupWizard)
{
RyujinxSetupWizardWindow window = new();
window.DataContext = setupWizard = new RyujinxSetupWizard(window.WizardPresenter, mwvm, () =>
{
window.Close();
IsUsingSetupWizard = false;
});
window.Height = 600;
window.Width = 750;
return window;
}
public static bool CanShowSetupWizard =>
!File.Exists(Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard"));
public static bool DisableSetupWizard()
{
if (!CanShowSetupWizard)
return false; //cannot disable; file already doesn't exist, so it's disabled.
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
try
{
File.Create(disableFile, 0).Dispose();
File.SetAttributes(disableFile, File.GetAttributes(disableFile) | FileAttributes.Hidden);
return true;
}
catch (Exception e)
{
Logger.Error?.PrintStack(LogClass.Application, e.Message);
return false;
}
}
public static bool EnableSetupWizard()
{
if (CanShowSetupWizard)
return false; //cannot enable; file already exists, so it's enabled.
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
try
{
File.Delete(disableFile);
return true;
}
catch (Exception e)
{
Logger.Error?.PrintStack(LogClass.Application, e.Message);
return false;
}
}
}
}

View File

@@ -45,6 +45,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Ryujinx.UI.SetupWizard;
using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -870,7 +871,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void RefreshGrid()
{
IObservableList<ApplicationData> appsList = Applications.ToObservableChangeSet()
_ = Applications.ToObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out ReadOnlyObservableCollection<ApplicationData> apps)
@@ -1013,7 +1014,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
private async Task HandleKeysInstallation(string filename)
public async Task HandleKeysInstallation(string filename)
{
try
{

View File

@@ -18,7 +18,6 @@
<viewModels:CompatibilityViewModel />
</window:StyleableAppWindow.DataContext>
<Grid RowDefinitions="Auto,Auto,*">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo

View File

@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL3;
using Ryujinx.UI.SetupWizard;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -138,7 +139,18 @@ namespace Ryujinx.Ava.UI.Windows
Executor.ExecuteBackgroundAsync(async () =>
{
await ShowIntelMacWarningAsync();
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ShowIntelMacWarningAsync();
if (Program.IsFirstStart && RyujinxSetupWizardWindow.CanShowSetupWizard)
{
Task windowTask = ShowAsync(RyujinxSetupWizardWindow.CreateWindow(ViewModel, out var wiz), this);
_ = wiz.Start();
await windowTask;
}
});
if (CommandLineState.FirmwareToInstallPathArg.TryGet(out FilePath fwPath))
{
if (fwPath is { ExistsAsFile: true, Extension: "xci" or "zip" } || fwPath.ExistsAsDirectory)
@@ -150,6 +162,8 @@ namespace Ryujinx.Ava.UI.Windows
else
Logger.Notice.Print(LogClass.UI, "Invalid firmware type provided. Path must be a directory, or a .zip or .xci file.");
}
await CheckLaunchState();
});
}
@@ -399,7 +413,7 @@ namespace Ryujinx.Ava.UI.Windows
}
}
}
else
else if (!RyujinxSetupWizardWindow.IsUsingSetupWizard)
{
ShowKeyErrorOnLoad = false;
@@ -538,8 +552,6 @@ namespace Ryujinx.Ava.UI.Windows
{
LoadApplications();
}
_ = CheckLaunchState();
}
private void SetMainContent(Control content = null)