From 60934077fe587ec3151390444d822480b822ebe3 Mon Sep 17 00:00:00 2001 From: altpyrion <294315026+altpyrion@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:21:27 +0200 Subject: [PATCH 1/3] refactor of installation locators and rough setup popup implementation --- .../Abstractions/DolphinInstallation.cs | 3 + .../Abstractions/IDolphinInstaller.cs | 7 + .../Abstractions/IDolphinLocator.cs | 6 + .../DolphinManagmentExtensions.cs | 18 ++ .../Linux/LinuxCommandEnvironment.cs | 22 ++ .../Linux/LinuxDolphinInstaller.cs | 0 .../Linux/LinuxDolphinLocator.cs | 33 +++ .../Linux/LinuxProcessService.cs | 123 +++++++++++ .../Services/Storage/FilePickerHelper.cs | 2 +- WheelWizard/SetupExtensions.cs | 3 +- WheelWizard/Views/App.axaml.cs | 12 ++ .../Popups/Generic/FirstTimeSetupPopup.axaml | 86 ++++++++ .../Generic/FirstTimeSetupPopup.axaml.cs | 200 ++++++++++++++++++ build-linux.bat | 0 14 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs create mode 100644 WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs create mode 100644 WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs create mode 100644 WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs create mode 100644 WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs create mode 100644 WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs create mode 100644 WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs create mode 100644 WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs create mode 100644 WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml create mode 100644 WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs mode change 100644 => 100755 build-linux.bat diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs b/WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs new file mode 100644 index 00000000..6f0050c9 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs @@ -0,0 +1,3 @@ +namespace WheelWizard.DolphinManagent.Abstractions; + +public record DolphinInstallation(string DisplayName, string LaunchTarget, bool Found); diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs b/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs new file mode 100644 index 00000000..8800612e --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs @@ -0,0 +1,7 @@ +namespace WheelWizard.DolphinManagent.Abstractions; + +public interface IDolphinInstaller +{ + IReadOnlyList AvailableInstallationMethods(); + //bool InstallDolphin(DolphinInstallation method); +} diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs b/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs new file mode 100644 index 00000000..9abfd6c5 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs @@ -0,0 +1,6 @@ +namespace WheelWizard.DolphinManagent.Abstractions; + +public interface IDolphinLocator +{ + IReadOnlyList DetectInstallations(); +} diff --git a/WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs b/WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs new file mode 100644 index 00000000..ec77cb56 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs @@ -0,0 +1,18 @@ +using WheelWizard.DolphinManagent.Abstractions; +using WheelWizard.DolphinManagent.Linux; + +namespace WheelWizard.DolphinManagent; + +public static class DolphinManagmentExtensions +{ + public static IServiceCollection AddDolphinManagement(this IServiceCollection services) + { +#if LINUX + services.AddSingleton(); + services.AddSingleton(); + //services.AddSingleton(); + services.AddSingleton(); +#endif + return services; + } +} diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs b/WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs new file mode 100644 index 00000000..570bc419 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs @@ -0,0 +1,22 @@ +using WheelWizard.Helpers; + +namespace WheelWizard.DolphinManagent.Linux; + +public interface ILinuxCommandEnvironment +{ + bool IsCommandAvailable(string command); + string DetectPackageManagerInstallCommand(); +} + +public sealed class LinuxCommandEnvironment : ILinuxCommandEnvironment +{ + public bool IsCommandAvailable(string command) + { + return EnvHelper.IsValidUnixCommand(command); + } + + public string DetectPackageManagerInstallCommand() + { + return EnvHelper.DetectLinuxPackageManagerInstallCommand(); + } +} diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs new file mode 100644 index 00000000..e69de29b diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs b/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs new file mode 100644 index 00000000..ddfedb54 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs @@ -0,0 +1,33 @@ +using WheelWizard.DolphinManagent.Abstractions; + +namespace WheelWizard.DolphinManagent.Linux; + +public sealed class LinuxDolphinLocator(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService) : IDolphinLocator +{ + private bool IsDolphinInstalledInFlatpak() + { + const string dolphinAppId = "org.DolphinEmu.dolphin-emu"; + var processResult = processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _); + + return processResult.IsSuccess && processResult.Value == 0 && stdOut.Split('\n').Any(line => line == dolphinAppId); + } + + private bool IsDolphinInstalledNative() + { + if (!commandEnvironment.IsCommandAvailable("dolphin-emu")) + { + return false; + } + var processResult = processService.Run("dolphin-emu", "--version"); + return processResult.IsSuccess && processResult.Value == 0; + } + + public IReadOnlyList DetectInstallations() + { + return + [ + new("Flatpak", "flatpak run org.DolphinEmu.dolphin-emu", IsDolphinInstalledInFlatpak()), + new("Native", "dolphin-emu", IsDolphinInstalledNative()), + ]; + } +} diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs b/WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs new file mode 100644 index 00000000..e0a2cba3 --- /dev/null +++ b/WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs @@ -0,0 +1,123 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace WheelWizard.DolphinManagent.Linux; + +public interface ILinuxProcessService +{ + OperationResult Run(string fileName, string arguments, out string stdOut, out string stdErr); + OperationResult Run(string fileName, string arguments); + Task> RunWithProgressAsync(string fileName, string arguments, IProgress? progress = null); + Task LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration); +} + +public sealed class LinuxProcessService : ILinuxProcessService +{ + public OperationResult Run(string fileName, string arguments, out string stdOut, out string stdErr) + { + var localStdOut = ""; + var localStdErr = ""; + var result = TryCatch( + () => + { + var processInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(processInfo); + if (process == null) + return -1; + + localStdOut = process.StandardOutput.ReadToEnd(); + localStdErr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + return process.ExitCode; + }, + $"Failed to run process: {fileName} {arguments}" + ); + + stdOut = localStdOut; + stdErr = localStdErr; + return result; + } + + public OperationResult Run(string fileName, string arguments) + { + return Run(fileName, arguments, out _, out _); + } + + public async Task> RunWithProgressAsync(string fileName, string arguments, IProgress? progress = null) + { + return await TryCatch( + async () => + { + var processInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(processInfo); + if (process == null) + return -1; + + process.OutputDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress); + process.ErrorDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync(); + return process.ExitCode; + }, + $"Failed to run process: {fileName} {arguments}" + ); + } + + public async Task LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration) + { + return await TryCatch( + async () => + { + using var process = new Process + { + StartInfo = new() + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + + process.Start(); + await Task.Delay(duration); + + if (!process.HasExited) + process.Kill(); + }, + $"Failed to run process: {fileName} {arguments}" + ); + } + + private static void ReportProgress(string? output, IProgress? progress) + { + if (string.IsNullOrWhiteSpace(output)) + return; + + var match = Regex.Match(output, @"(\d+)%"); + if (match.Success && int.TryParse(match.Groups[1].Value, out var percent)) + progress?.Report(percent); + } +} diff --git a/WheelWizard/Services/Storage/FilePickerHelper.cs b/WheelWizard/Services/Storage/FilePickerHelper.cs index e01a8abd..4b6270c2 100644 --- a/WheelWizard/Services/Storage/FilePickerHelper.cs +++ b/WheelWizard/Services/Storage/FilePickerHelper.cs @@ -47,7 +47,7 @@ public static async Task> OpenFilePickerAsync( if (storageProvider == null) return null; - var topLevel = TopLevel.GetTopLevel(storageProvider.MainWindow); + var topLevel = TopLevel.GetTopLevel(storageProvider.MainWindow); // Makes file picker popup not work when called from popup if (topLevel?.StorageProvider == null) return null; diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index ec4de64c..d9de9393 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -7,6 +7,7 @@ using WheelWizard.CustomCharacters; using WheelWizard.CustomDistributions; using WheelWizard.DolphinInstaller; +using WheelWizard.DolphinManagent; using WheelWizard.Features.Archives; using WheelWizard.Features.Patches; using WheelWizard.GameBanana; @@ -33,7 +34,7 @@ public static class SetupExtensions public static void AddWheelWizardServices(this IServiceCollection services) { // Features - services.AddDolphinInstaller(); + services.AddDolphinManagement(); services.AddLocalization(); services.AddSettings(); services.AddCustomCharacters(); diff --git a/WheelWizard/Views/App.axaml.cs b/WheelWizard/Views/App.axaml.cs index 399fa5a2..7f2ab655 100644 --- a/WheelWizard/Views/App.axaml.cs +++ b/WheelWizard/Views/App.axaml.cs @@ -197,6 +197,18 @@ private async Task InitializeDesktopAsync(IClassicDesktopStyleApplicationLifetim { try { + var settingsManager = Services.GetRequiredService(); + if (!settingsManager.PathsSetupCorrectly()) + { + var firstTimeSetup = new FirstTimeSetupPopup(); + var setupCompleted = await firstTimeSetup.ShowAndAwaitCompletionAsync(); + if (!setupCompleted) + { + desktop.Shutdown(); + return; + } + } + var resourceInstaller = Services.GetRequiredService(); if (resourceInstaller.GetResolvedResourcePath().IsFailure) { diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml new file mode 100644 index 00000000..ac8ffcb8 --- /dev/null +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs new file mode 100644 index 00000000..8129743f --- /dev/null +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs @@ -0,0 +1,200 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform.Storage; +using Microsoft.Extensions.Logging; +using WheelWizard.DolphinManagent.Abstractions; +using WheelWizard.Services; +using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Views.Popups.Base; + +namespace WheelWizard.Views.Popups.Generic; + +public partial class FirstTimeSetupPopup : PopupContent +{ + private sealed record DolphinCandidate(string DisplayName, string? Path, bool Found); + + private readonly TaskCompletionSource _completionSource = new(); + private bool _setupCompleted; + + private string? _selectedDolphinTarget; + + [Inject] + private ILogger Logger { get; set; } = null!; + + [Inject] + private IDolphinLocator DolphinLocator { get; set; } = null!; + + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + + public FirstTimeSetupPopup() + : base(true, false, true, "Wheel Wizard") + { + InitializeComponent(); + + PlatformTextBlock.Text = $"Detected platform: {GetPlatformName()}"; + PopulateDetectedLocations(); + } + + public Task ShowAndAwaitCompletionAsync() + { + Show(); + return _completionSource.Task; + } + + private static string GetPlatformName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "Windows"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "macOS"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return "Linux"; + return "Unknown"; + } + + private void PopulateDetectedLocations() + { + DetectedLocationsPanel.Children.Clear(); + + var candidates = DolphinLocator.DetectInstallations(); + if (candidates.Count == 0) + { + DetectedLocationsPanel.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + Opacity = 0.75, + TextWrapping = TextWrapping.Wrap, + Text = "No Dolphin installations were detected automatically. Please select one manually below.", + } + ); + return; + } + + foreach (var candidate in candidates) + DetectedLocationsPanel.Children.Add(CreateCandidateRow(candidate)); + } + + private RadioButton CreateCandidateRow(DolphinInstallation candidate) + { + var subtitle = candidate.Found ? candidate.LaunchTarget ?? string.Empty : "Not found on this system"; + + var content = new StackPanel { Spacing = 2 }; + content.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + FontWeight = FontWeight.SemiBold, + Text = candidate.DisplayName, + } + ); + content.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + Text = subtitle, + } + ); + + var radio = new RadioButton + { + GroupName = "DolphinLocation", + Content = content, + IsEnabled = candidate.Found, + Tag = candidate.LaunchTarget, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }; + radio.IsCheckedChanged += DetectedLocation_OnChecked; + return radio; + } + + private void DetectedLocation_OnChecked(object? sender, RoutedEventArgs e) + { + if (sender is not RadioButton { IsChecked: true } radio) + return; + + ManualPathTextBox.Text = string.Empty; + SetSelectedTarget(radio.Tag as string); + } + + private async void BrowseButton_OnClick(object? sender, RoutedEventArgs e) + { + try + { + var path = await OpenDolphinFilePickerAsync(); + Logger.LogInformation(path); + if (string.IsNullOrWhiteSpace(path)) + return; + + // A manual pick wins: clear any detected radio selection. + foreach (var child in DetectedLocationsPanel.Children) + { + if (child is RadioButton radio) + radio.IsChecked = false; + } + + ManualPathTextBox.Text = path; + SetSelectedTarget(path); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to pick a Dolphin location."); + ShowError("Something went wrong while selecting the file."); + } + } + + private void SetSelectedTarget(string? target) + { + _selectedDolphinTarget = string.IsNullOrWhiteSpace(target) ? null : target; + ContinueButton.IsEnabled = _selectedDolphinTarget != null; + ErrorTextBlock.IsVisible = false; + } + + private void ContinueButton_OnClick(object? sender, RoutedEventArgs e) + { + if (string.IsNullOrWhiteSpace(_selectedDolphinTarget)) + { + ShowError("Please select a Dolphin installation to continue."); + return; + } + + SettingsService.Set(SettingsService.DOLPHIN_LOCATION, _selectedDolphinTarget); + + _setupCompleted = true; + _completionSource.TrySetResult(true); + Close(); + } + + private void CloseButton_OnClick(object? sender, RoutedEventArgs e) => Close(); + + protected override void BeforeClose() => _completionSource.TrySetResult(_setupCompleted); + + private void ShowError(string message) + { + ErrorTextBlock.Text = message; + ErrorTextBlock.IsVisible = true; + } + + private static async Task OpenDolphinFilePickerAsync() + { + var executableFileType = new FilePickerFileType("Executable files") + { + Patterns = Environment.OSVersion.Platform switch + { + PlatformID.Win32NT => new[] { "*.exe" }, + PlatformID.Unix => new[] { "*", "*.sh" }, + PlatformID.MacOSX => new[] { "*", "*.app" }, + _ => new[] { "*" }, // Fallback + }, + }; + var filePath = await FilePickerHelper.OpenSingleFileAsync("Select the Dolphin executable", [executableFileType]); + return filePath; + } +} diff --git a/build-linux.bat b/build-linux.bat old mode 100644 new mode 100755 From 2498aaf85a2fee0cc7ace8508285c87efcfe2eb5 Mon Sep 17 00:00:00 2001 From: altpyrion <294315026+altpyrion@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:08:08 +0200 Subject: [PATCH 2/3] Added dolphin flatpak install button - Fixed typo - Fixed browse button on popup --- .../Features/LinuxDolphinInstallerTests.cs | 10 ++- .../DolphinInstaller/LinuxDolphinInstaller.cs | 4 +- .../Abstractions/DolphinInstallation.cs | 2 +- .../Abstractions/IDolphinInstaller.cs | 7 ++ .../Abstractions/IDolphinLocator.cs | 2 +- .../DolphinManagmentExtensions.cs | 8 +- .../Linux/LinuxCommandEnvironment.cs | 2 +- .../Linux/LinuxDolphinInstaller.cs | 81 +++++++++++++++++++ .../Linux/LinuxDolphinLocator.cs | 4 +- .../Linux/LinuxProcessService.cs | 2 +- .../Abstractions/IDolphinInstaller.cs | 7 -- .../Linux/LinuxDolphinInstaller.cs | 0 .../Features/Settings/SettingsManager.cs | 11 ++- .../Services/Storage/FilePickerHelper.cs | 52 ++++++------ WheelWizard/SetupExtensions.cs | 2 +- .../Generic/FirstTimeSetupPopup.axaml.cs | 80 +++++++++++++++--- 16 files changed, 209 insertions(+), 65 deletions(-) rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/Abstractions/DolphinInstallation.cs (62%) create mode 100644 WheelWizard/Features/DolphinManagement/Abstractions/IDolphinInstaller.cs rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/Abstractions/IDolphinLocator.cs (64%) rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/DolphinManagmentExtensions.cs (67%) rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/Linux/LinuxCommandEnvironment.cs (91%) create mode 100644 WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinInstaller.cs rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/Linux/LinuxDolphinLocator.cs (92%) rename WheelWizard/Features/{DolphinManagent => DolphinManagement}/Linux/LinuxProcessService.cs (98%) delete mode 100644 WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs delete mode 100644 WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs diff --git a/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs index fb95aa3b..0c28325e 100644 --- a/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs +++ b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs @@ -19,7 +19,10 @@ public LinuxDolphinInstallerTests() [Fact] public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakListHasDolphin() { - _processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\norg.DolphinEmu.dolphin-emu\n"); + _processService + .Run("flatpak", "list --app --columns=application", out var stdOut, out _) + .Returns(Ok(0)) + .AndDoes(callInfo => callInfo[2] = "Application ID\norg.DolphinEmu.dolphin-emu\n"); var result = _installer.IsDolphinInstalledInFlatpak(); @@ -29,7 +32,10 @@ public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakListHasDolphin() [Fact] public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakListHasNoDolphin() { - _processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\n"); + _processService + .Run("flatpak", "list --app --columns=application", out var stdOut, out _) + .Returns(Ok(0)) + .AndDoes(callInfo => callInfo[2] = "Application ID\n"); var result = _installer.IsDolphinInstalledInFlatpak(); diff --git a/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs index 46ea14ee..61fef12b 100644 --- a/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs +++ b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs @@ -17,9 +17,7 @@ public bool IsDolphinInstalledInFlatpak() const string dolphinAppId = "org.DolphinEmu.dolphin-emu"; var processResult = processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _); - return processResult.IsSuccess && processResult.Value == 0 && stdOut - .Split('\n') - .Any(line => line == dolphinAppId); + return processResult.IsSuccess && processResult.Value == 0 && stdOut.Split('\n').Any(line => line == dolphinAppId); } public bool IsDolphinInstalledNative() diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs b/WheelWizard/Features/DolphinManagement/Abstractions/DolphinInstallation.cs similarity index 62% rename from WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs rename to WheelWizard/Features/DolphinManagement/Abstractions/DolphinInstallation.cs index 6f0050c9..0bfae423 100644 --- a/WheelWizard/Features/DolphinManagent/Abstractions/DolphinInstallation.cs +++ b/WheelWizard/Features/DolphinManagement/Abstractions/DolphinInstallation.cs @@ -1,3 +1,3 @@ -namespace WheelWizard.DolphinManagent.Abstractions; +namespace WheelWizard.DolphinManagement.Abstractions; public record DolphinInstallation(string DisplayName, string LaunchTarget, bool Found); diff --git a/WheelWizard/Features/DolphinManagement/Abstractions/IDolphinInstaller.cs b/WheelWizard/Features/DolphinManagement/Abstractions/IDolphinInstaller.cs new file mode 100644 index 00000000..c0312af8 --- /dev/null +++ b/WheelWizard/Features/DolphinManagement/Abstractions/IDolphinInstaller.cs @@ -0,0 +1,7 @@ +namespace WheelWizard.DolphinManagement.Abstractions; + +public interface IDolphinInstaller +{ + //IReadOnlyList AvailableInstallationMethods(); + Task InstallDolphin(IProgress? progress); +} diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs b/WheelWizard/Features/DolphinManagement/Abstractions/IDolphinLocator.cs similarity index 64% rename from WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs rename to WheelWizard/Features/DolphinManagement/Abstractions/IDolphinLocator.cs index 9abfd6c5..d451daf8 100644 --- a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinLocator.cs +++ b/WheelWizard/Features/DolphinManagement/Abstractions/IDolphinLocator.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.DolphinManagent.Abstractions; +namespace WheelWizard.DolphinManagement.Abstractions; public interface IDolphinLocator { diff --git a/WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs b/WheelWizard/Features/DolphinManagement/DolphinManagmentExtensions.cs similarity index 67% rename from WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs rename to WheelWizard/Features/DolphinManagement/DolphinManagmentExtensions.cs index ec77cb56..c0bd18b9 100644 --- a/WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs +++ b/WheelWizard/Features/DolphinManagement/DolphinManagmentExtensions.cs @@ -1,7 +1,7 @@ -using WheelWizard.DolphinManagent.Abstractions; -using WheelWizard.DolphinManagent.Linux; +using WheelWizard.DolphinManagement.Abstractions; +using WheelWizard.DolphinManagement.Linux; -namespace WheelWizard.DolphinManagent; +namespace WheelWizard.DolphinManagement; public static class DolphinManagmentExtensions { @@ -10,7 +10,7 @@ public static IServiceCollection AddDolphinManagement(this IServiceCollection se #if LINUX services.AddSingleton(); services.AddSingleton(); - //services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); #endif return services; diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs b/WheelWizard/Features/DolphinManagement/Linux/LinuxCommandEnvironment.cs similarity index 91% rename from WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs rename to WheelWizard/Features/DolphinManagement/Linux/LinuxCommandEnvironment.cs index 570bc419..f1441cf7 100644 --- a/WheelWizard/Features/DolphinManagent/Linux/LinuxCommandEnvironment.cs +++ b/WheelWizard/Features/DolphinManagement/Linux/LinuxCommandEnvironment.cs @@ -1,6 +1,6 @@ using WheelWizard.Helpers; -namespace WheelWizard.DolphinManagent.Linux; +namespace WheelWizard.DolphinManagement.Linux; public interface ILinuxCommandEnvironment { diff --git a/WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinInstaller.cs new file mode 100644 index 00000000..01a3e8a0 --- /dev/null +++ b/WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinInstaller.cs @@ -0,0 +1,81 @@ +using WheelWizard.DolphinManagement.Abstractions; + +namespace WheelWizard.DolphinManagement.Linux; + +public sealed class LinuxDolphinInstaller(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService) + : IDolphinInstaller +{ + private bool IsFlatpakInstalled() + { + return commandEnvironment.IsCommandAvailable("flatpak"); + } + + private async Task InstallFlatpak(IProgress? progress = null) + { + if (IsFlatpakInstalled()) + return Ok(); + + var packageManagerCommand = commandEnvironment.DetectPackageManagerInstallCommand(); + if (string.IsNullOrWhiteSpace(packageManagerCommand)) + return Fail("Unsupported Linux distribution. Could not detect a package manager command."); + + var installResult = await processService.RunWithProgressAsync("pkexec", $"{packageManagerCommand} flatpak", progress); + if (installResult.IsFailure) + return installResult.Error; + + if (installResult.Value is 126 or 127) + return Fail("You need to be an administrator to install Flatpak."); + + if (installResult.Value != 0) + return Fail($"Flatpak installation failed with exit code {installResult.Value}."); + + if (!IsFlatpakInstalled()) + return Fail("Flatpak installation completed, but Flatpak is still unavailable."); + + return Ok(); + } + + public async Task InstallDolphin(IProgress? progress = null) + { + if (!IsFlatpakInstalled()) + { + var installFlatpakResult = await InstallFlatpak(progress); + if (installFlatpakResult.IsFailure) + return installFlatpakResult; + } + + var addRemoteResult = processService.Run( + "flatpak", + "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo" + ); + if (addRemoteResult.IsFailure) + return addRemoteResult.Error; + + if (addRemoteResult.Value != 0) + return Fail($"Adding the Dolphin Flatpak remote failed with exit code {addRemoteResult.Value}."); + + addRemoteResult = processService.Run( + "flatpak", + "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo" + ); + if (addRemoteResult.IsFailure) + return addRemoteResult.Error; + + if (addRemoteResult.Value != 0) + return Fail($"Adding the Flathub Flatpak remote failed with exit code {addRemoteResult.Value}."); + + var installDolphinResult = await processService.RunWithProgressAsync( + "flatpak", + "install --user -y dolphin org.DolphinEmu.dolphin-emu", + progress + ); + if (installDolphinResult.IsFailure) + return installDolphinResult.Error; + + if (installDolphinResult.Value != 0) + return Fail($"Dolphin installation failed with exit code {installDolphinResult.Value}."); + + var launchResult = await processService.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4)); + return launchResult.IsFailure ? launchResult.Error : Ok(); + } +} diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs b/WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinLocator.cs similarity index 92% rename from WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs rename to WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinLocator.cs index ddfedb54..44b47e26 100644 --- a/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs +++ b/WheelWizard/Features/DolphinManagement/Linux/LinuxDolphinLocator.cs @@ -1,6 +1,6 @@ -using WheelWizard.DolphinManagent.Abstractions; +using WheelWizard.DolphinManagement.Abstractions; -namespace WheelWizard.DolphinManagent.Linux; +namespace WheelWizard.DolphinManagement.Linux; public sealed class LinuxDolphinLocator(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService) : IDolphinLocator { diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs b/WheelWizard/Features/DolphinManagement/Linux/LinuxProcessService.cs similarity index 98% rename from WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs rename to WheelWizard/Features/DolphinManagement/Linux/LinuxProcessService.cs index e0a2cba3..746e8cc1 100644 --- a/WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs +++ b/WheelWizard/Features/DolphinManagement/Linux/LinuxProcessService.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Text.RegularExpressions; -namespace WheelWizard.DolphinManagent.Linux; +namespace WheelWizard.DolphinManagement.Linux; public interface ILinuxProcessService { diff --git a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs b/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs deleted file mode 100644 index 8800612e..00000000 --- a/WheelWizard/Features/DolphinManagent/Abstractions/IDolphinInstaller.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace WheelWizard.DolphinManagent.Abstractions; - -public interface IDolphinInstaller -{ - IReadOnlyList AvailableInstallationMethods(); - //bool InstallDolphin(DolphinInstallation method); -} diff --git a/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinInstaller.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 7f2f929a..91660837 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -23,11 +23,7 @@ public class SettingsManager : ISettingsManager private double _internalScale = -1.0; #region Constructor - public SettingsManager( - IWhWzSettingManager whWzSettingManager, - IDolphinSettingManager dolphinSettingManager, - IFileSystem fileSystem - ) + public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager, IFileSystem fileSystem) { _whWzSettingManager = whWzSettingManager; _dolphinSettingManager = dolphinSettingManager; @@ -90,7 +86,10 @@ IFileSystem fileSystem return false; // `~/.dolphin-emu` would be used if it exists - if (!PathManager.IsFlatpakDolphinFilePath(dolphinLocation) && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath)) + if ( + !PathManager.IsFlatpakDolphinFilePath(dolphinLocation) + && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath) + ) return false; return true; diff --git a/WheelWizard/Services/Storage/FilePickerHelper.cs b/WheelWizard/Services/Storage/FilePickerHelper.cs index 4b6270c2..8e6ea7e8 100644 --- a/WheelWizard/Services/Storage/FilePickerHelper.cs +++ b/WheelWizard/Services/Storage/FilePickerHelper.cs @@ -9,6 +9,18 @@ namespace WheelWizard.Services; public static class FilePickerHelper { + private static TopLevel? ResolveTopLevel(Visual? owner) + { + if (owner != null && TopLevel.GetTopLevel(owner) is { } ownerTopLevel) + return ownerTopLevel; + + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) + return null; + + // During first time setup there is no main window yet, so fall back to whatever window is currently open + return desktop.MainWindow ?? desktop.Windows.FirstOrDefault(window => window.IsActive) ?? desktop.Windows.FirstOrDefault(); + } + /// /// Opens a file picker with the specified options. /// @@ -19,13 +31,12 @@ public static class FilePickerHelper public static async Task> OpenFilePickerAsync( FilePickerFileType fileType, bool allowMultiple = true, - string title = "Select Files" + string title = "Select Files", + Visual? owner = null ) { - var storageProvider = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (storageProvider == null) - return []; - if (storageProvider.MainWindow?.StorageProvider == null) + var topLevel = ResolveTopLevel(owner); + if (topLevel?.StorageProvider == null) return []; var options = new FilePickerOpenOptions @@ -35,19 +46,15 @@ public static async Task> OpenFilePickerAsync( FileTypeFilter = new List { fileType }, }; - var selectedFiles = await storageProvider.MainWindow.StorageProvider.OpenFilePickerAsync(options); + var selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(options); return selectedFiles?.Select(TryResolveLocalPath).Where(path => !string.IsNullOrWhiteSpace(path)).Select(path => path!).ToList() ?? []; } - public static async Task OpenSingleFileAsync(string title, IEnumerable fileTypes) + public static async Task OpenSingleFileAsync(string title, IEnumerable fileTypes, Visual? owner = null) { - var storageProvider = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (storageProvider == null) - return null; - - var topLevel = TopLevel.GetTopLevel(storageProvider.MainWindow); // Makes file picker popup not work when called from popup + var topLevel = ResolveTopLevel(owner); if (topLevel?.StorageProvider == null) return null; @@ -73,13 +80,13 @@ public static async Task> OpenFilePickerAsync( return null; } - public static async Task> SelectFolderAsync(string title, IStorageFolder? suggestedStartLocation = null) + public static async Task> SelectFolderAsync( + string title, + IStorageFolder? suggestedStartLocation = null, + Visual? owner = null + ) { - var storageProvider = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (storageProvider == null) - return []; - - var topLevel = TopLevel.GetTopLevel(storageProvider.MainWindow); + var topLevel = ResolveTopLevel(owner); if (topLevel?.StorageProvider == null) return []; @@ -128,14 +135,11 @@ public static void OpenFolderInFileManager(string folderPath) string title, IEnumerable fileTypes, string defaultFileName = "untitled", - IStorageFolder? suggestedStartLocation = null + IStorageFolder? suggestedStartLocation = null, + Visual? owner = null ) { - var storageProvider = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (storageProvider == null) - return null; - - var topLevel = TopLevel.GetTopLevel(storageProvider.MainWindow); + var topLevel = ResolveTopLevel(owner); if (topLevel?.StorageProvider == null) return null; diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index d9de9393..fc52ec5e 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -7,7 +7,7 @@ using WheelWizard.CustomCharacters; using WheelWizard.CustomDistributions; using WheelWizard.DolphinInstaller; -using WheelWizard.DolphinManagent; +using WheelWizard.DolphinManagement; using WheelWizard.Features.Archives; using WheelWizard.Features.Patches; using WheelWizard.GameBanana; diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs index 8129743f..57a7a3bf 100644 --- a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs @@ -1,11 +1,13 @@ using System.Runtime.InteropServices; +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform.Storage; using Microsoft.Extensions.Logging; -using WheelWizard.DolphinManagent.Abstractions; +using WheelWizard.DolphinManagement.Abstractions; +using WheelWizard.Helpers; using WheelWizard.Services; using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; @@ -28,15 +30,19 @@ private sealed record DolphinCandidate(string DisplayName, string? Path, bool Fo [Inject] private IDolphinLocator DolphinLocator { get; set; } = null!; + [Inject] + private IDolphinInstaller DolphinInstaller { get; set; } = null!; + [Inject] private ISettingsManager SettingsService { get; set; } = null!; public FirstTimeSetupPopup() - : base(true, false, true, "Wheel Wizard") + : base(true, false, false, "Wheel Wizard") { InitializeComponent(); PlatformTextBlock.Text = $"Detected platform: {GetPlatformName()}"; + //BrowseButton.IsEnabled = EnvHelper.IsFlatpakSandboxed(); Not sure how to check for flatpak installation because this doesnt really work PopulateDetectedLocations(); } @@ -80,7 +86,7 @@ private void PopulateDetectedLocations() DetectedLocationsPanel.Children.Add(CreateCandidateRow(candidate)); } - private RadioButton CreateCandidateRow(DolphinInstallation candidate) + private Grid CreateCandidateRow(DolphinInstallation candidate) { var subtitle = candidate.Found ? candidate.LaunchTarget ?? string.Empty : "Not found on this system"; @@ -112,9 +118,62 @@ private RadioButton CreateCandidateRow(DolphinInstallation candidate) HorizontalContentAlignment = HorizontalAlignment.Stretch, }; radio.IsCheckedChanged += DetectedLocation_OnChecked; - return radio; + + var row = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + row.Children.Add(radio); + + if (!candidate.Found && candidate.DisplayName.Contains("flatpak", StringComparison.OrdinalIgnoreCase)) + { + var installButton = new Components.Button + { + Variant = Components.Button.ButtonsVariantType.Default, + Text = "Install", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8, 0, 0, 0), + }; + installButton.Click += InstallFlatpakButton_OnClick; + Grid.SetColumn(installButton, 1); + row.Children.Add(installButton); + } + + return row; } + private async void InstallFlatpakButton_OnClick(object? sender, RoutedEventArgs e) + { + var progressWindow = new ProgressWindow() + .SetGoal(t("progress.installing_dolphin")) + .SetExtraText(t("progress.this_may_take_a_while")); + progressWindow.Show(); + var progress = new Progress(progressWindow.UpdateProgress); + var installResult = await DolphinInstaller.InstallDolphin(progress); + progressWindow.Close(); + if (installResult.IsFailure) + { + await new MessageBoxWindow() + .SetMessageType(MessageBoxWindow.MessageType.Error) + .SetTitleText("Failed to install Dolphin") + .SetInfoText(installResult.Error.Message) + .ShowDialog(); + return; + } + + // Reload all radio buttons + var candidates = DolphinLocator.DetectInstallations(); + foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) + { + if (radio?.Tag is null) + continue; + var match = candidates.FirstOrDefault(c => radio.Tag.Equals(c.LaunchTarget)); + if (match != null) + { + radio.IsEnabled = match.Found; + } + } + } + + private static IEnumerable GetRadioButtons(Control row) => row is Panel panel ? panel.Children.OfType() : []; + private void DetectedLocation_OnChecked(object? sender, RoutedEventArgs e) { if (sender is not RadioButton { IsChecked: true } radio) @@ -128,17 +187,14 @@ private async void BrowseButton_OnClick(object? sender, RoutedEventArgs e) { try { - var path = await OpenDolphinFilePickerAsync(); + var path = await OpenDolphinFilePickerAsync(this); Logger.LogInformation(path); if (string.IsNullOrWhiteSpace(path)) return; // A manual pick wins: clear any detected radio selection. - foreach (var child in DetectedLocationsPanel.Children) - { - if (child is RadioButton radio) - radio.IsChecked = false; - } + foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) + radio.IsChecked = false; ManualPathTextBox.Text = path; SetSelectedTarget(path); @@ -182,7 +238,7 @@ private void ShowError(string message) ErrorTextBlock.IsVisible = true; } - private static async Task OpenDolphinFilePickerAsync() + private static async Task OpenDolphinFilePickerAsync(Visual owner) { var executableFileType = new FilePickerFileType("Executable files") { @@ -194,7 +250,7 @@ private void ShowError(string message) _ => new[] { "*" }, // Fallback }, }; - var filePath = await FilePickerHelper.OpenSingleFileAsync("Select the Dolphin executable", [executableFileType]); + var filePath = await FilePickerHelper.OpenSingleFileAsync("Select the Dolphin executable", [executableFileType], owner); return filePath; } } From fda641f368012fbadc69874a4315d37ce0eeea87 Mon Sep 17 00:00:00 2001 From: altpyrion <294315026+altpyrion@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:55:04 +0200 Subject: [PATCH 3/3] Added the path selection popup --- .../FirstTimeSetupPopup.DolphinStep.cs | 212 +++++++++++++++++ .../FirstTimeSetupPopup.GamePathsStep.cs | 72 ++++++ .../Popups/Generic/FirstTimeSetupPopup.axaml | 194 ++++++++++----- .../Generic/FirstTimeSetupPopup.axaml.cs | 225 ++++-------------- 4 files changed, 459 insertions(+), 244 deletions(-) create mode 100644 WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.DolphinStep.cs create mode 100644 WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.GamePathsStep.cs diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.DolphinStep.cs b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.DolphinStep.cs new file mode 100644 index 00000000..ec2d9f3d --- /dev/null +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.DolphinStep.cs @@ -0,0 +1,212 @@ +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform.Storage; +using Microsoft.Extensions.Logging; +using WheelWizard.DolphinManagement.Abstractions; +using WheelWizard.Services; +using WheelWizard.Shared.DependencyInjection; + +namespace WheelWizard.Views.Popups.Generic; + +public partial class FirstTimeSetupPopup +{ + private string? _selectedDolphinTarget; + + [Inject] + private IDolphinLocator DolphinLocator { get; set; } = null!; + + [Inject] + private IDolphinInstaller DolphinInstaller { get; set; } = null!; + + private bool IsDolphinStepComplete => !string.IsNullOrWhiteSpace(_selectedDolphinTarget); + + private void InitializeDolphinStep() + { + PlatformTextBlock.Text = $"Detected platform: {GetPlatformName()}"; + //BrowseButton.IsEnabled = EnvHelper.IsFlatpakSandboxed(); Not sure how to check for flatpak installation because this doesnt really work + PopulateDetectedLocations(); + } + + private void SaveDolphinStep() => SettingsService.Set(SettingsService.DOLPHIN_LOCATION, _selectedDolphinTarget!); + + private static string GetPlatformName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "Windows"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "macOS"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return "Linux"; + return "Unknown"; + } + + private void PopulateDetectedLocations() + { + DetectedLocationsPanel.Children.Clear(); + + var candidates = DolphinLocator.DetectInstallations(); + if (candidates.Count == 0) + { + DetectedLocationsPanel.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + Opacity = 0.75, + TextWrapping = TextWrapping.Wrap, + Text = "No Dolphin installations were detected automatically. Please select one manually below.", + } + ); + return; + } + + foreach (var candidate in candidates) + DetectedLocationsPanel.Children.Add(CreateCandidateRow(candidate)); + } + + private Grid CreateCandidateRow(DolphinInstallation candidate) + { + var subtitle = candidate.Found ? candidate.LaunchTarget ?? string.Empty : "Not found on this system"; + + var content = new StackPanel { Spacing = 2 }; + content.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + FontWeight = FontWeight.SemiBold, + Text = candidate.DisplayName, + } + ); + content.Children.Add( + new TextBlock + { + Classes = { "BodyText" }, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + Text = subtitle, + } + ); + + var radio = new RadioButton + { + GroupName = "DolphinLocation", + Content = content, + IsEnabled = candidate.Found, + Tag = candidate.LaunchTarget, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }; + radio.IsCheckedChanged += DetectedLocation_OnChecked; + + var row = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + row.Children.Add(radio); + + if (!candidate.Found && candidate.DisplayName.Contains("flatpak", StringComparison.OrdinalIgnoreCase)) + { + var installButton = new Components.Button + { + Variant = Components.Button.ButtonsVariantType.Default, + Text = "Install", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8, 0, 0, 0), + }; + installButton.Click += InstallFlatpakButton_OnClick; + Grid.SetColumn(installButton, 1); + row.Children.Add(installButton); + } + + return row; + } + + private async void InstallFlatpakButton_OnClick(object? sender, RoutedEventArgs e) + { + var progressWindow = new ProgressWindow() + .SetGoal(t("progress.installing_dolphin")) + .SetExtraText(t("progress.this_may_take_a_while")); + progressWindow.Show(); + var progress = new Progress(progressWindow.UpdateProgress); + var installResult = await DolphinInstaller.InstallDolphin(progress); + progressWindow.Close(); + if (installResult.IsFailure) + { + await new MessageBoxWindow() + .SetMessageType(MessageBoxWindow.MessageType.Error) + .SetTitleText("Failed to install Dolphin") + .SetInfoText(installResult.Error.Message) + .ShowDialog(); + return; + } + + // Reload all radio buttons + // TODO: Also reload the subtext of every radio button after install + var candidates = DolphinLocator.DetectInstallations(); + foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) + { + if (radio?.Tag is null) + continue; + var match = candidates.FirstOrDefault(c => radio.Tag.Equals(c.LaunchTarget)); + if (match != null) + { + radio.IsEnabled = match.Found; + } + } + } + + private static IEnumerable GetRadioButtons(Control row) => row is Panel panel ? panel.Children.OfType() : []; + + private void DetectedLocation_OnChecked(object? sender, RoutedEventArgs e) + { + if (sender is not RadioButton { IsChecked: true } radio) + return; + + ManualPathTextBox.Text = string.Empty; + SetSelectedTarget(radio.Tag as string); + } + + private async void BrowseButton_OnClick(object? sender, RoutedEventArgs e) + { + try + { + var path = await OpenDolphinFilePickerAsync(this); + Logger.LogInformation(path); + if (string.IsNullOrWhiteSpace(path)) + return; + + // A manual pick wins: clear any detected radio selection. + foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) + radio.IsChecked = false; + + ManualPathTextBox.Text = path; + SetSelectedTarget(path); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to pick a Dolphin location."); + ShowError("Something went wrong while selecting the file."); + } + } + + private void SetSelectedTarget(string? target) + { + _selectedDolphinTarget = string.IsNullOrWhiteSpace(target) ? null : target; + UpdateContinueState(); + } + + private static async Task OpenDolphinFilePickerAsync(Visual owner) + { + var executableFileType = new FilePickerFileType("Executable files") + { + Patterns = Environment.OSVersion.Platform switch + { + PlatformID.Win32NT => new[] { "*.exe" }, + PlatformID.Unix => new[] { "*", "*.sh" }, + PlatformID.MacOSX => new[] { "*", "*.app" }, + _ => new[] { "*" }, // Fallback + }, + }; + var filePath = await FilePickerHelper.OpenSingleFileAsync("Select the Dolphin executable", [executableFileType], owner); + return filePath; + } +} diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.GamePathsStep.cs b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.GamePathsStep.cs new file mode 100644 index 00000000..effc1f73 --- /dev/null +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.GamePathsStep.cs @@ -0,0 +1,72 @@ +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using Microsoft.Extensions.Logging; +using WheelWizard.Services; + +namespace WheelWizard.Views.Popups.Generic; + +public partial class FirstTimeSetupPopup +{ + private string? _selectedUserFolder; + private string? _selectedGameFile; + + private bool IsGamePathsStepComplete => + !string.IsNullOrWhiteSpace(_selectedUserFolder) && !string.IsNullOrWhiteSpace(_selectedGameFile); + + private void SaveGamePathsStep() + { + SettingsService.Set(SettingsService.USER_FOLDER_PATH, _selectedUserFolder!); + SettingsService.Set(SettingsService.GAME_LOCATION, _selectedGameFile!); + } + + private void AutoDetectPaths() + { + var folderPath = PathManager.TryFindUserFolderPath(); + if (!string.IsNullOrEmpty(folderPath)) + UserFolderTextBox.Text = folderPath; + _selectedUserFolder = folderPath; + } + + private async void BrowseUserFolderButton_OnClick(object? sender, RoutedEventArgs e) + { + try + { + var folders = await FilePickerHelper.SelectFolderAsync("Select the Dolphin user folder", owner: this); + var path = FilePickerHelper.TryResolveLocalPath(folders.FirstOrDefault()); + if (string.IsNullOrWhiteSpace(path)) + return; + + UserFolderTextBox.Text = path; + _selectedUserFolder = path; + UpdateContinueState(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to pick the Dolphin user folder."); + ShowError("Something went wrong while selecting the folder."); + } + } + + private async void BrowseGameFileButton_OnClick(object? sender, RoutedEventArgs e) + { + try + { + var gameFileType = new FilePickerFileType("Wii game files") + { + Patterns = ["*.iso", "*.wbfs", "*.rvz", "*.ciso", "*.nkit.iso", "*.wia"], + }; + var path = await FilePickerHelper.OpenSingleFileAsync("Select your Mario Kart Wii game file", [gameFileType], this); + if (string.IsNullOrWhiteSpace(path)) + return; + + GameFileTextBox.Text = path; + _selectedGameFile = path; + UpdateContinueState(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to pick the Mario Kart Wii game file."); + ShowError("Something went wrong while selecting the file."); + } + } +} diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml index ac8ffcb8..996e8ae5 100644 --- a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml @@ -6,81 +6,149 @@ xmlns:base="clr-namespace:WheelWizard.Views.Popups.Base" mc:Ignorable="d" x:Class="WheelWizard.Views.Popups.Generic.FirstTimeSetupPopup"> - - + + + + - + + + + + + + + + + + + - - + - + Text="Or select it manually" /> + + + + - + - - - + + + - - - - - - - - + Classes="BodyText" + Text="Wheel Wizard still needs your Dolphin user (data) folder and your Mario Kart Wii disc image." /> - + + + + + + + + + + + + + + + + + + + + - - + + + + + + diff --git a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs index 57a7a3bf..092b6374 100644 --- a/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs +++ b/WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml.cs @@ -1,14 +1,5 @@ -using System.Runtime.InteropServices; -using Avalonia; -using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform.Storage; using Microsoft.Extensions.Logging; -using WheelWizard.DolphinManagement.Abstractions; -using WheelWizard.Helpers; -using WheelWizard.Services; using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Base; @@ -17,33 +8,33 @@ namespace WheelWizard.Views.Popups.Generic; public partial class FirstTimeSetupPopup : PopupContent { - private sealed record DolphinCandidate(string DisplayName, string? Path, bool Found); + private enum SetupStep + { + Dolphin, + GamePaths, + } private readonly TaskCompletionSource _completionSource = new(); private bool _setupCompleted; - private string? _selectedDolphinTarget; + private SetupStep _currentStep = SetupStep.Dolphin; [Inject] private ILogger Logger { get; set; } = null!; - [Inject] - private IDolphinLocator DolphinLocator { get; set; } = null!; - - [Inject] - private IDolphinInstaller DolphinInstaller { get; set; } = null!; - [Inject] private ISettingsManager SettingsService { get; set; } = null!; + public bool WasSkipped { get; private set; } + public FirstTimeSetupPopup() : base(true, false, false, "Wheel Wizard") { InitializeComponent(); - PlatformTextBlock.Text = $"Detected platform: {GetPlatformName()}"; - //BrowseButton.IsEnabled = EnvHelper.IsFlatpakSandboxed(); Not sure how to check for flatpak installation because this doesnt really work - PopulateDetectedLocations(); + InitializeDolphinStep(); + ShowStep(SetupStep.Dolphin); + AutoDetectPaths(); } public Task ShowAndAwaitCompletionAsync() @@ -52,177 +43,65 @@ public Task ShowAndAwaitCompletionAsync() return _completionSource.Task; } - private static string GetPlatformName() + private void ShowStep(SetupStep step) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "Windows"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return "macOS"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return "Linux"; - return "Unknown"; - } + _currentStep = step; - private void PopulateDetectedLocations() - { - DetectedLocationsPanel.Children.Clear(); + DolphinStepPanel.IsVisible = step == SetupStep.Dolphin; + GamePathsStepPanel.IsVisible = step == SetupStep.GamePaths; - var candidates = DolphinLocator.DetectInstallations(); - if (candidates.Count == 0) - { - DetectedLocationsPanel.Children.Add( - new TextBlock - { - Classes = { "BodyText" }, - Opacity = 0.75, - TextWrapping = TextWrapping.Wrap, - Text = "No Dolphin installations were detected automatically. Please select one manually below.", - } - ); - return; - } + BackButton.IsVisible = step == SetupStep.GamePaths; + SkipButton.IsVisible = step == SetupStep.GamePaths; - foreach (var candidate in candidates) - DetectedLocationsPanel.Children.Add(CreateCandidateRow(candidate)); + UpdateContinueState(); } - private Grid CreateCandidateRow(DolphinInstallation candidate) + private void UpdateContinueState() { - var subtitle = candidate.Found ? candidate.LaunchTarget ?? string.Empty : "Not found on this system"; - - var content = new StackPanel { Spacing = 2 }; - content.Children.Add( - new TextBlock - { - Classes = { "BodyText" }, - FontWeight = FontWeight.SemiBold, - Text = candidate.DisplayName, - } - ); - content.Children.Add( - new TextBlock - { - Classes = { "BodyText" }, - Opacity = 0.7, - TextWrapping = TextWrapping.Wrap, - Text = subtitle, - } - ); - - var radio = new RadioButton + ContinueButton.IsEnabled = _currentStep switch { - GroupName = "DolphinLocation", - Content = content, - IsEnabled = candidate.Found, - Tag = candidate.LaunchTarget, - HorizontalContentAlignment = HorizontalAlignment.Stretch, + SetupStep.Dolphin => IsDolphinStepComplete, + SetupStep.GamePaths => IsGamePathsStepComplete, + _ => false, }; - radio.IsCheckedChanged += DetectedLocation_OnChecked; - - var row = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; - row.Children.Add(radio); - - if (!candidate.Found && candidate.DisplayName.Contains("flatpak", StringComparison.OrdinalIgnoreCase)) - { - var installButton = new Components.Button - { - Variant = Components.Button.ButtonsVariantType.Default, - Text = "Install", - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(8, 0, 0, 0), - }; - installButton.Click += InstallFlatpakButton_OnClick; - Grid.SetColumn(installButton, 1); - row.Children.Add(installButton); - } - - return row; + ErrorTextBlock.IsVisible = false; } - private async void InstallFlatpakButton_OnClick(object? sender, RoutedEventArgs e) + private void ContinueButton_OnClick(object? sender, RoutedEventArgs e) { - var progressWindow = new ProgressWindow() - .SetGoal(t("progress.installing_dolphin")) - .SetExtraText(t("progress.this_may_take_a_while")); - progressWindow.Show(); - var progress = new Progress(progressWindow.UpdateProgress); - var installResult = await DolphinInstaller.InstallDolphin(progress); - progressWindow.Close(); - if (installResult.IsFailure) - { - await new MessageBoxWindow() - .SetMessageType(MessageBoxWindow.MessageType.Error) - .SetTitleText("Failed to install Dolphin") - .SetInfoText(installResult.Error.Message) - .ShowDialog(); - return; - } - - // Reload all radio buttons - var candidates = DolphinLocator.DetectInstallations(); - foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) + if (_currentStep == SetupStep.Dolphin) { - if (radio?.Tag is null) - continue; - var match = candidates.FirstOrDefault(c => radio.Tag.Equals(c.LaunchTarget)); - if (match != null) + if (!IsDolphinStepComplete) { - radio.IsEnabled = match.Found; + ShowError("Please select a Dolphin installation to continue."); + return; } - } - } - - private static IEnumerable GetRadioButtons(Control row) => row is Panel panel ? panel.Children.OfType() : []; - private void DetectedLocation_OnChecked(object? sender, RoutedEventArgs e) - { - if (sender is not RadioButton { IsChecked: true } radio) + SaveDolphinStep(); + ShowStep(SetupStep.GamePaths); return; - - ManualPathTextBox.Text = string.Empty; - SetSelectedTarget(radio.Tag as string); - } - - private async void BrowseButton_OnClick(object? sender, RoutedEventArgs e) - { - try - { - var path = await OpenDolphinFilePickerAsync(this); - Logger.LogInformation(path); - if (string.IsNullOrWhiteSpace(path)) - return; - - // A manual pick wins: clear any detected radio selection. - foreach (var radio in DetectedLocationsPanel.Children.SelectMany(GetRadioButtons)) - radio.IsChecked = false; - - ManualPathTextBox.Text = path; - SetSelectedTarget(path); } - catch (Exception ex) + + if (!IsGamePathsStepComplete) { - Logger.LogError(ex, "Failed to pick a Dolphin location."); - ShowError("Something went wrong while selecting the file."); + ShowError("Please select both the Dolphin user folder and your Mario Kart Wii game file."); + return; } + + SaveGamePathsStep(); + CompleteSetup(); } - private void SetSelectedTarget(string? target) + private void BackButton_OnClick(object? sender, RoutedEventArgs e) => ShowStep(SetupStep.Dolphin); + + private void SkipButton_OnClick(object? sender, RoutedEventArgs e) { - _selectedDolphinTarget = string.IsNullOrWhiteSpace(target) ? null : target; - ContinueButton.IsEnabled = _selectedDolphinTarget != null; - ErrorTextBlock.IsVisible = false; + WasSkipped = true; + CompleteSetup(); } - private void ContinueButton_OnClick(object? sender, RoutedEventArgs e) + private void CompleteSetup() { - if (string.IsNullOrWhiteSpace(_selectedDolphinTarget)) - { - ShowError("Please select a Dolphin installation to continue."); - return; - } - - SettingsService.Set(SettingsService.DOLPHIN_LOCATION, _selectedDolphinTarget); - _setupCompleted = true; _completionSource.TrySetResult(true); Close(); @@ -237,20 +116,4 @@ private void ShowError(string message) ErrorTextBlock.Text = message; ErrorTextBlock.IsVisible = true; } - - private static async Task OpenDolphinFilePickerAsync(Visual owner) - { - var executableFileType = new FilePickerFileType("Executable files") - { - Patterns = Environment.OSVersion.Platform switch - { - PlatformID.Win32NT => new[] { "*.exe" }, - PlatformID.Unix => new[] { "*", "*.sh" }, - PlatformID.MacOSX => new[] { "*", "*.app" }, - _ => new[] { "*" }, // Fallback - }, - }; - var filePath = await FilePickerHelper.OpenSingleFileAsync("Select the Dolphin executable", [executableFileType], owner); - return filePath; - } }