Browse Source

add Launch options dialog

pull/55/head
Ionite 1 year ago
parent
commit
d3523cc86e
No known key found for this signature in database
  1. 3
      StabilityMatrix.Avalonia/App.axaml.cs
  2. 57
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  3. 32
      StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs
  4. 46
      StabilityMatrix.Avalonia/Converters/LaunchOptionConverter.cs
  5. 34
      StabilityMatrix.Avalonia/Converters/LaunchOptionIntDoubleConverter.cs
  6. 19
      StabilityMatrix.Avalonia/Converters/ValueConverterGroup.cs
  7. 34
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  8. 20
      StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs
  9. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  10. 68
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  11. 85
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  12. 72
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  13. 6
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  14. 78
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  15. 22
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  16. 172
      StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml
  17. 18
      StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs
  18. 1
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml
  19. 5
      StabilityMatrix.Core/Helper/Cache/LRUCache.cs
  20. 39
      StabilityMatrix.Core/Models/LaunchOption.cs
  21. 129
      StabilityMatrix.Core/Models/LaunchOptionCard.cs
  22. 35
      StabilityMatrix.Core/Models/LaunchOptionDefinition.cs
  23. 5
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  24. 14
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  25. 7
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

3
StabilityMatrix.Avalonia/App.axaml.cs

@ -121,6 +121,7 @@ public partial class App : Application
services.AddTransient<OneClickInstallViewModel>();
services.AddTransient<SelectModelVersionViewModel>();
services.AddTransient<SelectDataDirectoryViewModel>();
services.AddTransient<LaunchOptionsViewModel>();
// Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>();
@ -136,6 +137,7 @@ public partial class App : Application
.Register(provider.GetRequiredService<OneClickInstallViewModel>)
.Register(provider.GetRequiredService<SelectModelVersionViewModel>)
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>));
@ -152,6 +154,7 @@ public partial class App : Application
// Dialogs
services.AddTransient<SelectDataDirectoryDialog>();
services.AddTransient<LaunchOptionsDialog>();
// Controls
services.AddTransient<RefreshBadge>();

57
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -6,7 +6,9 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
namespace StabilityMatrix.Avalonia.Controls;
@ -55,6 +57,26 @@ public class BetterContentDialog : ContentDialog
set => SetValue(IsFooterVisibleProperty, value);
}
public static readonly StyledProperty<ScrollBarVisibility> ContentVerticalScrollBarVisibilityProperty
= AvaloniaProperty.Register<BetterContentDialog, ScrollBarVisibility>(
"ContentScrollBarVisibility", ScrollBarVisibility.Auto);
public ScrollBarVisibility ContentVerticalScrollBarVisibility
{
get => GetValue(ContentVerticalScrollBarVisibilityProperty);
set => SetValue(ContentVerticalScrollBarVisibilityProperty, value);
}
public static readonly StyledProperty<double> MaxDialogWidthProperty = AvaloniaProperty.Register<BetterContentDialog, double>(
"MaxDialogWidth");
public double MaxDialogWidth
{
get => GetValue(MaxDialogWidthProperty);
set => SetValue(MaxDialogWidthProperty, value);
}
public BetterContentDialog()
{
AddHandler(LoadedEvent, OnLoaded);
@ -85,25 +107,42 @@ public class BetterContentDialog : ContentDialog
private void OnLoaded(object? sender, RoutedEventArgs? e)
{
TryBindButtons();
// Check if we need to hide the footer
if (IsFooterVisible) return;
// Find the named grid
// https://github.com/amwx/FluentAvalonia/blob/master/src/FluentAvalonia/Styling/
// ControlThemes/FAControls/ContentDialogStyles.axaml#L96
var border = VisualChildren[0] as Border;
var panel = border?.Child as Panel;
var faBorder = panel?.Children[0] as FABorder;
// Set widths
if (MaxDialogWidth > 0)
{
faBorder!.MaxWidth = MaxDialogWidth;
}
var border2 = faBorder?.Child as Border;
var grid = border2?.Child as Grid;
// Named Grid 'DialogSpace'
if (border2?.Child is not Grid dialogSpaceGrid) throw new InvalidOperationException("Could not find DialogSpace grid");
var scrollViewer = dialogSpaceGrid.Children[0] as ScrollViewer;
var actualBorder = dialogSpaceGrid.Children[1] as Border;
// Get the parent border, which is what we want to hide
if (grid?.Children[1] is not Border actualBorder)
if (scrollViewer is null || actualBorder is null)
{
throw new InvalidOperationException("Could not find parent border");
}
// Hide the border
actualBorder.IsVisible = false;
// Set footer and scrollbar visibility states
actualBorder.IsVisible = IsFooterVisible;
scrollViewer.VerticalScrollBarVisibility = ContentVerticalScrollBarVisibility;
// Also call the vm's OnLoad
if (Content is Control {DataContext: ViewModelBase viewModel})
{
viewModel.OnLoaded();
Dispatcher.UIThread.InvokeAsync(
async () => await viewModel.OnLoadedAsync());
}
}
}

32
StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class LaunchOptionCardTemplateSelector : IDataTemplate
{
// public bool SupportsRecycling => false;
// ReSharper disable once CollectionNeverUpdated.Global
[Content]
public Dictionary<LaunchOptionType, IDataTemplate> Templates { get; } = new();
// Check if we can accept the provided data
public bool Match(object? data)
{
return data is LaunchOptionCard;
}
// Build the DataTemplate here
public Control Build(object? data)
{
if (data is not LaunchOptionCard card) throw new ArgumentException(null, nameof(data));
return Templates[card.Type].Build(card)!;
}
}

46
StabilityMatrix.Avalonia/Converters/LaunchOptionConverter.cs

@ -0,0 +1,46 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
public class LaunchOptionConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(string))
{
return value?.ToString() ?? "";
}
if (targetType == typeof(bool?))
{
return bool.TryParse(value?.ToString(), out var boolValue) && boolValue;
}
if (targetType == typeof(double?))
{
if (value == null)
{
return null;
}
return double.TryParse(value?.ToString(), out var doubleValue) ? doubleValue : 0;
}
if (targetType == typeof(int?))
{
if (value == null)
{
return null;
}
return int.TryParse(value?.ToString(), out var intValue) ? intValue : 0;
}
throw new ArgumentException("Unsupported type");
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
}

34
StabilityMatrix.Avalonia/Converters/LaunchOptionIntDoubleConverter.cs

@ -0,0 +1,34 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
public class LaunchOptionIntDoubleConverter : IValueConverter
{
// Convert from int to double
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(double?))
{
if (value == null)
{
return null;
}
return System.Convert.ToDouble(value);
}
throw new ArgumentException($"Unsupported type {targetType}");
}
// Convert from double to object int (floor)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(int?) || targetType == typeof(object))
{
return System.Convert.ToInt32(value);
}
throw new ArgumentException($"Unsupported type {targetType}");
}
}

19
StabilityMatrix.Avalonia/Converters/ValueConverterGroup.cs

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
public class ValueConverterGroup : List<IValueConverter>, IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
}
public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

34
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using AvaloniaEdit.Utils;
using Microsoft.Extensions.DependencyInjection;
@ -53,7 +54,7 @@ public static class DesignData
services.AddLogging();
services.AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<INotificationService, MockNotificationService>()
.AddSingleton<ISharedFolders, SharedFolders>()
.AddSingleton<ISharedFolders, MockSharedFolders>()
.AddSingleton<IDownloadService, MockDownloadService>()
.AddSingleton<ModelFinder>();
@ -77,6 +78,7 @@ public static class DesignData
var settingsManager = Services.GetRequiredService<ISettingsManager>();
var downloadService = Services.GetRequiredService<IDownloadService>();
var modelFinder = Services.GetRequiredService<ModelFinder>();
var packageFactory = Services.GetRequiredService<IPackageFactory>();
var notificationService = Services.GetRequiredService<INotificationService>();
// Main window
@ -107,6 +109,32 @@ public static class DesignData
// Sample data for dialogs
SelectModelVersionViewModel.Versions = sampleCivitVersions;
SelectModelVersionViewModel.SelectedVersion = sampleCivitVersions[0];
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[]
{
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{
Name = "Host",
Type = LaunchOptionType.String,
Description = "The host name for the Web UI",
DefaultValue = "localhost",
Options = { "--host" }
}),
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = { "--api" }
})
};
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages =
packageFactory.GetAllAvailablePackages().ToImmutableArray();
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
// Checkpoints page
CheckpointsPageViewModel.CheckpointFolders = new ObservableCollection<CheckpointFolder>
@ -178,8 +206,10 @@ public static class DesignData
public static CheckpointBrowserViewModel CheckpointBrowserViewModel => Services.GetRequiredService<CheckpointBrowserViewModel>();
public static SelectModelVersionViewModel SelectModelVersionViewModel => Services.GetRequiredService<SelectModelVersionViewModel>();
public static OneClickInstallViewModel OneClickInstallViewModel => Services.GetRequiredService<OneClickInstallViewModel>();
public static InstallerViewModel InstallerViewModel => Services.GetRequiredService<InstallerViewModel>();
public static InstallerViewModel InstallerViewModel { get; }
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => Services.GetRequiredService<SelectDataDirectoryViewModel>();
public static LaunchOptionsViewModel LaunchOptionsViewModel { get; }
public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{

20
StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs

@ -0,0 +1,20 @@
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockSharedFolders : ISharedFolders
{
public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory)
{
}
public void UpdateLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory)
{
}
public void RemoveLinksForAllPackages()
{
}
}

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -10,6 +10,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<Folder Include="Behaviors\" />
<Folder Include="Models\" />
</ItemGroup>
@ -22,9 +23,11 @@
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.0-rc1" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.8" />

68
StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -30,7 +32,7 @@ using PackageVersion = StabilityMatrix.Core.Models.PackageVersion;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class InstallerViewModel : ViewModelBase
public partial class InstallerViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -44,7 +46,7 @@ public partial class InstallerViewModel : ViewModelBase
[ObservableProperty] private BasePackage selectedPackage;
[ObservableProperty] private PackageVersion? selectedVersion;
[ObservableProperty] private ObservableCollection<BasePackage>? availablePackages;
[ObservableProperty] private IReadOnlyList<BasePackage>? availablePackages;
[ObservableProperty] private ObservableCollection<GitHubCommit>? availableCommits;
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions;
@ -63,8 +65,7 @@ public partial class InstallerViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))]
private PackageVersionType availableVersionTypes =
PackageVersionType.GithubRelease | PackageVersionType.Commit;
public string ReleaseLabelText => SelectedVersionType == PackageVersionType.GithubRelease
? "Version" : "Branch";
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch";
public bool IsReleaseMode
{
get => SelectedVersionType == PackageVersionType.GithubRelease;
@ -100,6 +101,12 @@ public partial class InstallerViewModel : ViewModelBase
// AvailablePackages and SelectedPackage need to be set in init
}
public override void OnLoaded()
{
if (AvailablePackages == null) return;
SelectedPackage = AvailablePackages[0];
}
public override async Task OnLoadedAsync()
{
@ -272,7 +279,20 @@ public partial class InstallerViewModel : ViewModelBase
ProcessRunner.OpenUrl(url);
}
}
// When available version types change, reset selected version type if not compatible
partial void OnAvailableVersionTypesChanged(PackageVersionType value)
{
if (!value.HasFlag(SelectedVersionType))
{
SelectedVersionType = value;
}
}
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value) => OnSelectedPackageChanged(SelectedPackage);
partial void OnSelectedPackageChanged(BasePackage? value)
{
if (value == null) return;
@ -285,40 +305,30 @@ public partial class InstallerViewModel : ViewModelBase
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
// Reset selected if not compatible
if (!AvailableVersionTypes.HasFlag(SelectedVersionType))
{
SelectedVersionType = PackageVersionType.Commit;
}
var isReleaseMode = SelectedVersionType == PackageVersionType.GithubRelease;
if (Design.IsDesignMode) return;
Task.Run(async () =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
var versions = (await value.GetAllVersions(isReleaseMode)).ToList();
Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList();
if (!versions.Any()) return;
Dispatcher.UIThread.Post(() =>
{
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
SelectedVersion = AvailableVersions[0];
ReleaseNotes = versions.First().ReleaseNotesMarkdown;
});
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0];
ReleaseNotes = versions.First().ReleaseNotesMarkdown;
Logger.Debug($"Loaded release notes for {ReleaseNotes}");
if (!isReleaseMode)
if (!IsReleaseMode)
{
var commits = await value.GetAllCommits(SelectedVersion!.TagName);
var commits = await value.GetAllCommits(SelectedVersion.TagName);
if (commits is null || commits.Count == 0) return;
Dispatcher.UIThread.Post(() =>
{
AvailableCommits = new ObservableCollection<GitHubCommit>(commits);
SelectedCommit = AvailableCommits[0];
SelectedVersion = AvailableVersions?.FirstOrDefault(packageVersion =>
packageVersion.TagName.ToLowerInvariant() is "master" or "main");
});
AvailableCommits = new ObservableCollection<GitHubCommit>(commits);
SelectedCommit = AvailableCommits[0];
SelectedVersion = AvailableVersions?.FirstOrDefault(packageVersion =>
packageVersion.TagName.ToLowerInvariant() is "master" or "main");
}
}).SafeFireAndForget();
}

85
StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(LaunchOptionsDialog))]
public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
{
private readonly LRUCache<string, ImmutableList<LaunchOptionCard>> cache = new(100);
[ObservableProperty] private string title = "Launch Options";
[ObservableProperty] private bool isSearchBoxEnabled = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FilteredCards))]
private string searchText = string.Empty;
[ObservableProperty]
private IReadOnlyList<LaunchOptionCard>? filteredCards;
public IReadOnlyList<LaunchOptionCard>? Cards { get; set; }
/// <summary>
/// Return cards that match the search text
/// </summary>
private IReadOnlyList<LaunchOptionCard>? GetFilteredCards()
{
var text = SearchText;
if (string.IsNullOrWhiteSpace(text) || text.Length < 2)
{
return Cards;
}
// Try cache
if (cache.Get(text, out var cachedCards))
{
return cachedCards!;
}
var searchCard = new LaunchOptionCard
{
Title = text.ToLowerInvariant(),
Type = LaunchOptionType.Bool,
Options = Array.Empty<LaunchOption>()
};
var extracted = FuzzySharp.Process
.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant());
var results = extracted
.Where(r => r.Score > 40)
.Select(r => r.Value)
.ToImmutableList();
cache.Add(text, results);
return results;
}
public void UpdateFilterCards() => FilteredCards = GetFilteredCards();
public override void OnLoaded()
{
base.OnLoaded();
UpdateFilterCards();
}
/// <summary>
/// Export the current cards options to a list of strings
/// </summary>
public List<LaunchOption> AsLaunchArgs()
{
var launchArgs = new List<LaunchOption>();
if (Cards is null) return launchArgs;
foreach (var card in Cards)
{
launchArgs.AddRange(card.Options);
}
return launchArgs;
}
}

72
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
@ -6,14 +7,19 @@ using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using System.Threading.Tasks.Dataflow;
using Avalonia;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using AvaloniaEdit.Document;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@ -33,6 +39,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
private readonly IPackageFactory packageFactory;
private readonly IPyRunner pyRunner;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> dialogFactory;
public override string Title => "Launch";
public override Symbol Icon => Symbol.PlayFilled;
@ -59,13 +67,14 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
private string webUiUrl = string.Empty;
public LaunchPageViewModel(ILogger<LaunchPageViewModel> logger, ISettingsManager settingsManager, IPackageFactory packageFactory,
IPyRunner pyRunner, INotificationService notificationService)
IPyRunner pyRunner, INotificationService notificationService, ServiceManager<ViewModelBase> dialogFactory)
{
this.logger = logger;
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.pyRunner = pyRunner;
this.notificationService = notificationService;
this.dialogFactory = dialogFactory;
EventManager.Instance.PackageLaunchRequested +=
async (s, e) => await OnPackageLaunchRequested(s, e);
@ -176,6 +185,67 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
RunningPackage = basePackage;
}
[RelayCommand]
private async Task Config()
{
var activeInstall = SelectedPackage;
var name = activeInstall?.PackageName;
if (name == null || activeInstall == null)
{
logger.LogWarning($"Selected package is null");
return;
}
var package = packageFactory.FindPackageByName(name);
if (package == null)
{
logger.LogWarning("Package {Name} not found", name);
return;
}
var definitions = package.LaunchOptions;
// Check if package supports IArgParsable
// Use dynamic parsed args over static
/*if (package is IArgParsable parsable)
{
var rootPath = activeInstall.FullPath!;
var moduleName = parsable.RelativeArgsDefinitionScriptPath;
var parser = new ArgParser(pyRunner, rootPath, moduleName);
definitions = await parser.GetArgsAsync();
}*/
// Open a config page
var userLaunchArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
var viewModel = dialogFactory.Get<LaunchOptionsViewModel>();
viewModel.Cards = LaunchOptionCard.FromDefinitions(definitions, userLaunchArgs)
.ToImmutableArray();
logger.LogDebug("Launching config dialog with cards: {CardsCount}",
viewModel.Cards.Count);
var dialog = new BetterContentDialog
{
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
IsPrimaryButtonEnabled = true,
PrimaryButtonText = "Save",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary,
Padding = new Thickness(0, 16),
Content = new LaunchOptionsDialog
{
DataContext = viewModel,
}
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
// Save config
var args = viewModel.AsLaunchArgs();
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
}
private async Task BeginUpdateConsole(CancellationToken ct)
{
try

6
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@ -333,8 +334,13 @@ public partial class PackageManagerViewModel : PageViewModelBase
private async Task ShowInstallDialog()
{
var viewModel = dialogFactory.Get<InstallerViewModel>();
viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
viewModel.SelectedPackage = viewModel.AvailablePackages[0];
var dialog = new BetterContentDialog
{
MaxDialogWidth = 700,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,

78
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -4,6 +4,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:idd="clr-namespace:Avalonia.Xaml.Interactions.DragAndDrop;assembly=Avalonia.Xaml.Interactions.DragAndDrop"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
@ -13,6 +15,15 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointsPage">
<controls:UserControlBase.DataTemplates>
<!-- Template for dropdown category checkbox item -->
<DataTemplate DataType="{x:Type vm:CheckpointFolder}">
<ui:ToggleMenuFlyoutItem
Text="{Binding TitleWithFilesCount}"
IsChecked="{Binding IsCategoryEnabled, Mode=TwoWay}"/>
</DataTemplate>
</controls:UserControlBase.DataTemplates>
<controls:UserControlBase.Resources>
<!--Direction="0"
ShadowDepth="0"-->
@ -39,24 +50,16 @@
</controls:Card>
</DataTemplate>
<!-- Right click menu for a checkpoint file -->
<!--<ui:CommandBarFlyout x:DataType="vm:CheckpointFile" Placement="Right" x:Key="FileCommandBarFlyout">
Note: unlike a regular CommandBar, primary items can be set as the xml content and don't need
to be wrapped in a <ui:CommandBarFlyout.PrimaryCommands> tag
<ui:CommandBarButton Label="Rename" IconSource="Save" ToolTip.Tip="Rename"
Command="{Binding RenameCommand}" />
<ui:CommandBarButton Label="Delete" IconSource="Delete" ToolTip.Tip="Delete"
Command="{Binding DeleteCommand}" />
</ui:CommandBarFlyout>-->
<!-- Checkpoint File Card -->
<DataTemplate DataType="{x:Type vm:CheckpointFile}" x:Key="CheckpointFileDataTemplate">
<Border
DragDrop.AllowDrop="False"
Background="Transparent"
BorderThickness="0"
BorderThickness="2"
Margin="8">
<!--TODO: MouseDragElementBehavior-->
<controls:Card Width="260">
<!-- Right click menu for a checkpoint file -->
<controls:Card.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Command="{Binding RenameCommand}"
@ -209,21 +212,37 @@
</Border>
</DataTemplate>
<!-- Checkpoint Folder Expander -->
<DataTemplate DataType="{x:Type vm:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate">
<Expander
Header="{Binding Title}"
IsExpanded="True"
IsExpanded="{Binding IsExpanded}"
Margin="8"
Padding="8,8,8,8"
IsVisible="{Binding IsCategoryEnabled, FallbackValue=True}">
<Expander.ContextMenu>
<ContextMenu>
<!-- ReSharper disable once Xaml.RedundantResource -->
<MenuItem Header="Show in Explorer"
Command="{Binding ShowInExplorerCommand}"
CommandParameter="{Binding DirectoryPath}"/>
</ContextMenu>
</Expander.ContextMenu>
<!-- Right click menu for a checkpoint folder -->
<Expander.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Text="Show in Explorer" IconSource="Open"
Command="{Binding ShowInExplorerCommand}"
CommandParameter="{Binding DirectoryPath}"/>
<ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutSubItem Text="New" IconSource="Add">
<ui:MenuFlyoutSubItem.Items>
<ui:MenuFlyoutItem Text="Folder" IconSource="OpenFolder"
Command="{Binding CreateSubFolderCommand}"/>
</ui:MenuFlyoutSubItem.Items>
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Expander.ContextFlyout>
<!-- Editable header -->
<Expander.Header>
<Grid>
<TextBlock Text="{Binding Title}" VerticalAlignment="Center"/>
</Grid>
</Expander.Header>
<StackPanel Orientation="Vertical">
<!-- Subfolders -->
<StackPanel Orientation="Vertical">
@ -238,11 +257,13 @@
Background="Transparent"
DragDrop.AllowDrop="True">
<ItemsRepeater
Classes="ItemsDragAndDrop"
ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding CheckpointFiles}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal"/>
</ItemsRepeater.Layout>
<TextBlock Text="Hi"/>
</ItemsRepeater>
<!-- Blurred background for drag and drop -->
<Border
@ -289,11 +310,6 @@
</Expander>
</DataTemplate>
<!-- Template for dropdown category checkbox item -->
<DataTemplate DataType="{x:Type vm:CheckpointFolder}" x:Key="CategoryItemTemplate">
<CheckBox Content="{Binding Title}" IsChecked="{Binding IsCategoryEnabled, Mode=TwoWay}" />
</DataTemplate>
<!-- Template for dropdown box -->
<ControlTemplate x:Key="CategoryDropDownTemplate">
<Grid>
@ -362,9 +378,7 @@
VerticalAlignment="Center"
HorizontalAlignment="Right">
<DropDownButton.Flyout>
<MenuFlyout
ItemTemplate="{StaticResource CategoryItemTemplate}"
Placement="Bottom"
<ui:FAMenuFlyout
ItemsSource="{Binding CheckpointFolders}"/>
</DropDownButton.Flyout>
</DropDownButton>
@ -408,6 +422,10 @@
</StackPanel>
</Grid>
</ScrollViewer>
<!-- Overlay for draggable file panels -->
<Panel Name="OverlayPanel"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" />
</Grid>
</controls:UserControlBase>

22
StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml

@ -14,20 +14,11 @@
xmlns:octokit="clr-namespace:Octokit;assembly=Octokit"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
x:DataType="dialogs:InstallerViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="500"
d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
<controls:UserControlBase.Resources>
</controls:UserControlBase.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid RowDefinitions="Auto,Auto,*">
<StackPanel
Grid.Row="1"
Margin="16,8,16,0"
@ -52,9 +43,9 @@
TextWrapping="Wrap" />
</StackPanel>
<Grid Grid.Row="2" HorizontalAlignment="Left" ColumnDefinitions="Auto,Auto,*">
<Grid Grid.Row="2" HorizontalAlignment="Left" ColumnDefinitions="auto,*,auto">
<ListBox
Margin="16"
Margin="8,16"
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage, Mode=TwoWay}">
@ -86,8 +77,9 @@
</ListBox>
<StackPanel
MinWidth="400"
Grid.Column="1"
Margin="16,16,0,16"
Margin="8,16,0,16"
Orientation="Vertical">
<TextBlock
FontSize="24"
@ -221,7 +213,7 @@
<mdxaml:MarkdownScrollViewer
Grid.Column="2"
Margin="16"
Source="{Binding ReleaseNotes, Mode=OneWay}"/>
Markdown="{Binding ReleaseNotes, Mode=OneWay}"/>
<ContentPresenter
Grid.Column="0"
Grid.ColumnSpan="3"

172
StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml

@ -0,0 +1,172 @@
<controls:UserControlBase 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:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
d:DataContext="{x:Static mocks:DesignData.LaunchOptionsViewModel}"
x:DataType="dialogs:LaunchOptionsViewModel"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="650"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.LaunchOptionsDialog">
<controls:UserControlBase.Resources>
<converters:LaunchOptionConverter x:Key="LaunchOptionConverter" />
<converters:LaunchOptionIntDoubleConverter x:Key="LaunchOptionIntDoubleConverter" />
<converters:ValueConverterGroup x:Key="LaunchOptionIntToStringConverter">
<converters:LaunchOptionConverter />
<converters:LaunchOptionIntDoubleConverter />
</converters:ValueConverterGroup>
</controls:UserControlBase.Resources>
<Grid MinWidth="400" RowDefinitions="0.2*,0.8*" Margin="8">
<StackPanel
HorizontalAlignment="Stretch"
Spacing="4"
Margin="0,0,0,16"
Orientation="Vertical">
<!-- Title -->
<TextBlock
FontSize="24"
FontWeight="Bold"
Margin="16"
Text="{Binding Title}"
TextWrapping="Wrap" />
<!-- Search box -->
<TextBox
HorizontalAlignment="Stretch"
Margin="8,0"
Watermark="Search..."
Text="{Binding SearchText, Mode=TwoWay}"
VerticalAlignment="Top"
IsVisible="{Binding IsSearchBoxEnabled}"
x:Name="SearchBox">
<TextBox.InnerRightContent>
<ui:SymbolIcon Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
</StackPanel>
<!-- Option Cards -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl
HorizontalAlignment="Stretch"
Padding="8"
ItemsSource="{Binding FilteredCards}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<controls:LaunchOptionCardTemplateSelector>
<!-- Int type card (textboxes) -->
<DataTemplate x:DataType="models:LaunchOptionCard" x:Key="{x:Static models:LaunchOptionType.Int}">
<controls:Card Margin="0,8">
<StackPanel
HorizontalAlignment="Stretch"
Margin="8,0,8,0"
Orientation="Vertical">
<TextBlock
FontSize="16"
FontWeight="Bold"
Margin="0,8"
Text="{Binding Title}"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Options}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<Label Content="{Binding Name}" />
<ui:NumberBox
HorizontalAlignment="Stretch"
Margin="8"
PlaceholderText="{Binding DefaultValue, Mode=OneWay, Converter={StaticResource LaunchOptionConverter}}"
SpinButtonPlacementMode="Compact"
ValidationMode="Disabled"
Value="{Binding OptionValue, Converter={StaticResource LaunchOptionIntDoubleConverter}, Mode=TwoWay}"
VerticalAlignment="Stretch" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:Card>
</DataTemplate>
<!-- String type card (textboxes) -->
<DataTemplate DataType="{x:Type models:LaunchOptionCard}" x:Key="{x:Static models:LaunchOptionType.String}">
<controls:Card Margin="0,8">
<StackPanel
HorizontalAlignment="Stretch"
Margin="8,0,8,0"
Orientation="Vertical">
<TextBlock
FontSize="16"
FontWeight="Bold"
Margin="0,8"
Text="{Binding Title}"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Options}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<Label Content="{Binding Name}" />
<!--PlaceholderEnabled="{Binding HasDefaultValue}"-->
<TextBox
HorizontalAlignment="Stretch"
Margin="8"
Watermark="{Binding DefaultValue}"
Text="{Binding OptionValue, Converter={StaticResource LaunchOptionConverter}}"
VerticalAlignment="Stretch" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:Card>
</DataTemplate>
<!-- Bool type card (checkboxes) -->
<DataTemplate DataType="{x:Type models:LaunchOptionCard}" x:Key="{x:Static models:LaunchOptionType.Bool}">
<controls:Card Margin="0,8">
<StackPanel
HorizontalAlignment="Left"
Margin="8,0,8,0"
Orientation="Vertical">
<TextBlock
FontSize="16"
FontWeight="Bold"
Margin="0,8"
Text="{Binding Title}"
TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Options}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox
Content="{Binding Name}"
IsChecked="{Binding OptionValue, Converter={StaticResource LaunchOptionConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</controls:Card>
</DataTemplate>
</controls:LaunchOptionCardTemplateSelector>
</ItemsControl.DataTemplates>
</ItemsControl>
</ScrollViewer>
</Grid>
</controls:UserControlBase>

18
StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs

@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
public partial class LaunchOptionsDialog : UserControl
{
public LaunchOptionsDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

1
StabilityMatrix.Avalonia/Views/LaunchPageView.axaml

@ -47,6 +47,7 @@
Grid.Column="1"
Width="48"
Margin="8,8,0,0"
Command="{Binding ConfigCommand}"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
FontSize="16">

5
StabilityMatrix.Core/Helper/Cache/LRUCache.cs

@ -1,8 +1,9 @@
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace StabilityMatrix.Core.Helper.Cache;
// ReSharper disable once InconsistentNaming
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class LRUCache<TK,TV> where TK : notnull
{
private readonly int capacity;

39
StabilityMatrix.Core/Models/LaunchOption.cs

@ -6,9 +6,9 @@ namespace StabilityMatrix.Core.Models;
public class LaunchOption
{
public string Name { get; init; }
public LaunchOptionType Type { get; init; }
public required string Name { get; init; }
public LaunchOptionType Type { get; init; } = LaunchOptionType.Bool;
[JsonIgnore]
public object? DefaultValue { get; init; }
@ -19,29 +19,36 @@ public class LaunchOption
[JsonConverter(typeof(LaunchOptionValueJsonConverter))]
public object? OptionValue { get; set; }
/// <summary>
/// Checks if the option has no user entered value,
/// or that the value is the same as the default value.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public bool IsEmptyOrDefault()
{
switch (Type)
return Type switch
{
case LaunchOptionType.Bool:
return OptionValue == null;
case LaunchOptionType.Int:
return OptionValue == null || (int?) OptionValue == (int?) DefaultValue;
case LaunchOptionType.String:
return OptionValue == null || (string?) OptionValue == (string?) DefaultValue;
default:
throw new ArgumentOutOfRangeException();
}
LaunchOptionType.Bool => OptionValue == null,
LaunchOptionType.Int => OptionValue == null ||
(int?) OptionValue == (int?) DefaultValue,
LaunchOptionType.String => OptionValue == null ||
(string?) OptionValue == (string?) DefaultValue,
_ => throw new ArgumentOutOfRangeException()
};
}
public void SetValueFromString(string? value)
/// <summary>
/// Parses a string value to the correct type for the option.
/// This returned object can be assigned to OptionValue.
/// </summary>
public static object? ParseValue(string? value, LaunchOptionType type)
{
OptionValue = Type switch
return type switch
{
LaunchOptionType.Bool => bool.TryParse(value, out var boolValue) ? boolValue : null,
LaunchOptionType.Int => int.TryParse(value, out var intValue) ? intValue : null,
LaunchOptionType.String => value,
_ => throw new ArgumentException($"Unknown option type {Type}")
_ => throw new ArgumentException($"Unknown option type {type}")
};
}

129
StabilityMatrix.Core/Models/LaunchOptionCard.cs

@ -1,34 +1,125 @@
using System.Collections.ObjectModel;
using System.Collections.Immutable;
using System.Diagnostics;
namespace StabilityMatrix.Core.Models;
public class LaunchOptionCard
public readonly record struct LaunchOptionCard
{
public string Title { get; set; }
public LaunchOptionType Type { get; set; }
public string? Description { get; set; }
public ObservableCollection<LaunchOption> Options { get; set; } = new();
public LaunchOptionCard(string title, LaunchOptionType type = LaunchOptionType.Bool)
public required string Title { get; init; }
public required LaunchOptionType Type { get; init; }
public required IReadOnlyList<LaunchOption> Options { get; init; }
public string? Description { get; init; }
public static LaunchOptionCard FromDefinition(LaunchOptionDefinition definition)
{
Title = title;
Type = type;
return new LaunchOptionCard
{
Title = definition.Name,
Description = definition.Description,
Type = definition.Type,
Options = definition.Options.Select(s =>
{
var option = new LaunchOption
{
Name = s,
Type = definition.Type,
DefaultValue = definition.DefaultValue
};
return option;
}).ToImmutableArray()
};
}
public LaunchOptionCard(LaunchOptionDefinition definition)
/// <summary>
/// Yield LaunchOptionCards given definitions and launch args to load
/// </summary>
/// <param name="definitions"></param>
/// <param name="launchArgs"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static IEnumerable<LaunchOptionCard> FromDefinitions(
IEnumerable<LaunchOptionDefinition> definitions,
IEnumerable<LaunchOption> launchArgs)
{
Title = definition.Name;
Description = definition.Description;
Type = definition.Type;
foreach (var optionName in definition.Options)
// During card creation, store dict of options with initial values
var initialOptions = new Dictionary<string, object>();
// Dict of
var launchArgsDict = launchArgs.ToDictionary(launchArg => launchArg.Name);
// Create cards
foreach (var definition in definitions)
{
var option = new LaunchOption
// Check that non-bool types have exactly one option
if (definition.Type != LaunchOptionType.Bool && definition.Options.Count != 1)
{
throw new InvalidOperationException(
$"Definition: '{definition.Name}' has {definition.Options.Count} options," +
$" it must have exactly 1 option for non-bool types");
}
// Store initial values
if (definition.InitialValue != null)
{
// For bool types, initial value can be string (single/multiple options) or bool (single option)
if (definition.Type == LaunchOptionType.Bool)
{
// For single option, check bool
if (definition.Options.Count == 1 && definition.InitialValue is bool boolValue)
{
initialOptions[definition.Options.First()] = boolValue;
}
else
{
// For single/multiple options (string only)
var option = definition.Options.FirstOrDefault(opt => opt.Equals(definition.InitialValue));
if (option == null)
{
throw new InvalidOperationException(
$"Definition '{definition.Name}' has InitialValue of '{definition.InitialValue}', but it was not found in options:" +
$" '{string.Join(",", definition.Options)}'");
}
initialOptions[option] = true;
}
}
else
{
// Otherwise store initial value for first option
initialOptions[definition.Options.First()] = definition.InitialValue;
}
}
// Create the new card
var card = new LaunchOptionCard
{
Name = optionName,
Title = definition.Name,
Description = definition.Description,
Type = definition.Type,
DefaultValue = definition.DefaultValue
Options = definition.Options.Select(s =>
{
// Parse defaults and user loaded values here
var userOption = launchArgsDict.GetValueOrDefault(s);
var userValue = userOption?.OptionValue;
// If no user value, check set initial value
if (userValue is null)
{
var initialValue = initialOptions.GetValueOrDefault(s);
userValue ??= initialValue;
Debug.WriteLineIf(initialValue != null,
$"Using initial value {initialValue} for option {s}");
}
var option = new LaunchOption
{
Name = s,
Type = definition.Type,
DefaultValue = definition.DefaultValue,
OptionValue = userValue
};
return option;
}).ToImmutableArray()
};
Options.Add(option);
yield return card;
}
}
}

35
StabilityMatrix.Core/Models/LaunchOptionDefinition.cs

@ -5,16 +5,23 @@ namespace StabilityMatrix.Core.Models;
/// <summary>
/// Defines a launch option for a BasePackage.
/// </summary>
public class LaunchOptionDefinition
public record LaunchOptionDefinition
{
public string Name { get; init; }
/// <summary>
/// Name or title of the card.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Type of the option. "bool", "int", or "string"
/// - "bool" can supply 1 or more flags in the Options list (e.g. ["--api", "--lowvram"])
/// - "int" and "string" should supply a single flag in the Options list (e.g. ["--width"], ["--api"])
/// </summary>
public LaunchOptionType Type { get; init; } = LaunchOptionType.Bool;
public required LaunchOptionType Type { get; init; }
/// <summary>
/// Optional description of the option.
/// </summary>
public string? Description { get; init; }
/// <summary>
@ -27,23 +34,31 @@ public class LaunchOptionDefinition
/// Initial value for the option if no set value is available, set as the user value on save.
/// Use `DefaultValue` to provide a server-side default that is ignored for launch and saving.
/// </summary>
public object? InitialValue { get; set; }
public object? InitialValue { get; init; }
// Minimum number of selected options
public int? MinSelectedOptions { get; set; }
// Maximum number of selected options
public int? MaxSelectedOptions { get; set; }
/// <summary>
/// Minimum number of selected options (for validation)
/// </summary>
public int? MinSelectedOptions { get; init; }
/// <summary>
/// Maximum number of selected options (for validation)
/// </summary>
public int? MaxSelectedOptions { get; init; }
/// <summary>
/// List of option flags like "--api", "--lowvram", etc.
/// </summary>
public List<string> Options { get; set; }
public List<string> Options { get; init; } = new();
/// <summary>
/// Constant for the Extras launch option.
/// </summary>
[JsonIgnore]
public static LaunchOptionDefinition Extras => new()
{
Name = "Extra Launch Arguments",
Type = LaunchOptionType.String,
Options = new() {""}
Options = new List<string> {""}
};
}

5
StabilityMatrix.Core/Models/Packages/A3WebUI.cs

@ -67,6 +67,7 @@ public class A3WebUI : BaseGitPackage
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
@ -78,24 +79,28 @@ public class A3WebUI : BaseGitPackage
new()
{
Name = "Xformers",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--xformers" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() {"--api"}
},
new()
{
Name = "Skip Torch CUDA Check",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() {"--skip-torch-cuda-test"}
},
new()
{
Name = "Skip Python Version Check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() {"--skip-python-version-check"}
},

14
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -44,35 +44,39 @@ public class ComfyUI : BaseGitPackage
[SharedFolderType.Hypernetwork] = "models/hypernetworks",
};
public override List<LaunchOptionDefinition> LaunchOptions => new()
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition>
{
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--normalvram",
_ => null
},
Options = new() { "--highvram", "--normalvram", "--lowvram", "--novram" }
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
},
new()
{
Name = "Use CPU only",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() {"--cpu"}
Options = {"--cpu"}
},
new()
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() { "--disable-xformers" }
Options = { "--disable-xformers" }
},
new()
{
Name = "Auto-Launch",
Options = new() { "--auto-launch" }
Type = LaunchOptionType.Bool,
Options = { "--auto-launch" }
},
LaunchOptionDefinition.Extras
};

7
StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

@ -69,6 +69,7 @@ public class VladAutomatic : BaseGitPackage
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
@ -80,22 +81,26 @@ public class VladAutomatic : BaseGitPackage
new()
{
Name = "Force use of Intel OneAPI XPU backend",
Type = LaunchOptionType.Bool,
Options = new() { "--use-ipex" }
},
new()
{
Name = "Use DirectML if no compatible GPU is detected",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu() && HardwareHelper.HasAmdGpu(),
Options = new() { "--use-directml" }
},
new()
{
Name = "Force use of Nvidia CUDA backend",
Type = LaunchOptionType.Bool,
Options = new() { "--use-cuda" }
},
new()
{
Name = "Force use of AMD ROCm backend",
Type = LaunchOptionType.Bool,
Options = new() { "--use-rocm" }
},
new()
@ -107,11 +112,13 @@ public class VladAutomatic : BaseGitPackage
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = new() { "--api" }
},
new()
{
Name = "Debug Logging",
Type = LaunchOptionType.Bool,
Options = new() { "--debug" }
},
LaunchOptionDefinition.Extras

Loading…
Cancel
Save