Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WheelWizard.DolphinManagent.Abstractions;

public record DolphinInstallation(string DisplayName, string LaunchTarget, bool Found);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace WheelWizard.DolphinManagent.Abstractions;

public interface IDolphinInstaller
{
IReadOnlyList<DolphinInstallation> AvailableInstallationMethods();
//bool InstallDolphin(DolphinInstallation method);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace WheelWizard.DolphinManagent.Abstractions;

public interface IDolphinLocator
{
IReadOnlyList<DolphinInstallation> DetectInstallations();
}
18 changes: 18 additions & 0 deletions WheelWizard/Features/DolphinManagent/DolphinManagmentExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<ILinuxCommandEnvironment, LinuxCommandEnvironment>();
services.AddSingleton<ILinuxProcessService, LinuxProcessService>();
//services.AddSingleton<IDolphinInstaller, LinuxDolphinInstaller>();
services.AddSingleton<IDolphinLocator, LinuxDolphinLocator>();
#endif
return services;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Empty file.
33 changes: 33 additions & 0 deletions WheelWizard/Features/DolphinManagent/Linux/LinuxDolphinLocator.cs
Original file line number Diff line number Diff line change
@@ -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<DolphinInstallation> DetectInstallations()
{
return
[
new("Flatpak", "flatpak run org.DolphinEmu.dolphin-emu", IsDolphinInstalledInFlatpak()),
new("Native", "dolphin-emu", IsDolphinInstalledNative()),
];
}
}
123 changes: 123 additions & 0 deletions WheelWizard/Features/DolphinManagent/Linux/LinuxProcessService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace WheelWizard.DolphinManagent.Linux;

public interface ILinuxProcessService
{
OperationResult<int> Run(string fileName, string arguments, out string stdOut, out string stdErr);
OperationResult<int> Run(string fileName, string arguments);
Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? progress = null);
Task<OperationResult> LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration);
}

public sealed class LinuxProcessService : ILinuxProcessService
{
public OperationResult<int> 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<int> Run(string fileName, string arguments)
{
return Run(fileName, arguments, out _, out _);
}

public async Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? 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<OperationResult> 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<int>? 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);
}
}
2 changes: 1 addition & 1 deletion WheelWizard/Services/Storage/FilePickerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static async Task<List<string>> 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;

Expand Down
3 changes: 2 additions & 1 deletion WheelWizard/SetupExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
12 changes: 12 additions & 0 deletions WheelWizard/Views/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ private async Task InitializeDesktopAsync(IClassicDesktopStyleApplicationLifetim
{
try
{
var settingsManager = Services.GetRequiredService<ISettingsManager>();
if (!settingsManager.PathsSetupCorrectly())
{
var firstTimeSetup = new FirstTimeSetupPopup();
var setupCompleted = await firstTimeSetup.ShowAndAwaitCompletionAsync();
if (!setupCompleted)
{
desktop.Shutdown();
return;
}
}

var resourceInstaller = Services.GetRequiredService<IMiiRenderingResourceInstaller>();
if (resourceInstaller.GetResolvedResourcePath().IsFailure)
{
Expand Down
86 changes: 86 additions & 0 deletions WheelWizard/Views/Popups/Generic/FirstTimeSetupPopup.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<base:PopupContent xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:components="clr-namespace:WheelWizard.Views.Components"
xmlns:base="clr-namespace:WheelWizard.Views.Popups.Base"
mc:Ignorable="d"
x:Class="WheelWizard.Views.Popups.Generic.FirstTimeSetupPopup">
<Grid Width="520" MinHeight="360" MaxHeight="560" RowDefinitions="Auto,Auto,Auto,*,Auto,Auto">
<TextBlock Classes="TitleText"
Text="Welcome to Wheel Wizard" />

<TextBlock Grid.Row="1"
Margin="0,14,0,0"
TextWrapping="Wrap"
Classes="BodyText"
Text="Let's get you set up. Wheel Wizard needs to know which Dolphin Emulator installation to use. We've scanned the usual locations for your system below." />

<Border Grid.Row="2"
Margin="0,18,0,0"
Padding="14"
Background="#12000000"
BorderBrush="{StaticResource Neutral700}"
BorderThickness="1"
CornerRadius="10">
<StackPanel Spacing="6">
<TextBlock Classes="BodyText"
FontWeight="SemiBold"
Text="Dolphin Emulator" />
<TextBlock x:Name="PlatformTextBlock"
Classes="BodyText"
Opacity="0.75"
Text="Detecting installations..." />
</StackPanel>
</Border>

<ScrollViewer Grid.Row="3"
Margin="0,12,0,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="DetectedLocationsPanel" Spacing="8" />
</ScrollViewer>

<StackPanel Grid.Row="4" Margin="0,14,0,0" Spacing="8">
<TextBlock Classes="BodyText"
FontWeight="SemiBold"
Text="Or select it manually" />
<Grid ColumnDefinitions="*,Auto">
<TextBox x:Name="ManualPathTextBox"
Classes="dark"
Grid.Column="0"
Height="40"
Margin="0,0,8,0"
VerticalContentAlignment="Center"
Watermark="No path selected"
IsReadOnly="True" />
<components:Button x:Name="BrowseButton"
Grid.Column="1"
Variant="Default"
Text="Browse..."
Click="BrowseButton_OnClick" />
</Grid>
<TextBlock x:Name="ErrorTextBlock"
Classes="BodyText"
Foreground="{StaticResource Danger400}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>

<UniformGrid Grid.Row="5"
Columns="2"
Margin="0,24,0,0">
<components:Button x:Name="CloseButton"
Margin="0,0,6,0"
Variant="Default"
Text="Close and Exit"
Click="CloseButton_OnClick" />
<components:Button x:Name="ContinueButton"
Margin="6,0,0,0"
Variant="Primary"
Text="Continue"
IsEnabled="False"
Click="ContinueButton_OnClick" />
</UniformGrid>
</Grid>
</base:PopupContent>
Loading
Loading