diff --git a/src/Ryujinx.Common/SharedConstants.cs b/src/Ryujinx.Common/SharedConstants.cs index 53b6f1350..cb6f1ef36 100644 --- a/src/Ryujinx.Common/SharedConstants.cs +++ b/src/Ryujinx.Common/SharedConstants.cs @@ -13,6 +13,12 @@ namespace Ryujinx.Common public const string SetupGuideWikiUrl = "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 = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide"; } diff --git a/src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs b/src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs index 5d0bea0ee..34c4c492e 100644 --- a/src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs +++ b/src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs @@ -1,10 +1,13 @@ using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Gommon; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Utilities; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.HLE.FileSystem; 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() { if (!Directory.Exists(FirmwareSourcePath)) diff --git a/src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs b/src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs index e063132c6..60f2957e4 100644 --- a/src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs +++ b/src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs @@ -1,11 +1,14 @@ using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using Gommon; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Utilities; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.Exceptions; @@ -20,21 +23,48 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages { public override Result CompleteStep() => !Directory.Exists(KeysFolderPath) - ? Result.Fail + ? Result.Fail : InstallKeys(KeysFolderPath); - [ObservableProperty] - public partial string KeysFolderPath { get; set; } + 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 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] private static async Task Browse(TextBox tb) { - Optional result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions - { - Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle] - }); + Optional result = + await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync( + new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle] + }); - if (result.TryGet(out IStorageFolder keyFolder)) + if (result.TryGet(out IStorageFolder keyFolder)) { tb.Text = keyFolder.TryGetLocalPath(); } diff --git a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs index 57b7bb66b..59e189de3 100644 --- a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs +++ b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs @@ -1,14 +1,22 @@ using Avalonia; using Avalonia.Controls.Notifications; +using Avalonia.Media.Imaging; +using Avalonia.Styling; +using Avalonia.Threading; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.SetupWizard.Pages; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.SetupWizard { - public class RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode) + public class RyujinxSetupWizard : IDisposable, INotifyPropertyChanged { private bool _configWasModified; @@ -18,12 +26,12 @@ namespace Ryujinx.Ava.UI.SetupWizard public async Task Start() { - NotificationManager = wizardWindow.CreateNotificationManager( + NotificationManager = _window.CreateNotificationManager( // I wanted to do bottom center but that...literally just shows top center? Okay. // Fuck it, weird window height hack to do it instead. // 120 is not exact, just a random number. Looks fine though. NotificationPosition.TopCenter, - margin: new Thickness(0, wizardWindow.Height - 120, 0, 0) + margin: new Thickness(0, _window.Height - 120, 0, 0) ); RyujinxSetupWizardWindow.IsOpen = true; @@ -49,13 +57,38 @@ namespace Ryujinx.Ava.UI.SetupWizard ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath); NotificationManager = null; - wizardWindow.Close(); + _window.Close(); 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 SetupKeys() { - if (overwriteMode || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet) + if (_overwrite || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet) { Retry: bool result = await NextPage() @@ -75,7 +108,7 @@ namespace Ryujinx.Ava.UI.SetupWizard private async ValueTask SetupFirmware() { - if (overwriteMode || !HasFirmware) + if (_overwrite || !HasFirmware) { if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet) { @@ -99,13 +132,54 @@ namespace Ryujinx.Ava.UI.SetupWizard 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() { _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(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } } } diff --git a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs index 90fc37fed..451d9fa8f 100644 --- a/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs +++ b/src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Gommon; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; @@ -33,7 +34,7 @@ namespace Ryujinx.Ava.UI.SetupWizard owner ); _ = wiz.Start(); - return windowTask; + return windowTask.ContinueWith(_ => wiz.Dispose()); } public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false) diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs index 033170091..6b3a09fa1 100644 --- a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs +++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs @@ -1,7 +1,9 @@ using Avalonia; using Avalonia.Controls; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Controls; +using System; namespace Ryujinx.Ava.UI.SetupWizard { @@ -34,6 +36,7 @@ namespace Ryujinx.Ava.UI.SetupWizard public SetupWizardPage WithHelpContent(object? content) { HelpContent = content; + HasHelpContent = content != null; return this; } @@ -44,12 +47,17 @@ namespace Ryujinx.Ava.UI.SetupWizard return this; } - public SetupWizardPage WithContent(out TViewModel boundViewModel) - where TControl : RyujinxControl, new() - where TViewModel : SetupWizardPageContext, new() - => WithContent( - boundViewModel = new() { NotificationManager = ownerWizard.NotificationManager } - ); + public SetupWizardPage WithContent(out TContext boundContext) + where TControl : RyujinxControl, new() + where TContext : SetupWizardPageContext, new() + { + boundContext = new() { NotificationManager = ownerWizard.NotificationManager }; + + if (boundContext.CreateHelpContent() is { } content) + WithHelpContent(content); + + return WithContent(boundContext); + } public SetupWizardPage WithActionContent(LocaleKeys content) => WithActionContent(LocaleManager.Instance[content]); diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs index 2380115f9..a404612a6 100644 --- a/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs +++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs @@ -18,12 +18,16 @@ namespace Ryujinx.Ava.UI.SetupWizard public bool IsFirstPage => isFirstPage; + public RyujinxSetupWizard Parent => ownerWizard; + [ObservableProperty] public partial string? Title { get; set; } [ObservableProperty] public partial object? Content { get; set; } [ObservableProperty] public partial object? HelpContent { get; set; } + [ObservableProperty] public partial bool HasHelpContent { get; set; } + [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 93cf9c4c0..147f995bf 100644 --- a/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs +++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls; using Gommon; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; @@ -9,5 +10,10 @@ namespace Ryujinx.Ava.UI.SetupWizard public RyujinxNotificationManager NotificationManager { get; init; } public abstract Result CompleteStep(); + + public virtual Control CreateHelpContent() + { + return null; + } } } diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml index acb120018..bff0aa5d6 100644 --- a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml +++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml @@ -10,12 +10,11 @@ x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView"> - + - - - - - - - - - - - - + + + + + + + + + + diff --git a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs index b7d32d675..dd9ed93a2 100644 --- a/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs +++ b/src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs @@ -1,5 +1,9 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; using Ryujinx.Ava.UI.Controls; +using Ryujinx.Common.Helper; + namespace Ryujinx.Ava.UI.SetupWizard { public partial class SetupWizardPageView : RyujinxControl @@ -8,6 +12,12 @@ namespace Ryujinx.Ava.UI.SetupWizard { InitializeComponent(); } + + private void Button_OnClick(object sender, RoutedEventArgs e) + { + if (sender is Button { Tag: string url }) + OpenHelper.OpenUrl(url); + } } }