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);