diff --git a/assets/locales.json b/assets/locales.json
index e12b75be3..351b36e6e 100644
--- a/assets/locales.json
+++ b/assets/locales.json
@@ -25216,6 +25216,81 @@
"zh_CN": "",
"zh_TW": ""
}
+ },
+ {
+ "ID": "SetupWizardFinalPageTitle",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Setup Complete",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SetupWizardFinalPageDescription",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Your installation of Ryubing (aka Ryujinx) has been completed.\n\nIf you require assistance, feel free to join our Discord server and ask for help,\nafter verifying your possession of a modded Nintendo Switch.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SetupWizardFinalPageAction",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Finish",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
}
]
}
diff --git a/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml
new file mode 100644
index 000000000..79f861b2a
--- /dev/null
+++ b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml.cs b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml.cs
new file mode 100644
index 000000000..4f68d22ca
--- /dev/null
+++ b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPage.axaml.cs
@@ -0,0 +1,22 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Common.Helper;
+
+namespace Ryujinx.Ava.UI.SetupWizard.Pages
+{
+ public partial class SetupFinishedPage : RyujinxControl
+ {
+ public SetupFinishedPage()
+ {
+ InitializeComponent();
+ }
+
+ private void Button_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { Tag: string url })
+ OpenHelper.OpenUrl(url);
+ }
+ }
+}
+
diff --git a/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPageContext.cs b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPageContext.cs
new file mode 100644
index 000000000..5506eb772
--- /dev/null
+++ b/src/Ryujinx/UI/SetupWizard/Pages/Final/SetupFinishedPageContext.cs
@@ -0,0 +1,13 @@
+using Gommon;
+using Ryujinx.Ava.Common.Locale;
+
+namespace Ryujinx.Ava.UI.SetupWizard.Pages
+{
+ public class SetupFinishedPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFinalPageTitle)
+ {
+ public override LocaleKeys ActionContent => LocaleKeys.SetupWizardFinalPageAction;
+
+ // informative step; this implementation is not called.
+ public override Result CompleteStep() => Result.Success;
+ }
+}
diff --git a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
index 4d156c6ec..c40e8c02c 100644
--- a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
+++ b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
@@ -35,8 +35,9 @@ namespace Ryujinx.Ava.UI.SetupWizard
}
Retry:
- bool result = await NextPage(out SetupFirmwarePageContext fwContext)
- .Show();
+ bool result =
+ await NextPage(out SetupFirmwarePageContext fwContext)
+ .Show();
if (!result)
return false;
@@ -49,5 +50,10 @@ namespace Ryujinx.Ava.UI.SetupWizard
return true;
}
+
+ private ValueTask Finish()
+ => NextPage(out _)
+ .WithHelpButtonVisible(false)
+ .Show();
}
}
diff --git a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
index 6a00910b3..ccf906bfa 100644
--- a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
+++ b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
@@ -43,7 +43,8 @@ namespace Ryujinx.Ava.UI.SetupWizard
where TContext : SetupWizardPageContext, new()
=> NextPage()
.WithContent(out boundContext)
- .WithTitle(boundContext.Title);
+ .WithTitle(boundContext.Title)
+ .WithActionContent(boundContext.ActionContent);
public void SignalConfigModified()
{
@@ -82,6 +83,9 @@ namespace Ryujinx.Ava.UI.SetupWizard
if (!await SetupFirmware())
goto Keys;
+ if (!await Finish())
+ goto Firmware;
+
Return:
if (_configWasModified)
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
index d99c6fff2..e51eb2a17 100644
--- a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
+++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
@@ -68,7 +68,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
where TControl : RyujinxControl, new()
where TContext : SetupWizardPageContext, new()
{
- boundContext = new() { NotificationManager = ownerWizard.NotificationManager };
+ boundContext = new() { OwningWizard = ownerWizard };
if (boundContext.CreateHelpContent() is { } content)
WithHelpContent(content);
@@ -84,5 +84,11 @@ namespace Ryujinx.Ava.UI.SetupWizard
ActionContent = content;
return this;
}
+
+ public SetupWizardPage WithHelpButtonVisible(bool visible)
+ {
+ ShowHelpButton = visible;
+ return this;
+ }
}
}
diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
index a404612a6..bdea37a01 100644
--- a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
+++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
@@ -28,6 +28,8 @@ namespace Ryujinx.Ava.UI.SetupWizard
[ObservableProperty] public partial bool HasHelpContent { get; set; }
+ [ObservableProperty] public partial bool ShowHelpButton { get; set; } = true;
+
[ObservableProperty]
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];
diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
index d65099bd4..7c7f58122 100644
--- a/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
+++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
@@ -3,23 +3,33 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
+
namespace Ryujinx.Ava.UI.SetupWizard
{
public abstract class SetupWizardPageContext(LocaleKeys title) : BaseModel
{
+ public RyujinxSetupWizard OwningWizard
+ {
+ get;
+ init
+ {
+ field = value;
+ NotificationManager = field.NotificationManager;
+ }
+ }
+
+ public RyujinxNotificationManager NotificationManager { get; private init; }
+
public LocaleKeys Title => title;
-
- public RyujinxNotificationManager NotificationManager { get; init; }
+
+ public virtual LocaleKeys ActionContent => LocaleKeys.SetupWizardActionNext;
// ReSharper disable once UnusedMemberInSuper.Global
// it's used implicitly as we use this type as a where guard for generics for WithContent,
// it also ensures all context types implement completion
public abstract Result CompleteStep();
+
#nullable enable
- public virtual object? CreateHelpContent()
-#nullable disable
- {
- return null;
- }
+ public virtual object? CreateHelpContent() => null;
}
}
diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
index bff0aa5d6..224af0676 100644
--- a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
+++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
@@ -70,8 +70,7 @@
-
+
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
index 863d3e4eb..9729c76da 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
using Gommon;
@@ -31,6 +32,7 @@ namespace Ryujinx.Ava.UI.Views.Main
{
public MainWindow Window { get; private set; }
+
public MainMenuBarView()
{
InitializeComponent();
@@ -51,7 +53,9 @@ namespace Ryujinx.Ava.UI.Views.Main
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
- SetupWizardMenuItem.Command = Commands.Create(() => RyujinxSetupWizardWindow.ShowAsync(overwriteMode: true));
+ SetupWizardMenuItem.Command = Commands.Create(() =>
+ RyujinxSetupWizardWindow.ShowAsync(overwriteMode: !PollShiftPressed())
+ );
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
@@ -64,9 +68,42 @@ namespace Ryujinx.Ava.UI.Views.Main
WindowSize1440PMenuItem.Command =
WindowSize2160PMenuItem.Command = Commands.Create(ChangeWindowSize);
+ KeyDown += OnKeyDown;
+ KeyUp += OnKeyUp;
+
LocaleManager.Instance.LocaleChanged += OnLocaleChanged;
}
+ ///
+ /// KeyUp is not reliably invoked (or invoked at all, seemingly) when a window showing up causes the main menu bar to view,
+ /// as shift is technically raised when that control is no longer the foreground control.
+ ///
+ /// This stores to a temp variable, sets to false (if it is true), then returns the temp variable.
+ ///
+ private bool PollShiftPressed()
+ {
+ bool temp = IsShiftPressed;
+ if (temp)
+ IsShiftPressed = false;
+ return temp;
+ }
+
+ private bool IsShiftPressed { get; set; }
+
+ private void OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key is Key.LeftShift or Key.RightShift && !IsShiftPressed)
+ //down is called even for keys that have been held for a while, aka key repeats.
+ //the check for shift being pressed prevents setting the variable every time the down event is received, if shift is already known to be pressed.
+ IsShiftPressed = true;
+ }
+
+ private void OnKeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key is Key.LeftShift or Key.RightShift)
+ IsShiftPressed = false;
+ }
+
private void OnLocaleChanged()
{
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
@@ -147,11 +184,13 @@ namespace Ryujinx.Ava.UI.Views.Main
}
else
{
- bool customConfigExists = File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
+ bool customConfigExists =
+ File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
if (!ViewModel.IsGameRunning || !customConfigExists)
{
- await Window.SettingsWindow.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
+ await Window.SettingsWindow
+ .ShowDialog(Window); // The game is not running, or if the user configuration does not exist
}
else
{
@@ -175,7 +214,8 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!MiiApplet.CanStart(out ApplicationData appData, out BlitStruct nacpData))
return;
- await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
+ await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen,
+ nacpData);
}
public async Task OpenCheatManagerForCurrentApp()
@@ -183,7 +223,8 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!ViewModel.IsGameRunning)
return;
- string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
+ string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties
+ .Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
await StyleableAppWindow.ShowAsync(
new CheatWindow(
@@ -212,18 +253,24 @@ namespace Ryujinx.Ava.UI.Views.Main
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
if (ViewModel.AreMimeTypesRegistered)
- await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
+ await ContentDialogHelper.CreateInfoDialog(
+ LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty,
+ LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
+ await ContentDialogHelper.CreateErrorDialog(
+ LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
}
private async Task UninstallFileTypes()
{
ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
if (!ViewModel.AreMimeTypesRegistered)
- await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
+ await ContentDialogHelper.CreateInfoDialog(
+ LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty,
+ LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
+ await ContentDialogHelper.CreateErrorDialog(
+ LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
}
private void ChangeWindowSize(string resolution)
@@ -235,7 +282,7 @@ namespace Ryujinx.Ava.UI.Views.Main
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
- (ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
+ (ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);