Overhaul setup wizard help pages

the context can now override a virtual method named `CreateHelpContent` which the setup wizard system will automatically try to use when you use the generic overload taking a generic context type. If the return is null, it skips setting entirely (the default impl is null)

additionally made the discord join link a button with code copied from the about window, and made it centered at the bottom.
This commit is contained in:
GreemDev 2025-11-27 02:11:49 -06:00
parent 1c6652bae1
commit 09d8b4880b
10 changed files with 229 additions and 40 deletions

View file

@ -13,6 +13,12 @@ namespace Ryujinx.Common
public const string SetupGuideWikiUrl = public const string SetupGuideWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide"; "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
public const string DumpKeysWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Keys";
public const string DumpFirmwareWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Firmware";
public const string MultiplayerWikiUrl = public const string MultiplayerWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide"; "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
} }

View file

@ -1,10 +1,13 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; 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.UI.Helpers;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using System; using System;
@ -68,6 +71,32 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
} }
} }
public override Control CreateHelpContent()
{
Grid grid = new()
{
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
HorizontalAlignment = HorizontalAlignment.Center
};
grid.Children.Add(new TextBlock
{
Text = "Not sure how to get your firmware off of your Switch?",
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 0
});
grid.Children.Add(new HyperlinkButton
{
Content = "Click here to view a guide.",
NavigateUri = new Uri(SharedConstants.DumpFirmwareWikiUrl),
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 1
});
return grid;
}
public override Result CompleteStep() public override Result CompleteStep()
{ {
if (!Directory.Exists(FirmwareSourcePath)) if (!Directory.Exists(FirmwareSourcePath))

View file

@ -1,11 +1,14 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; 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.UI.Helpers;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.Exceptions;
@ -20,21 +23,48 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
{ {
public override Result CompleteStep() => public override Result CompleteStep() =>
!Directory.Exists(KeysFolderPath) !Directory.Exists(KeysFolderPath)
? Result.Fail ? Result.Fail
: InstallKeys(KeysFolderPath); : InstallKeys(KeysFolderPath);
[ObservableProperty] public override Control CreateHelpContent()
public partial string KeysFolderPath { get; set; } {
Grid grid = new()
{
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
HorizontalAlignment = HorizontalAlignment.Center
};
grid.Children.Add(new TextBlock
{
Text = "Not sure how to get your keys?",
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 0
});
grid.Children.Add(new HyperlinkButton
{
Content = "Click here to view a guide.",
HorizontalAlignment = HorizontalAlignment.Center,
NavigateUri = new Uri(SharedConstants.DumpKeysWikiUrl),
GridRow = 1
});
return grid;
}
[ObservableProperty] public partial string KeysFolderPath { get; set; }
[RelayCommand] [RelayCommand]
private static async Task Browse(TextBox tb) private static async Task Browse(TextBox tb)
{ {
Optional<IStorageFolder> result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions Optional<IStorageFolder> result =
{ await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle] new FolderPickerOpenOptions
}); {
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle]
});
if (result.TryGet(out IStorageFolder keyFolder)) if (result.TryGet(out IStorageFolder keyFolder))
{ {
tb.Text = keyFolder.TryGetLocalPath(); tb.Text = keyFolder.TryGetLocalPath();
} }

View file

@ -1,14 +1,22 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.SetupWizard.Pages; using Ryujinx.Ava.UI.SetupWizard.Pages;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public class RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode) public class RyujinxSetupWizard : IDisposable, INotifyPropertyChanged
{ {
private bool _configWasModified; private bool _configWasModified;
@ -18,12 +26,12 @@ namespace Ryujinx.Ava.UI.SetupWizard
public async Task Start() public async Task Start()
{ {
NotificationManager = wizardWindow.CreateNotificationManager( NotificationManager = _window.CreateNotificationManager(
// I wanted to do bottom center but that...literally just shows top center? Okay. // I wanted to do bottom center but that...literally just shows top center? Okay.
// Fuck it, weird window height hack to do it instead. // Fuck it, weird window height hack to do it instead.
// 120 is not exact, just a random number. Looks fine though. // 120 is not exact, just a random number. Looks fine though.
NotificationPosition.TopCenter, NotificationPosition.TopCenter,
margin: new Thickness(0, wizardWindow.Height - 120, 0, 0) margin: new Thickness(0, _window.Height - 120, 0, 0)
); );
RyujinxSetupWizardWindow.IsOpen = true; RyujinxSetupWizardWindow.IsOpen = true;
@ -49,13 +57,38 @@ namespace Ryujinx.Ava.UI.SetupWizard
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath); ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
NotificationManager = null; NotificationManager = null;
wizardWindow.Close(); _window.Close();
RyujinxSetupWizardWindow.IsOpen = false; RyujinxSetupWizardWindow.IsOpen = false;
} }
public Bitmap DiscordLogo
{
get;
set => SetField(ref field, value);
}
private void Ryujinx_ThemeChanged()
{
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
}
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
private void UpdateLogoTheme(string theme)
{
bool isDarkTheme = theme == "Dark" ||
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
string themeName = isDarkTheme ? "Dark" : "Light";
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
private async ValueTask<bool> SetupKeys() private async ValueTask<bool> SetupKeys()
{ {
if (overwriteMode || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet) if (_overwrite || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
{ {
Retry: Retry:
bool result = await NextPage() bool result = await NextPage()
@ -75,7 +108,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
private async ValueTask<bool> SetupFirmware() private async ValueTask<bool> SetupFirmware()
{ {
if (overwriteMode || !HasFirmware) if (_overwrite || !HasFirmware)
{ {
if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet) if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
{ {
@ -99,13 +132,54 @@ namespace Ryujinx.Ava.UI.SetupWizard
return true; return true;
} }
private SetupWizardPage FirstPage() => new(wizardWindow.WizardPresenter, this, isFirstPage: true); private SetupWizardPage FirstPage() => new(_window.WizardPresenter, this, isFirstPage: true);
private SetupWizardPage NextPage() => new(wizardWindow.WizardPresenter, this); private SetupWizardPage NextPage() => new(_window.WizardPresenter, this);
public void SignalConfigModified() public void SignalConfigModified()
{ {
_configWasModified = true; _configWasModified = true;
} }
private readonly RyujinxSetupWizardWindow _window;
private readonly bool _overwrite;
public RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode)
{
_window = wizardWindow;
_overwrite = overwriteMode;
if (Program.PreviewerDetached)
{
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle);
RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
}
}
public void Dispose()
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
DiscordLogo.Dispose();
Console.WriteLine("disposed");
GC.SuppressFinalize(this);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
} }
} }

View file

@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Gommon;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
@ -33,7 +34,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
owner owner
); );
_ = wiz.Start(); _ = wiz.Start();
return windowTask; return windowTask.ContinueWith(_ => wiz.Dispose());
} }
public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false) public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false)

View file

@ -1,7 +1,9 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using System;
namespace Ryujinx.Ava.UI.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
@ -34,6 +36,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
public SetupWizardPage WithHelpContent(object? content) public SetupWizardPage WithHelpContent(object? content)
{ {
HelpContent = content; HelpContent = content;
HasHelpContent = content != null;
return this; return this;
} }
@ -44,12 +47,17 @@ namespace Ryujinx.Ava.UI.SetupWizard
return this; return this;
} }
public SetupWizardPage WithContent<TControl, TViewModel>(out TViewModel boundViewModel) public SetupWizardPage WithContent<TControl, TContext>(out TContext boundContext)
where TControl : RyujinxControl<TViewModel>, new() where TControl : RyujinxControl<TContext>, new()
where TViewModel : SetupWizardPageContext, new() where TContext : SetupWizardPageContext, new()
=> WithContent<TControl>( {
boundViewModel = new() { NotificationManager = ownerWizard.NotificationManager } boundContext = new() { NotificationManager = ownerWizard.NotificationManager };
);
if (boundContext.CreateHelpContent() is { } content)
WithHelpContent(content);
return WithContent<TControl>(boundContext);
}
public SetupWizardPage WithActionContent(LocaleKeys content) => public SetupWizardPage WithActionContent(LocaleKeys content) =>
WithActionContent(LocaleManager.Instance[content]); WithActionContent(LocaleManager.Instance[content]);

View file

@ -18,12 +18,16 @@ namespace Ryujinx.Ava.UI.SetupWizard
public bool IsFirstPage => isFirstPage; public bool IsFirstPage => isFirstPage;
public RyujinxSetupWizard Parent => ownerWizard;
[ObservableProperty] public partial string? Title { get; set; } [ObservableProperty] public partial string? Title { get; set; }
[ObservableProperty] public partial object? Content { get; set; } [ObservableProperty] public partial object? Content { get; set; }
[ObservableProperty] public partial object? HelpContent { get; set; } [ObservableProperty] public partial object? HelpContent { get; set; }
[ObservableProperty] public partial bool HasHelpContent { get; set; }
[ObservableProperty] [ObservableProperty]
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext]; public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];

View file

@ -1,3 +1,4 @@
using Avalonia.Controls;
using Gommon; using Gommon;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
@ -9,5 +10,10 @@ namespace Ryujinx.Ava.UI.SetupWizard
public RyujinxNotificationManager NotificationManager { get; init; } public RyujinxNotificationManager NotificationManager { get; init; }
public abstract Result CompleteStep(); public abstract Result CompleteStep();
public virtual Control CreateHelpContent()
{
return null;
}
} }
} }

View file

@ -10,12 +10,11 @@
x:Class="Ryujinx.Ava.UI.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,*">
<TextBlock Grid.Row="0" <TextBlock Grid.Row="0"
TextWrapping="WrapWithOverflow" TextWrapping="WrapWithOverflow"
FontSize="46" FontSize="46"
Text="{Binding Title}" /> Text="{Binding Title}" />
<ContentPresenter Grid.Row="1" <ContentPresenter Grid.Row="1"
Content="{Binding}" Content="{Binding}"
IsVisible="{Binding !#InfoToggle.IsChecked}" IsVisible="{Binding !#InfoToggle.IsChecked}"
@ -28,22 +27,44 @@
</ContentPresenter.DataTemplates> </ContentPresenter.DataTemplates>
</ContentPresenter> </ContentPresenter>
<Grid Grid.Row="2" <Grid Grid.Row="1"
ColumnDefinitions="Auto,*" ColumnDefinitions="*" RowDefinitions="*,Auto"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
IsVisible="{Binding #InfoToggle.IsChecked}"> IsVisible="{Binding #InfoToggle.IsChecked}">
<StackPanel Spacing="5" VerticalAlignment="Top"> <Border
<HyperlinkButton NavigateUri="https://discord.gg/PEuzjrFXUA" Content="Join Discord" /> Margin="15"
</StackPanel> IsVisible="{Binding HasHelpContent}"
<ContentPresenter Content="{Binding}" HorizontalAlignment="Stretch"
Grid.Column="1" VerticalAlignment="Stretch"
Margin="20,0,0,0" ClipToBounds="True"
TextWrapping="WrapWithOverflow"> CornerRadius="5"
<ContentPresenter.DataTemplates> Background="{DynamicResource AppListBackgroundColor}">
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}"> <ContentPresenter Content="{Binding}"
<ContentControl Content="{Binding HelpContent}" /> Margin="5"
</DataTemplate> TextWrapping="WrapWithOverflow" VerticalAlignment="Center" HorizontalAlignment="Center">
</ContentPresenter.DataTemplates> <ContentPresenter.DataTemplates>
</ContentPresenter> <DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
<ContentControl Content="{Binding HelpContent}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
</Border>
<Button Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
MinWidth="45"
MinHeight="32"
MaxWidth="45"
MaxHeight="32"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="5"
Tag="https://discord.gg/PEuzjrFXUA"
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
<Image Source="{Binding Parent.DiscordLogo}" />
</Button>
</Grid> </Grid>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>

View file

@ -1,5 +1,9 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Common.Helper;
namespace Ryujinx.Ava.UI.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage> public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage>
@ -8,6 +12,12 @@ namespace Ryujinx.Ava.UI.SetupWizard
{ {
InitializeComponent(); InitializeComponent();
} }
private void Button_OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: string url })
OpenHelper.OpenUrl(url);
}
} }
} }