Browse Source

Merge pull request #31 from ionite34/launch-options

pull/5/head
Ionite 1 year ago committed by GitHub
parent
commit
f0db227177
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      StabilityMatrix.Tests/Helper/EventManagerTests.cs
  2. 43
      StabilityMatrix.Tests/Helper/PackageFactoryTests.cs
  3. 25
      StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
  4. 1
      StabilityMatrix.Tests/Usings.cs
  5. 6
      StabilityMatrix.sln
  6. 20
      StabilityMatrix/App.xaml.cs
  7. 66
      StabilityMatrix/Helper/DialogErrorHandler.cs
  8. 34
      StabilityMatrix/Helper/DialogFactory.cs
  9. 18
      StabilityMatrix/Helper/IDialogErrorHandler.cs
  10. 8
      StabilityMatrix/Helper/IDialogFactory.cs
  11. 6
      StabilityMatrix/Helper/ISettingsManager.cs
  12. 19
      StabilityMatrix/Helper/SettingsManager.cs
  13. 47
      StabilityMatrix/IPyRunner.cs
  14. 59
      StabilityMatrix/LaunchOptionsDialog.xaml
  15. 29
      StabilityMatrix/LaunchOptionsDialog.xaml.cs
  16. 22
      StabilityMatrix/LaunchPage.xaml
  17. 8
      StabilityMatrix/MainWindow.xaml
  18. 3
      StabilityMatrix/MainWindow.xaml.cs
  19. 2
      StabilityMatrix/Models/BasePackage.cs
  20. 3
      StabilityMatrix/Models/InstalledPackage.cs
  21. 10
      StabilityMatrix/Models/LaunchOption.cs
  22. 24
      StabilityMatrix/Models/LaunchOptionCard.cs
  23. 17
      StabilityMatrix/Models/LaunchOptionDefinition.cs
  24. 20
      StabilityMatrix/Models/Packages/A3WebUI.cs
  25. 22
      StabilityMatrix/Models/Packages/DankDiffusion.cs
  26. 11
      StabilityMatrix/Models/TaskResult.cs
  27. 2
      StabilityMatrix/PyIOStream.cs
  28. 40
      StabilityMatrix/PyRunner.cs
  29. 10
      StabilityMatrix/ViewModels/InstallerViewModel.cs
  30. 58
      StabilityMatrix/ViewModels/LaunchOptionsDialogViewModel.cs
  31. 95
      StabilityMatrix/ViewModels/LaunchViewModel.cs
  32. 18
      StabilityMatrix/ViewModels/SettingsViewModel.cs
  33. 32
      StabilityMatrix/ViewModels/SnackbarViewModel.cs

43
StabilityMatrix.Tests/Helper/EventManagerTests.cs

@ -0,0 +1,43 @@
using StabilityMatrix.Helper;
namespace StabilityMatrix.Tests.Helper;
[TestClass]
public class EventManagerTests
{
private EventManager eventManager;
[TestInitialize]
public void TestInitialize()
{
eventManager = EventManager.Instance;
}
[TestMethod]
public void GlobalProgressChanged_ShouldBeInvoked()
{
// Arrange
var progress = 0;
eventManager.GlobalProgressChanged += (sender, args) => progress = args;
// Act
eventManager.OnGlobalProgressChanged(100);
// Assert
Assert.AreEqual(100, progress);
}
[TestMethod]
public void RequestPageChange_ShouldBeInvoked()
{
// Arrange
var pageType = typeof(object);
eventManager.PageChangeRequested += (sender, args) => pageType = args;
// Act
eventManager.RequestPageChange(typeof(int));
// Assert
Assert.AreEqual(typeof(int), pageType);
}
}

43
StabilityMatrix.Tests/Helper/PackageFactoryTests.cs

@ -0,0 +1,43 @@
using StabilityMatrix.Helper;
using StabilityMatrix.Models;
using StabilityMatrix.Models.Packages;
namespace StabilityMatrix.Tests.Helper;
[TestClass]
public class PackageFactoryTests
{
private PackageFactory packageFactory;
private IEnumerable<BasePackage> fakeBasePackages;
[TestInitialize]
public void Setup()
{
fakeBasePackages = new List<BasePackage>
{
new DankDiffusion()
};
packageFactory = new PackageFactory(fakeBasePackages);
}
[TestMethod]
public void GetAllAvailablePackages_ReturnsAllPackages()
{
var result = packageFactory.GetAllAvailablePackages();
Assert.AreEqual(1, result.Count());
}
[TestMethod]
public void FindPackageByName_ReturnsPackage()
{
var result = packageFactory.FindPackageByName("dank-diffusion");
Assert.IsNotNull(result);
}
[TestMethod]
public void FindPackageByName_ReturnsNull()
{
var result = packageFactory.FindPackageByName("not-a-package");
Assert.IsNull(result);
}
}

25
StabilityMatrix.Tests/StabilityMatrix.Tests.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows10.0.17763.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNext" Version="4.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StabilityMatrix\StabilityMatrix.csproj" />
</ItemGroup>
</Project>

1
StabilityMatrix.Tests/Usings.cs

@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;

6
StabilityMatrix.sln

@ -5,6 +5,8 @@ VisualStudioVersion = 17.6.33717.318
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix", "StabilityMatrix\StabilityMatrix.csproj", "{7CA2E862-B121-495D-8CCC-2D6EF56A3312}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Tests", "StabilityMatrix.Tests\StabilityMatrix.Tests.csproj", "{63EF4330-CCFF-4677-B14C-1A700CD81FDA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Release|Any CPU.Build.0 = Release|Any CPU
{63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

20
StabilityMatrix/App.xaml.cs

@ -38,22 +38,31 @@ namespace StabilityMatrix
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPageService, PageService>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<ISnackbarService, SnackbarService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<IPyRunner, PyRunner>();
serviceCollection.AddTransient<IDialogFactory, DialogFactory>();
serviceCollection.AddTransient<MainWindow>();
serviceCollection.AddTransient<SettingsPage>();
serviceCollection.AddTransient<LaunchPage>();
serviceCollection.AddTransient<InstallPage>();
serviceCollection.AddTransient<TextToImagePage>();
serviceCollection.AddTransient<MainWindowViewModel>();
serviceCollection.AddTransient<SnackbarViewModel>();
serviceCollection.AddTransient<LaunchOptionsDialogViewModel>();
serviceCollection.AddSingleton<SettingsViewModel>();
serviceCollection.AddSingleton<LaunchViewModel>();
serviceCollection.AddSingleton<InstallerViewModel>();
serviceCollection.AddSingleton<TextToImageViewModel>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<BasePackage, A3WebUI>();
serviceCollection.AddSingleton<BasePackage, DankDiffusion>();
serviceCollection.AddSingleton<ISnackbarService, SnackbarService>();
serviceCollection.AddSingleton<ISettingsManager, SettingsManager>();
serviceCollection.AddSingleton<IDialogErrorHandler, DialogErrorHandler>();
var jsonOptions = new JsonSerializerOptions
{
@ -70,10 +79,15 @@ namespace StabilityMatrix
client.BaseAddress = new Uri("http://localhost:7860");
});
// Logging configuration
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");
var logConfig = new NLog.Config.LoggingConfiguration();
// File logging
var fileTarget = new NLog.Targets.FileTarget("logfile") { FileName = logPath };
logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, fileTarget);
// Log trace+ to debug console
var debugTarget = new NLog.Targets.DebuggerTarget("debugger") { Layout = "${message}" };
logConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, fileTarget);
logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, debugTarget);
NLog.LogManager.Configuration = logConfig;
serviceCollection.AddLogging(log =>

66
StabilityMatrix/Helper/DialogErrorHandler.cs

@ -0,0 +1,66 @@
using System.Threading.Tasks;
using System;
using System.Windows.Threading;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Models;
using StabilityMatrix.ViewModels;
using Wpf.Ui.Common;
using Wpf.Ui.Contracts;
using Wpf.Ui.Controls;
using Wpf.Ui.Controls.IconElements;
namespace StabilityMatrix.Helper;
/// <summary>
/// Generic recoverable error handler using content dialogs.
/// </summary>
public class DialogErrorHandler : IDialogErrorHandler
{
private readonly ISnackbarService snackbarService;
private readonly SnackbarViewModel snackbarViewModel;
public DialogErrorHandler(ISnackbarService snackbarService, SnackbarViewModel snackbarViewModel)
{
this.snackbarService = snackbarService;
this.snackbarViewModel = snackbarViewModel;
}
/// <summary>
/// Shows a generic error snackbar with the given message.
/// </summary>
public void ShowSnackbarAsync(string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000)
{
snackbarViewModel.SnackbarAppearance = level switch
{
LogLevel.Error => ControlAppearance.Danger,
LogLevel.Warning => ControlAppearance.Caution,
LogLevel.Information => ControlAppearance.Info,
_ => ControlAppearance.Secondary
};
snackbarService.Timeout = timeoutMilliseconds;
var icon = new SymbolIcon(SymbolRegular.ErrorCircle24);
snackbarService.ShowAsync("Error", message, icon, snackbarViewModel.SnackbarAppearance);
}
/// <summary>
/// Attempt to run the given action, showing a generic error snackbar if it fails.
/// </summary>
public async Task<TaskResult<T>> TryAsync<T>(Task<T> task, string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000)
{
try
{
return new TaskResult<T>
{
Result = await task
};
}
catch (Exception e)
{
ShowSnackbarAsync(message, level, timeoutMilliseconds);
return new TaskResult<T>
{
Exception = e
};
}
}
}

34
StabilityMatrix/Helper/DialogFactory.cs

@ -0,0 +1,34 @@
using System;
using StabilityMatrix.Models;
using StabilityMatrix.ViewModels;
using Wpf.Ui.Contracts;
namespace StabilityMatrix.Helper;
public class DialogFactory : IDialogFactory
{
private readonly IContentDialogService contentDialogService;
private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel;
private readonly ISettingsManager settingsManager;
public DialogFactory(IContentDialogService contentDialogService, LaunchOptionsDialogViewModel launchOptionsDialogViewModel, ISettingsManager settingsManager)
{
this.contentDialogService = contentDialogService;
this.launchOptionsDialogViewModel = launchOptionsDialogViewModel;
this.settingsManager = settingsManager;
}
public LaunchOptionsDialog CreateLaunchOptionsDialog(BasePackage selectedPackage, InstalledPackage installedPackage)
{
var definitions = selectedPackage.LaunchOptions;
launchOptionsDialogViewModel.SelectedPackage = selectedPackage;
launchOptionsDialogViewModel.Cards.Clear();
// Create cards
launchOptionsDialogViewModel.CardsFromDefinitions(definitions);
// Load user settings
var userLaunchArgs = settingsManager.GetLaunchArgs(installedPackage.Id);
launchOptionsDialogViewModel.LoadFromLaunchArgs(userLaunchArgs);
return new LaunchOptionsDialog(contentDialogService, launchOptionsDialogViewModel);
}
}

18
StabilityMatrix/Helper/IDialogErrorHandler.cs

@ -0,0 +1,18 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Models;
namespace StabilityMatrix.Helper;
public interface IDialogErrorHandler
{
/// <summary>
/// Shows a generic error snackbar with the given message.
/// </summary>
void ShowSnackbarAsync(string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000);
/// <summary>
/// Attempt to run the given action, showing a generic error snackbar if it fails.
/// </summary>
Task<TaskResult<T>> TryAsync<T>(Task<T> task, string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000);
}

8
StabilityMatrix/Helper/IDialogFactory.cs

@ -0,0 +1,8 @@
using StabilityMatrix.Models;
namespace StabilityMatrix.Helper;
public interface IDialogFactory
{
LaunchOptionsDialog CreateLaunchOptionsDialog(BasePackage selectedPackage, InstalledPackage installedPackage);
}

6
StabilityMatrix/Helper/ISettingsManager.cs

@ -1,4 +1,6 @@
using StabilityMatrix.Models;
using System;
using System.Collections.Generic;
using StabilityMatrix.Models;
namespace StabilityMatrix.Helper;
@ -12,4 +14,6 @@ public interface ISettingsManager
void SetHasInstalledVenv(bool hasInstalledVenv);
void SetNavExpanded(bool navExpanded);
void UpdatePackageVersionNumber(string packageName, string newVersion);
List<string> GetLaunchArgs(Guid packageId);
void SaveLaunchArgs(Guid packageId, List<string> launchArgs);
}

19
StabilityMatrix/Helper/SettingsManager.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
@ -87,6 +88,24 @@ public class SettingsManager : ISettingsManager
SaveSettings();
}
public List<string> GetLaunchArgs(Guid packageId)
{
var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId);
return packageData?.LaunchArgs ?? new List<string>();
}
public void SaveLaunchArgs(Guid packageId, List<string> launchArgs)
{
var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId);
if (packageData == null)
{
return;
}
packageData.LaunchArgs = launchArgs;
SaveSettings();
}
private void LoadSettings()
{
var settingsContent = File.ReadAllText(SettingsPath);

47
StabilityMatrix/IPyRunner.cs

@ -0,0 +1,47 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix;
public interface IPyRunner
{
/// <summary>
/// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization.
/// </summary>
/// <exception cref="FileNotFoundException">Thrown if Python DLL not found.</exception>
Task Initialize();
/// <summary>
/// One-time setup for get-pip
/// </summary>
Task SetupPip();
/// <summary>
/// Install a Python package with pip
/// </summary>
Task InstallPackage(string package);
/// <summary>
/// Evaluate Python expression and return its value as a string
/// </summary>
/// <param name="expression"></param>
Task<string> Eval(string expression);
/// <summary>
/// Evaluate Python expression and return its value
/// </summary>
/// <param name="expression"></param>
Task<T> Eval<T>(string expression);
/// <summary>
/// Execute Python code without returning a value
/// </summary>
/// <param name="code"></param>
Task Exec(string code);
/// <summary>
/// Return the Python version as a PyVersionInfo struct
/// </summary>
Task<PyVersionInfo> GetVersionInfo();
}

59
StabilityMatrix/LaunchOptionsDialog.xaml

@ -0,0 +1,59 @@
<ui:ContentDialog
CloseButtonText="Close"
DialogHeight="600"
DialogWidth="600"
Loaded="LaunchOptionsDialog_OnLoaded"
Title="Launch Options"
d:DataContext="{d:DesignInstance Type=viewModels:LaunchOptionsDialogViewModel,
IsDesignTimeCreatable=True}"
d:DesignHeight="512"
d:DesignWidth="512"
mc:Ignorable="d"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
x:Class="StabilityMatrix.LaunchOptionsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StabilityMatrix"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:StabilityMatrix.Models"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ui:ContentDialog.Resources>
<Style BasedOn="{StaticResource {x:Type ui:ContentDialog}}" TargetType="{x:Type local:LaunchOptionsDialog}" />
<DataTemplate DataType="{x:Type models:LaunchOptionCard}" x:Key="LaunchOptionCardDataTemplate">
<ui:Card Margin="16,8,8,8">
<StackPanel
HorizontalAlignment="Left"
Margin="8,0,8,0"
Orientation="Vertical">
<TextBlock
FontSize="16"
FontWeight="Bold"
Margin="0,8"
Text="{Binding Title}" />
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Options}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ui:Card>
</DataTemplate>
</ui:ContentDialog.Resources>
<Grid Height="700" Width="700">
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!-- Options cards -->
<ItemsControl ItemTemplate="{StaticResource LaunchOptionCardDataTemplate}" ItemsSource="{Binding Cards}" />
</StackPanel>
</ScrollViewer>
</Grid>
</ui:ContentDialog>

29
StabilityMatrix/LaunchOptionsDialog.xaml.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using StabilityMatrix.ViewModels;
using Wpf.Ui.Contracts;
using Wpf.Ui.Controls.ContentDialogControl;
namespace StabilityMatrix;
public partial class LaunchOptionsDialog : ContentDialog
{
private readonly LaunchOptionsDialogViewModel viewModel;
public List<string> AsLaunchArgs() => viewModel.AsLaunchArgs();
public LaunchOptionsDialog(IContentDialogService dialogService, LaunchOptionsDialogViewModel viewModel) : base(
dialogService.GetContentPresenter())
{
this.viewModel = viewModel;
InitializeComponent();
DataContext = viewModel;
}
private void LaunchOptionsDialog_OnLoaded(object sender, RoutedEventArgs e)
{
viewModel.OnLoad();
}
}

22
StabilityMatrix/LaunchPage.xaml

@ -116,10 +116,16 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.8*" />
<ColumnDefinition Width="0.2*" />
</Grid.ColumnDefinitions>
<ui:Button
Background="Green"
Command="{Binding LaunchCommand}"
Content="Launch"
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Left"
Margin="8,8,0,0"
@ -131,6 +137,7 @@
Background="DarkRed"
Command="{Binding StopCommand}"
Content="Stop"
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Left"
Margin="8,8,0,0"
@ -138,6 +145,21 @@
Visibility="{Binding StopButtonVisibility, FallbackValue=Hidden}"
Width="72"
x:Name="StopButton" />
<ui:Button
Command="{Binding ConfigCommand}"
FontSize="16"
Grid.Column="1"
Grid.Row="0"
HorizontalAlignment="Left"
Margin="8,8,0,0"
VerticalAlignment="Stretch"
Width="48"
x:Name="ConfigButton">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="Settings32" />
</ui:Button.Icon>
</ui:Button>
</Grid>
<ComboBox
Grid.Column="1"
Grid.Row="0"

8
StabilityMatrix/MainWindow.xaml

@ -21,8 +21,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ui:FluentWindow.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="{Binding ProgressState}"
ProgressValue="{Binding ProgressValue}"/>
<TaskbarItemInfo ProgressState="{Binding ProgressState}" ProgressValue="{Binding ProgressValue}" />
</ui:FluentWindow.TaskbarItemInfo>
<Grid>
@ -75,6 +74,11 @@
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationView.FooterMenuItems>
<ui:NavigationView.ContentOverlay>
<Grid>
<ui:Snackbar x:Name="RootSnackbar" />
</Grid>
</ui:NavigationView.ContentOverlay>
</ui:NavigationView>
</Grid>
<Grid Grid.Row="1" Visibility="{Binding SimpleModeVisibility, FallbackValue=Hidden}">

3
StabilityMatrix/MainWindow.xaml.cs

@ -19,7 +19,7 @@ namespace StabilityMatrix
private readonly ISettingsManager settingsManager;
public MainWindow(IPageService pageService, IContentDialogService contentDialogService,
MainWindowViewModel mainWindowViewModel, ISettingsManager settingsManager)
MainWindowViewModel mainWindowViewModel, ISettingsManager settingsManager, ISnackbarService snackbarService)
{
InitializeComponent();
@ -31,6 +31,7 @@ namespace StabilityMatrix
RootNavigation.Navigating += (_, _) => Debug.WriteLine("Navigating");
RootNavigation.SetPageService(pageService);
snackbarService.SetSnackbarControl(RootSnackbar);
contentDialogService.SetContentPresenter(RootContentDialog);
EventManager.Instance.PageChangeRequested += InstanceOnPageChangeRequested;

2
StabilityMatrix/Models/BasePackage.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StabilityMatrix.Models;
@ -18,6 +19,7 @@ public abstract class BasePackage
public abstract Task Shutdown();
public abstract Task<bool> CheckForUpdates();
public abstract Task<string?> Update();
public abstract List<LaunchOptionDefinition> LaunchOptions { get; }
internal virtual string DownloadLocation => $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages\\{Name}.zip";
internal virtual string InstallLocation => $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages\\{Name}";

3
StabilityMatrix/Models/InstalledPackage.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace StabilityMatrix.Models;
@ -17,6 +18,6 @@ public class InstalledPackage
public string? PackageVersion { get; set; }
// Install path
public string? Path { get; set; }
public string? LaunchCommand { get; set; }
public List<string>? LaunchArgs { get; set; }
}

10
StabilityMatrix/Models/LaunchOption.cs

@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.Models;
public partial class LaunchOption : ObservableObject
{
public string Name { get; set; }
[ObservableProperty] private bool selected = false;
}

24
StabilityMatrix/Models/LaunchOptionCard.cs

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Documents;
using ABI.Windows.Data.Xml.Dom;
using StabilityMatrix.Helper;
namespace StabilityMatrix.Models;
public class LaunchOptionCard
{
public string Title { get; set; }
public string? Description { get; set; }
public ObservableCollection<LaunchOption> Options { get; set; } = new();
public LaunchOptionCard(LaunchOptionDefinition definition)
{
Title = definition.Name;
foreach (var optionName in definition.Options)
{
var option = new LaunchOption {Name = optionName};
Options.Add(option);
}
}
}

17
StabilityMatrix/Models/LaunchOptionDefinition.cs

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace StabilityMatrix.Models;
/// <summary>
/// Defines a launch option for a BasePackage.
/// </summary>
public class LaunchOptionDefinition
{
public string Name { get; set; }
// Minimum number of selected options
public int? MinSelectedOptions { get; set; }
// Maximum number of selected options
public int? MaxSelectedOptions { get; set; }
// List of option flags like "--api", "--lowvram", etc.
public List<string> Options { get; set; }
}

20
StabilityMatrix/Models/Packages/A3WebUI.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@ -28,6 +29,25 @@ public class A3WebUI : BasePackage
public override bool UpdateAvailable { get; set; } = false;
public override List<LaunchOptionDefinition> LaunchOptions => new()
{
new LaunchOptionDefinition
{
Name = "API",
Options = new List<string> { "--api" }
},
new LaunchOptionDefinition
{
Name = "VRAM",
Options = new List<string> { "--lowvram", "--medvram" }
},
new LaunchOptionDefinition
{
Name = "Xformers",
Options = new List<string> { "--xformers" }
}
};
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private string webUrl = string.Empty;

22
StabilityMatrix/Models/Packages/DankDiffusion.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StabilityMatrix.Models.Packages;
@ -10,6 +11,25 @@ public class DankDiffusion : BasePackage
public override string GithubUrl => "https://github.com/mohnjiles/dank-diffusion";
public override string LaunchCommand => "";
public override List<LaunchOptionDefinition> LaunchOptions => new()
{
new LaunchOptionDefinition
{
Name = "API",
Options = new List<string> { "--api" }
},
new LaunchOptionDefinition
{
Name = "VRAM",
Options = new List<string> { "--lowvram", "--medvram" }
},
new LaunchOptionDefinition
{
Name = "Xformers",
Options = new List<string> { "--xformers" }
}
};
public override Task<string?> DownloadPackage(bool isUpdate = false)
{
throw new System.NotImplementedException();

11
StabilityMatrix/Models/TaskResult.cs

@ -0,0 +1,11 @@
using System;
namespace StabilityMatrix.Models;
public class TaskResult<T>
{
public T? Result { get; set; }
public Exception? Exception { get; set; }
public bool IsSuccessful => Exception is null && Result != null;
}

2
StabilityMatrix/PyIOStream.cs

@ -10,7 +10,7 @@ namespace StabilityMatrix;
/// Implement the interface of the sys.stdout redirection
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal class PyIOStream
public class PyIOStream
{
private readonly StringBuilder TextBuilder;
private readonly StringWriter TextWriter;

40
StabilityMatrix/PyRunner.cs

@ -2,17 +2,19 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NLog;
using Python.Runtime;
using StabilityMatrix.Helper;
using ILogger = NLog.ILogger;
namespace StabilityMatrix;
internal record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial);
public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial);
internal static class PyRunner
public class PyRunner : IPyRunner
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ILogger<PyRunner> logger;
private const string RelativeDllPath = @"Assets\Python310\python310.dll";
private const string RelativeExePath = @"Assets\Python310\python.exe";
@ -23,25 +25,31 @@ internal static class PyRunner
public static string PipExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativePipExePath);
public static string GetPipPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativeGetPipPath);
public static PyIOStream? StdOutStream;
public static PyIOStream? StdErrStream;
private static readonly SemaphoreSlim PyRunning = new(1, 1);
private PyIOStream? StdOutStream;
private PyIOStream? StdErrStream;
public PyRunner(ILogger<PyRunner> logger)
{
this.logger = logger;
}
/// <summary>
/// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization.
/// </summary>
/// <exception cref="FileNotFoundException">Thrown if Python DLL not found.</exception>
public static async Task Initialize()
public async Task Initialize()
{
if (PythonEngine.IsInitialized) return;
Logger.Trace($"Initializing Python runtime with DLL '{DllPath}'");
logger.LogInformation("Initializing Python runtime with DLL: {DllPath}", DllPath);
// Check PythonDLL exists
if (!File.Exists(DllPath))
{
logger.LogError("Python DLL not found");
throw new FileNotFoundException("Python DLL not found", DllPath);
}
Runtime.PythonDLL = DllPath;
@ -62,7 +70,7 @@ internal static class PyRunner
/// <summary>
/// One-time setup for get-pip
/// </summary>
public static async Task SetupPip()
public async Task SetupPip()
{
if (!File.Exists(GetPipPath))
{
@ -75,7 +83,7 @@ internal static class PyRunner
/// <summary>
/// Install a Python package with pip
/// </summary>
public static async Task InstallPackage(string package)
public async Task InstallPackage(string package)
{
if (!File.Exists(PipExePath))
{
@ -92,7 +100,7 @@ internal static class PyRunner
/// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param>
/// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception>
private static async Task<T> RunInThreadWithLock<T>(Func<T> func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
private async Task<T> RunInThreadWithLock<T>(Func<T> func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
{
// Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
@ -119,7 +127,7 @@ internal static class PyRunner
/// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param>
/// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception>
private static async Task RunInThreadWithLock(Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
private async Task RunInThreadWithLock(Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
{
// Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
@ -143,7 +151,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value as a string
/// </summary>
/// <param name="expression"></param>
public static async Task<string> Eval(string expression)
public async Task<string> Eval(string expression)
{
return await Eval<string>(expression);
}
@ -152,7 +160,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value
/// </summary>
/// <param name="expression"></param>
public static Task<T> Eval<T>(string expression)
public Task<T> Eval<T>(string expression)
{
return RunInThreadWithLock(() =>
{
@ -165,7 +173,7 @@ internal static class PyRunner
/// Execute Python code without returning a value
/// </summary>
/// <param name="code"></param>
public static Task Exec(string code)
public Task Exec(string code)
{
return RunInThreadWithLock(() =>
{
@ -176,7 +184,7 @@ internal static class PyRunner
/// <summary>
/// Return the Python version as a PyVersionInfo struct
/// </summary>
public static async Task<PyVersionInfo> GetVersionInfo()
public async Task<PyVersionInfo> GetVersionInfo()
{
var version = await RunInThreadWithLock(() =>
{

10
StabilityMatrix/ViewModels/InstallerViewModel.cs

@ -19,6 +19,7 @@ public partial class InstallerViewModel : ObservableObject
{
private readonly ILogger<InstallerViewModel> logger;
private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
@ -47,10 +48,11 @@ public partial class InstallerViewModel : ObservableObject
private bool updateAvailable;
public InstallerViewModel(ILogger<InstallerViewModel> logger, ISettingsManager settingsManager,
IPackageFactory packageFactory)
IPackageFactory packageFactory, IPyRunner pyRunner)
{
this.logger = logger;
this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
ProgressText = "shrug";
InstallButtonText = "Install";
@ -120,16 +122,16 @@ public partial class InstallerViewModel : ObservableObject
await InstallPackage();
ProgressText = "Installing dependencies...";
await PyRunner.Initialize();
await pyRunner.Initialize();
if (!settingsManager.Settings.HasInstalledPip)
{
await PyRunner.SetupPip();
await pyRunner.SetupPip();
settingsManager.SetHasInstalledPip(true);
}
if (!settingsManager.Settings.HasInstalledVenv)
{
await PyRunner.InstallPackage("virtualenv");
await pyRunner.InstallPackage("virtualenv");
settingsManager.SetHasInstalledVenv(true);
}

58
StabilityMatrix/ViewModels/LaunchOptionsDialogViewModel.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Models;
namespace StabilityMatrix.ViewModels;
public partial class LaunchOptionsDialogViewModel : ObservableObject
{
public ObservableCollection<LaunchOptionCard> Cards { get; set; } = new();
[ObservableProperty]
private BasePackage? selectedPackage;
/// <summary>
/// Export the current cards options to a list of strings
/// </summary>
public List<string> AsLaunchArgs()
{
return (
from card in Cards from option in card.Options
where option.Selected select option.Name).ToList();
}
/// <summary>
/// Create cards using definitions
/// </summary>
public void CardsFromDefinitions(List<LaunchOptionDefinition> definitions)
{
foreach (var definition in definitions)
{
Cards.Add(new LaunchOptionCard(definition));
}
}
/// <summary>
/// Import the current cards options from a list of strings
/// </summary>
public void LoadFromLaunchArgs(IEnumerable<string> launchArgs)
{
var launchArgsSet = new HashSet<string>(launchArgs);
foreach (var card in Cards)
{
foreach (var option in card.Options)
{
option.Selected = launchArgsSet.Contains(option.Name);
}
}
}
public void OnLoad()
{
Debug.WriteLine("In LaunchOptions OnLoad");
}
}

95
StabilityMatrix/ViewModels/LaunchViewModel.cs

@ -7,9 +7,12 @@ using System.Windows;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Uwp.Notifications;
using StabilityMatrix.Helper;
using StabilityMatrix.Models;
using Wpf.Ui.Contracts;
using Wpf.Ui.Controls.ContentDialogControl;
namespace StabilityMatrix.ViewModels;
@ -17,23 +20,26 @@ public partial class LaunchViewModel : ObservableObject
{
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly IContentDialogService contentDialogService;
private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel;
private readonly ILogger<LaunchViewModel> logger;
private readonly IPyRunner pyRunner;
private readonly IDialogFactory dialogFactory;
private BasePackage? runningPackage;
private bool clearingPackages = false;
[ObservableProperty]
private string consoleInput = "";
[ObservableProperty] private string consoleInput = "";
[ObservableProperty]
private string consoleOutput = "";
[ObservableProperty] private string consoleOutput = "";
[ObservableProperty]
private Visibility launchButtonVisibility;
[ObservableProperty] private Visibility launchButtonVisibility;
[ObservableProperty]
private Visibility stopButtonVisibility;
[ObservableProperty] private Visibility stopButtonVisibility;
private InstalledPackage? selectedPackage;
public InstalledPackage? SelectedPackage
{
get => selectedPackage;
@ -51,13 +57,23 @@ public partial class LaunchViewModel : ObservableObject
}
}
[ObservableProperty]
private ObservableCollection<InstalledPackage> installedPackages = new();
[ObservableProperty] private ObservableCollection<InstalledPackage> installedPackages = new();
public event EventHandler? ScrollNeeded;
public LaunchViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory)
public LaunchViewModel(ISettingsManager settingsManager,
IPackageFactory packageFactory,
IContentDialogService contentDialogService,
LaunchOptionsDialogViewModel launchOptionsDialogViewModel,
ILogger<LaunchViewModel> logger,
IPyRunner pyRunner,
IDialogFactory dialogFactory)
{
this.pyRunner = pyRunner;
this.dialogFactory = dialogFactory;
this.contentDialogService = contentDialogService;
this.launchOptionsDialogViewModel = launchOptionsDialogViewModel;
this.logger = logger;
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
SetProcessRunning(false);
@ -69,7 +85,7 @@ public partial class LaunchViewModel : ObservableObject
{
if (e.Argument.StartsWith("http"))
{
Process.Start(new ProcessStartInfo(e.Argument) { UseShellExecute = true });
Process.Start(new ProcessStartInfo(e.Argument) {UseShellExecute = true});
}
}
@ -84,7 +100,7 @@ public partial class LaunchViewModel : ObservableObject
return;
}
await PyRunner.Initialize();
await pyRunner.Initialize();
// Get path from package
var packagePath = SelectedPackage.Path!;
@ -102,6 +118,39 @@ public partial class LaunchViewModel : ObservableObject
SetProcessRunning(true);
});
[RelayCommand]
public async Task ConfigAsync()
{
var activeInstall = SelectedPackage;
var name = activeInstall?.Name;
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;
}
// Open a config page
var dialog = dialogFactory.CreateLaunchOptionsDialog(package, activeInstall);
dialog.IsPrimaryButtonEnabled = true;
dialog.PrimaryButtonText = "Save";
dialog.CloseButtonText = "Cancel";
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
// Save config
var args = dialog.AsLaunchArgs();
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
}
private void RunningPackageOnStartupComplete(object? sender, string url)
{
new ToastContentBuilder()
@ -113,16 +162,20 @@ public partial class LaunchViewModel : ObservableObject
public void OnLoaded()
{
LoadPackages();
if (InstalledPackages.Any() && settingsManager.Settings.ActiveInstalledPackage != null)
lock (InstalledPackages)
{
SelectedPackage =
InstalledPackages[
InstalledPackages.IndexOf(InstalledPackages.FirstOrDefault(x =>
x.Id == settingsManager.Settings.ActiveInstalledPackage))];
// Skip if no packages
if (!InstalledPackages.Any())
{
logger.LogTrace($"No packages for {nameof(LaunchViewModel)}");
return;
}
else if (InstalledPackages.Any())
var activePackageId = settingsManager.Settings.ActiveInstalledPackage;
if (activePackageId != null)
{
SelectedPackage = InstalledPackages[0];
SelectedPackage = InstalledPackages.FirstOrDefault(
x => x.Id == activePackageId) ?? InstalledPackages[0];
}
}
}
@ -140,6 +193,7 @@ public partial class LaunchViewModel : ObservableObject
runningPackage.ConsoleOutput -= OnConsoleOutput;
runningPackage.Exited -= OnExit;
}
runningPackage?.Shutdown();
runningPackage = null;
SetProcessRunning(false);
@ -162,6 +216,7 @@ public partial class LaunchViewModel : ObservableObject
{
InstalledPackages.Add(package);
}
clearingPackages = false;
}

18
StabilityMatrix/ViewModels/SettingsViewModel.cs

@ -17,6 +17,8 @@ namespace StabilityMatrix.ViewModels;
public partial class SettingsViewModel : ObservableObject
{
private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
private readonly IDialogErrorHandler dialogErrorHandler;
public ObservableCollection<string> AvailableThemes => new()
{
@ -27,11 +29,13 @@ public partial class SettingsViewModel : ObservableObject
private readonly IContentDialogService contentDialogService;
private readonly IA3WebApi a3WebApi;
public SettingsViewModel(ISettingsManager settingsManager, IContentDialogService contentDialogService, IA3WebApi a3WebApi)
public SettingsViewModel(ISettingsManager settingsManager, IContentDialogService contentDialogService, IA3WebApi a3WebApi, IPyRunner pyRunner, IDialogErrorHandler dialogErrorHandler)
{
this.settingsManager = settingsManager;
this.contentDialogService = contentDialogService;
this.dialogErrorHandler = dialogErrorHandler;
this.a3WebApi = a3WebApi;
this.pyRunner = pyRunner;
SelectedTheme = settingsManager.Settings.Theme ?? "Dark";
}
@ -66,8 +70,8 @@ public partial class SettingsViewModel : ObservableObject
public AsyncRelayCommand PythonVersionCommand => new(async () =>
{
// Get python version
await PyRunner.Initialize();
var result = await PyRunner.GetVersionInfo();
await pyRunner.Initialize();
var result = await pyRunner.GetVersionInfo();
// Show dialog box
var dialog = contentDialogService.CreateDialog();
dialog.Title = "Python version info";
@ -78,9 +82,7 @@ public partial class SettingsViewModel : ObservableObject
public RelayCommand AddInstallationCommand => new(() =>
{
var initialDirectory = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages";
// Show dialog box to choose a folder
var dialog = new VistaFolderBrowserDialog
{
Description = "Select a folder",
@ -107,13 +109,17 @@ public partial class SettingsViewModel : ObservableObject
[RelayCommand]
private async Task PingWebApi()
{
var result = await a3WebApi.GetPing();
var result = await dialogErrorHandler.TryAsync(a3WebApi.GetPing(), "Failed to ping web api");
if (result.IsSuccessful)
{
var dialog = contentDialogService.CreateDialog();
dialog.Title = "Web API ping";
dialog.Content = result;
dialog.PrimaryButtonText = "Ok";
await dialog.ShowAsync();
}
}
public async Task OnLoaded()
{

32
StabilityMatrix/ViewModels/SnackbarViewModel.cs

@ -0,0 +1,32 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Wpf.Ui.Common;
using Wpf.Ui.Contracts;
using Wpf.Ui.Controls;
using Wpf.Ui.Controls.IconElements;
using SymbolIcon = Wpf.Ui.Controls.IconElements.SymbolIcon;
namespace StabilityMatrix.ViewModels;
public partial class SnackbarViewModel : ObservableObject
{
private readonly ISnackbarService snackbarService;
[ObservableProperty]
private ControlAppearance snackbarAppearance = ControlAppearance.Secondary;
[ObservableProperty]
private int snackbarTimeout = 2000;
public SnackbarViewModel(ISnackbarService snackbarService)
{
this.snackbarService = snackbarService;
}
[RelayCommand]
private void OnOpenSnackbar(object sender)
{
snackbarService.Timeout = SnackbarTimeout;
snackbarService.Show("Some title.", "Some message.", new SymbolIcon(SymbolRegular.Fluent24), SnackbarAppearance);
}
}
Loading…
Cancel
Save