Browse Source

Merge branch 'dev' into debug-image-memory

pull/629/head
Ionite 8 months ago committed by GitHub
parent
commit
f9e305b803
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 1
      StabilityMatrix.Avalonia/App.axaml
  3. 3
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 80
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  5. 23
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  6. 3
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  7. 135
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  8. 45
      StabilityMatrix.Avalonia/Languages/Resources.resx
  9. 5
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  10. 1
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  11. 20
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  12. 148
      StabilityMatrix.Avalonia/Services/RunningPackageService.cs
  13. 73
      StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml
  14. 403
      StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml
  15. 1
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  16. 105
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  17. 16
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  18. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  19. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  20. 15
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
  21. 9
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  22. 84
      StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs
  23. 20
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs
  24. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  25. 15
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
  26. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  27. 25
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  28. 144
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  29. 8
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  30. 3
      StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
  31. 7
      StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs
  32. 55
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  33. 271
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  34. 2
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs
  35. 15
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  36. 161
      StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs
  37. 93
      StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml
  38. 54
      StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml.cs
  39. 46
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  40. 110
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  41. 276
      StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml
  42. 595
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  43. 33
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs
  44. 6
      StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs
  45. 8
      StabilityMatrix.Core/Helper/EventManager.cs
  46. 1
      StabilityMatrix.Core/Helper/Factory/IPackageFactory.cs
  47. 74
      StabilityMatrix.Core/Helper/Factory/PackageFactory.cs
  48. 16
      StabilityMatrix.Core/Inference/ComfyClient.cs
  49. 124
      StabilityMatrix.Core/Models/Api/Comfy/ComfyAuxPreprocessor.cs
  50. 17
      StabilityMatrix.Core/Models/Api/Comfy/ComfyInputInfo.cs
  51. 16
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
  52. 13
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs
  53. 3
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  54. 2
      StabilityMatrix.Core/Models/HybridModelFile.cs
  55. 14
      StabilityMatrix.Core/Models/InferenceRunCustomPromptEventArgs.cs
  56. 105
      StabilityMatrix.Core/Models/Packages/Extensions/ExtensionSpecifier.cs
  57. 25
      StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs
  58. 7
      StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs
  59. 1
      StabilityMatrix.Core/Models/Settings/Settings.cs
  60. 1
      StabilityMatrix.Core/Models/Settings/TeachingTip.cs
  61. 15
      StabilityMatrix.Tests/Helper/PackageFactoryTests.cs

7
CHANGELOG.md

@ -5,6 +5,13 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.10.0-dev.1
### Changed
- Revamped the Packages page to enable running multiple packages at the same time
- Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.9.0 ## v2.9.0
### Added ### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI - Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI

1
StabilityMatrix.Avalonia/App.axaml

@ -54,6 +54,7 @@
<StyleInclude Source="Styles/DockStyles.axaml"/> <StyleInclude Source="Styles/DockStyles.axaml"/>
<StyleInclude Source="Styles/BorderStyles.axaml"/> <StyleInclude Source="Styles/BorderStyles.axaml"/>
<StyleInclude Source="Styles/TextBoxStyles.axaml"/> <StyleInclude Source="Styles/TextBoxStyles.axaml"/>
<StyleInclude Source="Styles/CommandBarButtonStyles.axaml"/>
<StyleInclude Source="Controls/AdvancedImageBox.axaml"/> <StyleInclude Source="Controls/AdvancedImageBox.axaml"/>
<StyleInclude Source="Controls/FrameCarousel.axaml"/> <StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/> <StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>

3
StabilityMatrix.Avalonia/App.axaml.cs

@ -328,9 +328,8 @@ public sealed class App : Application
{ {
Pages = Pages =
{ {
provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<NewPackageManagerViewModel>(), provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(), provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(), provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>() provider.GetRequiredService<OutputsPageViewModel>()

80
StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml

@ -36,29 +36,46 @@
<sg:SpacedGrid <sg:SpacedGrid
ColumnDefinitions="0.34*,0.65*" ColumnDefinitions="0.34*,0.65*"
ColumnSpacing="8" ColumnSpacing="8"
RowDefinitions="Auto,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto"
RowSpacing="4"> RowSpacing="4">
<!-- Image Select --> <!-- Image Select -->
<controls:SelectImageCard <controls:SelectImageCard
Grid.RowSpan="2" Grid.RowSpan="3"
Padding="6" Padding="6"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
DataContext="{Binding SelectImageCardViewModel}" DataContext="{Binding SelectImageCardViewModel}"
FontSize="13" /> FontSize="13" />
<!-- TODO: Preprocessor Model --> <!-- Preprocessor Model -->
<ui:FAComboBox <sg:SpacedGrid
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Margin="0,0,0,4" ColumnDefinitions="*,Auto">
HorizontalAlignment="Stretch" <ui:FAComboBox
markupExtensions:ShowDisabledTooltipExtension.ShowOnDisabled="True" Margin="0,0,0,4"
Header="{x:Static lang:Resources.Label_Preprocessor}" SelectedItem="{Binding SelectedPreprocessor}"
IsEnabled="False" ItemsSource="{Binding ClientManager.Preprocessors}"
Theme="{StaticResource FAComboBoxHybridModelTheme}" DisplayMemberBinding="{Binding DisplayName}"
ToolTip.ShowDelay="1200" HorizontalAlignment="Stretch"
ToolTip.Tip="{StaticResource WipFeatureToolTip}" /> Header="{x:Static lang:Resources.Label_Preprocessor}"/>
<Button
ToolTip.Tip="{x:Static lang:Resources.Action_PreviewPreprocessor}"
Padding="7"
VerticalAlignment="Center"
Margin="0,23,0,0"
Command="{Binding PreviewPreprocessorCommand}"
CommandParameter="{Binding SelectedPreprocessor}"
Grid.Column="1">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="15"
IsFilled="True"
Symbol="Play" />
</Button>
</sg:SpacedGrid>
<!-- ControlNet Model --> <!-- ControlNet Model -->
<ui:FAComboBox <ui:FAComboBox
@ -101,6 +118,45 @@
</ui:FAComboBox.DataTemplates> </ui:FAComboBox.DataTemplates>
</ui:FAComboBox> </ui:FAComboBox>
<!-- Dimensions -->
<Grid
Grid.Row="2"
Grid.Column="1"
Margin="0,4,0,0"
ColumnDefinitions="*,Auto,*"
RowDefinitions="Auto,*">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,4,4"
Text="{x:Static lang:Resources.Label_Width}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="0"
Margin="0,0,4,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Width}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="4,0,0,4"
Text="{x:Static lang:Resources.Label_Height}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="2"
Margin="4,0,0,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Height}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
</Grid>
</sg:SpacedGrid> </sg:SpacedGrid>
<sg:SpacedGrid <sg:SpacedGrid

23
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -98,6 +98,19 @@ public static class DesignData
}, },
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
},
new()
{
Id = Guid.NewGuid(),
DisplayName = "Running Comfy",
PackageName = "ComfyUI",
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
} }
}, },
ActiveInstalledPackageId = activePackageId ActiveInstalledPackageId = activePackageId
@ -507,6 +520,16 @@ public static class DesignData
} }
) )
}; };
vm.Categories =
[
new PackageOutputCategory
{
Name = "Category 1",
Path = "path1",
SubDirectories = [new PackageOutputCategory { Name = "SubCategory 1", Path = "path3" }]
},
new PackageOutputCategory { Name = "Category 2", Path = "path2" }
];
return vm; return vm;
} }
} }

3
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -41,6 +41,9 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<ComfyScheduler> Schedulers { get; } = public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>(ComfyScheduler.Defaults); new ObservableCollectionExtended<ComfyScheduler>(ComfyScheduler.Defaults);
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>(ComfyAuxPreprocessor.Defaults);
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanUserConnect))] [NotifyPropertyChangedFor(nameof(CanUserConnect))]
private bool isConnected; private bool isConnected;

135
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -392,6 +392,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Preview Preprocessor.
/// </summary>
public static string Action_PreviewPreprocessor {
get {
return ResourceManager.GetString("Action_PreviewPreprocessor", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Quit. /// Looks up a localized string similar to Quit.
/// </summary> /// </summary>
@ -671,6 +680,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to This action cannot be undone..
/// </summary>
public static string Label_ActionCannotBeUndone {
get {
return ResourceManager.GetString("Label_ActionCannotBeUndone", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Addons. /// Looks up a localized string similar to Addons.
/// </summary> /// </summary>
@ -743,6 +761,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Are you sure you want to delete {0} images?.
/// </summary>
public static string Label_AreYouSureDeleteImages {
get {
return ResourceManager.GetString("Label_AreYouSureDeleteImages", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Augmentation Level. /// Looks up a localized string similar to Augmentation Level.
/// </summary> /// </summary>
@ -851,6 +878,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to We&apos;re checking some hardware specifications to determine compatibility..
/// </summary>
public static string Label_CheckingHardware {
get {
return ResourceManager.GetString("Label_CheckingHardware", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Checkpoint Manager. /// Looks up a localized string similar to Checkpoint Manager.
/// </summary> /// </summary>
@ -860,6 +896,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Checkpoints.
/// </summary>
public static string Label_Checkpoints {
get {
return ResourceManager.GetString("Label_Checkpoints", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to CivitAI. /// Looks up a localized string similar to CivitAI.
/// </summary> /// </summary>
@ -959,6 +1004,24 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Confirm Exit.
/// </summary>
public static string Label_ConfirmExit {
get {
return ResourceManager.GetString("Label_ConfirmExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to exit? This will also close any currently running packages..
/// </summary>
public static string Label_ConfirmExitDetail {
get {
return ResourceManager.GetString("Label_ConfirmExitDetail", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Confirm Password. /// Looks up a localized string similar to Confirm Password.
/// </summary> /// </summary>
@ -1004,6 +1067,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Console.
/// </summary>
public static string Label_Console {
get {
return ResourceManager.GetString("Label_Console", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to This will move all generated images from the selected packages to the Consolidated directory of the shared outputs folder. This action cannot be undone.. /// Looks up a localized string similar to This will move all generated images from the selected packages to the Consolidated directory of the shared outputs folder. This action cannot be undone..
/// </summary> /// </summary>
@ -1229,6 +1301,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Everything looks good!.
/// </summary>
public static string Label_EverythingLooksGood {
get {
return ResourceManager.GetString("Label_EverythingLooksGood", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.. /// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience..
/// </summary> /// </summary>
@ -1589,6 +1670,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Model Browser.
/// </summary>
public static string Label_ModelBrowser {
get {
return ResourceManager.GetString("Label_ModelBrowser", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Model Description. /// Looks up a localized string similar to Model Description.
/// </summary> /// </summary>
@ -1715,6 +1805,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to We recommend a GPU with CUDA support for the best experience. You can continue without one, but some packages may not work, and inference may be slower..
/// </summary>
public static string Label_NvidiaGpuRecommended {
get {
return ResourceManager.GetString("Label_NvidiaGpuRecommended", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to 1 image selected. /// Looks up a localized string similar to 1 image selected.
/// </summary> /// </summary>
@ -1778,6 +1877,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Packages.
/// </summary>
public static string Label_Packages {
get {
return ResourceManager.GetString("Label_Packages", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Package Type. /// Looks up a localized string similar to Package Type.
/// </summary> /// </summary>
@ -2246,6 +2354,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto-Scroll to End.
/// </summary>
public static string Label_ToggleAutoScrolling {
get {
return ResourceManager.GetString("Label_ToggleAutoScrolling", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Trigger words:. /// Looks up a localized string similar to Trigger words:.
/// </summary> /// </summary>
@ -2390,6 +2507,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Web UI.
/// </summary>
public static string Label_WebUi {
get {
return ResourceManager.GetString("Label_WebUi", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Width. /// Looks up a localized string similar to Width.
/// </summary> /// </summary>
@ -2534,6 +2660,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to The &apos;Open Web UI&apos; button has moved to the command bar.
/// </summary>
public static string TeachingTip_WebUiButtonMoved {
get {
return ResourceManager.GetString("TeachingTip_WebUiButtonMoved", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The app will relaunch after updating. /// Looks up a localized string similar to The app will relaunch after updating.
/// </summary> /// </summary>

45
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -975,4 +975,49 @@
<data name="Label_Config" xml:space="preserve"> <data name="Label_Config" xml:space="preserve">
<value>Config</value> <value>Config</value>
</data> </data>
<data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Preview Preprocessor</value>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Auto-Scroll to End</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Confirm Exit</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>Are you sure you want to exit? This will also close any currently running packages.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Console</value>
</data>
<data name="Label_WebUi" xml:space="preserve">
<value>Web UI</value>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Packages</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>This action cannot be undone.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>Are you sure you want to delete {0} images?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>We're checking some hardware specifications to determine compatibility.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>Everything looks good!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>We recommend a GPU with CUDA support for the best experience. You can continue without one, but some packages may not work, and inference may be slower.</value>
</data>
<data name="Label_Checkpoints" xml:space="preserve">
<value>Checkpoints</value>
</data>
<data name="Label_ModelBrowser" xml:space="preserve">
<value>Model Browser</value>
</data>
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>The 'Open Web UI' button has moved to the command bar</value>
</data>
</root> </root>

5
StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs

@ -1,7 +1,10 @@
namespace StabilityMatrix.Avalonia.Models; using System.Collections.ObjectModel;
namespace StabilityMatrix.Avalonia.Models;
public class PackageOutputCategory public class PackageOutputCategory
{ {
public ObservableCollection<PackageOutputCategory> SubDirectories { get; set; } = new();
public required string Name { get; set; } public required string Name { get; set; }
public required string Path { get; set; } public required string Path { get; set; }
} }

1
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

@ -44,6 +44,7 @@ public interface IInferenceClientManager : IDisposable, INotifyPropertyChanged,
IObservableCollection<ComfySampler> Samplers { get; } IObservableCollection<ComfySampler> Samplers { get; }
IObservableCollection<ComfyUpscaler> Upscalers { get; } IObservableCollection<ComfyUpscaler> Upscalers { get; }
IObservableCollection<ComfyScheduler> Schedulers { get; } IObservableCollection<ComfyScheduler> Schedulers { get; }
IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; }
Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default); Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default);

20
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -14,6 +14,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Inference;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -101,6 +102,11 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
public IObservableCollection<ComfyScheduler> Schedulers { get; } = public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>(); new ObservableCollectionExtended<ComfyScheduler>();
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>();
private readonly SourceCache<ComfyAuxPreprocessor, string> preprocessorsSource = new(p => p.Value);
public InferenceClientManager( public InferenceClientManager(
ILogger<InferenceClientManager> logger, ILogger<InferenceClientManager> logger,
IApiFactory apiFactory, IApiFactory apiFactory,
@ -166,6 +172,8 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
schedulersSource.Connect().DeferUntilLoaded().Bind(Schedulers).Subscribe(); schedulersSource.Connect().DeferUntilLoaded().Bind(Schedulers).Subscribe();
preprocessorsSource.Connect().DeferUntilLoaded().Bind(Preprocessors).Subscribe();
settingsManager.RegisterOnLibraryDirSet(_ => settingsManager.RegisterOnLibraryDirSet(_ =>
{ {
Dispatcher.UIThread.Post(ResetSharedProperties, DispatcherPriority.Background); Dispatcher.UIThread.Post(ResetSharedProperties, DispatcherPriority.Background);
@ -270,6 +278,15 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
}); });
logger.LogTrace("Loaded scheduler methods: {@Schedulers}", schedulerNames); logger.LogTrace("Loaded scheduler methods: {@Schedulers}", schedulerNames);
} }
// Add preprocessor names from Inference_Core_AIO_Preprocessor node (might not exist if no extension)
if (
await Client.GetOptionalNodeOptionNamesAsync("Inference_Core_AIO_Preprocessor", "preprocessor") is
{ } preprocessorNames
)
{
preprocessorsSource.EditDiff(preprocessorNames.Select(n => new ComfyAuxPreprocessor(n)));
}
} }
/// <summary> /// <summary>
@ -342,6 +359,9 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
u => !modelUpscalersSource.Lookup(u.Name).HasValue u => !modelUpscalersSource.Lookup(u.Name).HasValue
); );
downloadableUpscalersSource.EditDiff(remoteUpscalers, ComfyUpscaler.Comparer); downloadableUpscalersSource.EditDiff(remoteUpscalers, ComfyUpscaler.Comparer);
// Default Preprocessors
preprocessorsSource.EditDiff(ComfyAuxPreprocessor.Defaults);
} }
/// <inheritdoc /> /// <inheritdoc />

148
StabilityMatrix.Avalonia/Services/RunningPackageService.cs

@ -0,0 +1,148 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services;
[Singleton]
public partial class RunningPackageService(
ILogger<RunningPackageService> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
IPyRunner pyRunner
) : ObservableObject
{
// 🤔 what if we put the ConsoleViewModel inside the BasePackage? 🤔
[ObservableProperty]
private ObservableDictionary<Guid, RunningPackageViewModel> runningPackages = [];
public async Task<PackagePair?> StartPackage(InstalledPackage installedPackage, string? command = null)
{
var activeInstallName = installedPackage.PackageName;
var basePackage = string.IsNullOrWhiteSpace(activeInstallName)
? null
: packageFactory.GetNewBasePackage(installedPackage);
if (basePackage == null)
{
logger.LogWarning(
"During launch, package name '{PackageName}' did not match a definition",
activeInstallName
);
notificationService.Show(
new Notification(
"Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error
)
);
return null;
}
// If this is the first launch (LaunchArgs is null),
// load and save a launch options dialog vm
// so that dynamic initial values are saved.
if (installedPackage.LaunchArgs == null)
{
var definitions = basePackage.LaunchOptions;
// Create config cards and save them
var cards = LaunchOptionCard
.FromDefinitions(definitions, Array.Empty<LaunchOption>())
.ToImmutableArray();
var args = cards.SelectMany(c => c.Options).ToList();
logger.LogDebug(
"Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))
);
settingsManager.SaveLaunchArgs(installedPackage.Id, args);
}
if (basePackage is not StableSwarm)
{
await pyRunner.Initialize();
}
// Get path from package
var packagePath = new DirectoryPath(settingsManager.LibraryDir, installedPackage.LibraryPath!);
if (basePackage is not StableSwarm)
{
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
}
// Clear console and start update processing
var console = new ConsoleViewModel();
console.StartUpdates();
// Update shared folder links (in case library paths changed)
await basePackage.UpdateModelFolders(
packagePath,
installedPackage.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod
);
// Load user launch args from settings and convert to string
var userArgs = installedPackage.LaunchArgs ?? [];
var userArgsString = string.Join(" ", userArgs.Select(opt => opt.ToArgString()));
// Join with extras, if any
userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments);
// Use input command if provided, otherwise use package launch command
command ??= basePackage.LaunchCommand;
await basePackage.RunPackage(packagePath, command, userArgsString, o => console.Post(o));
var runningPackage = new PackagePair(installedPackage, basePackage);
var viewModel = new RunningPackageViewModel(
settingsManager,
notificationService,
this,
runningPackage,
console
);
RunningPackages.Add(runningPackage.InstalledPackage.Id, viewModel);
return runningPackage;
}
public async Task StopPackage(Guid id)
{
if (RunningPackages.TryGetValue(id, out var vm))
{
var runningPackage = vm.RunningPackage;
await runningPackage.BasePackage.WaitForShutdown();
RunningPackages.Remove(id);
}
}
public RunningPackageViewModel? GetRunningPackageViewModel(Guid id) =>
RunningPackages.TryGetValue(id, out var vm) ? vm : null;
private static async Task UnpackSiteCustomize(DirectoryPath venvPath)
{
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath);
var file = sitePackages.JoinFile("sitecustomize.py");
file.Directory?.Create();
await Assets.PyScriptSiteCustomize.ExtractTo(file, true);
}
}

73
StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml

@ -99,6 +99,43 @@
</Style> </Style>
</Style> </Style>
<!-- Danger -->
<Style Selector="Button.borderless-danger">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/>
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Info --> <!-- Info -->
<Style Selector="Button.info"> <Style Selector="Button.info">
<Style Selector="^ /template/ ui|FABorder#Root"> <Style Selector="^ /template/ ui|FABorder#Root">
@ -140,6 +177,42 @@
</Style> </Style>
</Style> </Style>
<Style Selector="Button.borderless-info">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/>
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!--Accent Button--> <!--Accent Button-->
<Style Selector="Button.accent"> <Style Selector="Button.accent">
<Style Selector="^ /template/ ui|FABorder#Root"> <Style Selector="^ /template/ ui|FABorder#Root">

403
StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml

@ -0,0 +1,403 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Border Padding="20">
<ui:CommandBar
Margin="8"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton Classes="success" Label="Success Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="accent" Label="FA Accent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="systemaccent" Label="System Accent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="danger" Label="Danger Button" Margin="8" HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="info" Label="Info Button" Margin="8" HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent-info" Label="Semi-Transparent Info Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent" Label="Transparent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent-full" Label="Transparent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Label="Disabled Button" Margin="8" IsEnabled="False"
HorizontalAlignment="Center" />
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
</Border>
</Design.PreviewWith>
<!-- Success -->
<Style Selector="ui|CommandBarButton.success">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeGreenColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColor}" />
</Style>
<Style Selector="^ /template/ TextBlock#TextLabel">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkGreenColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkGreenColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="Green" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkGreenColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkGreenColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Danger -->
<Style Selector="ui|CommandBarButton.danger">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Info -->
<Style Selector="ui|CommandBarButton.info">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!--Accent Button-->
<Style Selector="ui|CommandBarButton.accent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- SystemAccent -->
<Style Selector="ui|CommandBarButton.systemaccent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark1}"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark1}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark2}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark2}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Transparent -->
<Style Selector="ui|CommandBarButton.transparent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Semi-Transparent Info -->
<Style Selector="ui|CommandBarButton.transparent-info">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColorTransparent}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Transparent red -->
<Style Selector="ui|CommandBarButton.transparent-red">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeCoralRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeCoralRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkCoralRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkCoralRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Full Transparent -->
<Style Selector="ui|CommandBarButton.transparent-full">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
</Styles>

1
StabilityMatrix.Avalonia/Styles/ThemeColors.axaml

@ -21,6 +21,7 @@
<Color x:Key="ThemeCyanColor">#00BCD4</Color> <Color x:Key="ThemeCyanColor">#00BCD4</Color>
<Color x:Key="ThemeTealColor">#009688</Color> <Color x:Key="ThemeTealColor">#009688</Color>
<Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color> <Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color>
<SolidColorBrush x:Key="ThemeDarkDarkGreenColorBrush">#2C582C</SolidColorBrush>
<Color x:Key="ThemeDarkGreenColor">#3A783C</Color> <Color x:Key="ThemeDarkGreenColor">#3A783C</Color>
<Color x:Key="ThemeGreenColor">#4BA04F</Color> <Color x:Key="ThemeGreenColor">#4BA04F</Color>
<Color x:Key="ThemeGreenColorTransparent">#AA4BA04F</Color> <Color x:Key="ThemeGreenColorTransparent">#AA4BA04F</Color>

105
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -17,9 +17,9 @@ using CommunityToolkit.Mvvm.Input;
using ExifLibrary; using ExifLibrary;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nito.Disposables.Internals;
using NLog; using NLog;
using Refit; using Refit;
using Semver;
using SkiaSharp; using SkiaSharp;
using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
@ -59,6 +59,7 @@ public abstract partial class InferenceGenerationViewModelBase
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly RunningPackageService runningPackageService;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory; private readonly ServiceManager<ViewModelBase> vmFactory;
@ -79,12 +80,14 @@ public abstract partial class InferenceGenerationViewModelBase
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
INotificationService notificationService, INotificationService notificationService,
ISettingsManager settingsManager ISettingsManager settingsManager,
RunningPackageService runningPackageService
) )
: base(notificationService) : base(notificationService)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.runningPackageService = runningPackageService;
this.vmFactory = vmFactory; this.vmFactory = vmFactory;
ClientManager = inferenceClientManager; ClientManager = inferenceClientManager;
@ -259,6 +262,30 @@ public abstract partial class InferenceGenerationViewModelBase
} }
} }
public async Task RunCustomGeneration(
InferenceQueueCustomPromptEventArgs args,
CancellationToken cancellationToken = default
)
{
if (ClientManager.Client is not { } client)
{
throw new InvalidOperationException("Client is not connected");
}
var generationArgs = new ImageGenerationEventArgs
{
Client = client,
Nodes = args.Builder.ToNodeDictionary(),
OutputNodeNames = args.Builder.Connections.OutputNodeNames.ToArray(),
Project = InferenceProjectDocument.FromLoadable(this),
FilesToTransfer = args.FilesToTransfer,
Parameters = new GenerationParameters(),
ClearOutputImages = true
};
await RunGeneration(generationArgs, cancellationToken);
}
/// <summary> /// <summary>
/// Runs a generation task /// Runs a generation task
/// </summary> /// </summary>
@ -643,12 +670,10 @@ public abstract partial class InferenceGenerationViewModelBase
{ {
// Get prompt required extensions // Get prompt required extensions
// Just static for now but could do manifest lookup when we support custom workflows // Just static for now but could do manifest lookup when we support custom workflows
var requiredExtensions = nodeDictionary var requiredExtensionSpecifiers = nodeDictionary.RequiredExtensions.ToList();
.ClassTypeRequiredExtensions.Values.SelectMany(x => x)
.ToHashSet();
// Skip if no extensions required // Skip if no extensions required
if (requiredExtensions.Count == 0) if (requiredExtensionSpecifiers.Count == 0)
{ {
return true; return true;
} }
@ -661,20 +686,63 @@ public abstract partial class InferenceGenerationViewModelBase
await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync( await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync(
localPackagePair.InstalledPackage localPackagePair.InstalledPackage
) )
).ToImmutableArray(); ).ToList();
var localExtensionsByGitUrl = localExtensions
.Where(ext => ext.GitRepositoryUrl is not null)
.ToDictionary(ext => ext.GitRepositoryUrl!, ext => ext);
var requiredExtensionReferences = requiredExtensionSpecifiers
.Select(specifier => specifier.Name)
.ToHashSet();
var missingExtensions = requiredExtensions var missingExtensions = new List<ExtensionSpecifier>();
.Except(localExtensions.Select(ext => ext.GitRepositoryUrl).WhereNotNull()) var outOfDateExtensions =
.ToImmutableArray(); new List<(ExtensionSpecifier Specifier, InstalledPackageExtension Installed)>();
if (missingExtensions.Length == 0) // Check missing extensions and out of date extensions
foreach (var specifier in requiredExtensionSpecifiers)
{
if (!localExtensionsByGitUrl.TryGetValue(specifier.Name, out var localExtension))
{
missingExtensions.Add(specifier);
continue;
}
// Check if constraint is specified
if (specifier.Constraint is not null && specifier.TryGetSemVersionRange(out var semVersionRange))
{
// Get version to compare
localExtension = await manager.GetInstalledExtensionInfoAsync(localExtension);
// Try to parse local tag to semver
if (
localExtension.Version?.Tag is not null
&& SemVersion.TryParse(
localExtension.Version.Tag,
SemVersionStyles.AllowV,
out var localSemVersion
)
)
{
// Check if not satisfied
if (!semVersionRange.Contains(localSemVersion))
{
outOfDateExtensions.Add((specifier, localExtension));
}
}
}
}
if (missingExtensions.Count == 0 && outOfDateExtensions.Count == 0)
{ {
return true; return true;
} }
var dialog = DialogHelper.CreateMarkdownDialog( var dialog = DialogHelper.CreateMarkdownDialog(
$"#### The following extensions are required for this workflow:\n" $"#### The following extensions are required for this workflow:\n"
+ $"{string.Join("\n- ", missingExtensions)}", + $"{string.Join("\n- ", missingExtensions.Select(ext => ext.Name))}"
+ $"{string.Join("\n- ", outOfDateExtensions.Select(pair => $"{pair.Item1.Name} {pair.Specifier.Constraint} {pair.Specifier.Version} (Current Version: {pair.Installed.Version?.Tag})"))}",
"Install Required Extensions?" "Install Required Extensions?"
); );
@ -692,13 +760,13 @@ public abstract partial class InferenceGenerationViewModelBase
var steps = new List<IPackageStep>(); var steps = new List<IPackageStep>();
foreach (var missingExtensionUrl in missingExtensions) foreach (var missingExtension in missingExtensions)
{ {
if (!manifestExtensionsMap.TryGetValue(missingExtensionUrl, out var extension)) if (!manifestExtensionsMap.TryGetValue(missingExtension.Name, out var extension))
{ {
Logger.Warn( Logger.Warn(
"Extension {MissingExtensionUrl} not found in manifests", "Extension {MissingExtensionUrl} not found in manifests",
missingExtensionUrl missingExtension.Name
); );
continue; continue;
} }
@ -722,15 +790,12 @@ public abstract partial class InferenceGenerationViewModelBase
return; return;
// Restart Package // Restart Package
// TODO: This should be handled by some DI package manager service
var launchPage = App.Services.GetRequiredService<LaunchPageViewModel>();
try try
{ {
await Dispatcher.UIThread.InvokeAsync(async () => await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await launchPage.Stop(); await runningPackageService.StopPackage(localPackagePair.InstalledPackage.Id);
await launchPage.LaunchAsync(); await runningPackageService.StartPackage(localPackagePair.InstalledPackage);
}); });
} }
catch (Exception e) catch (Exception e)

16
StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

@ -98,11 +98,7 @@ public abstract partial class InferenceTabViewModelBase
protected async Task<ViewState> SaveViewState() protected async Task<ViewState> SaveViewState()
{ {
var eventArgs = new SaveViewStateEventArgs(); var eventArgs = new SaveViewStateEventArgs();
saveViewStateRequestedEventManager?.RaiseEvent( saveViewStateRequestedEventManager?.RaiseEvent(this, eventArgs, nameof(SaveViewStateRequested));
this,
eventArgs,
nameof(SaveViewStateRequested)
);
if (eventArgs.StateTask is not { } stateTask) if (eventArgs.StateTask is not { } stateTask)
{ {
@ -128,7 +124,7 @@ public abstract partial class InferenceTabViewModelBase
// TODO: Dock reset not working, using this hack for now to get a new view // TODO: Dock reset not working, using this hack for now to get a new view
var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>(); var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>();
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo()); navService.NavigateTo<NewPackageManagerViewModel>(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null; ((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition()); navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition());
} }
@ -157,9 +153,7 @@ public abstract partial class InferenceTabViewModelBase
if (result == ContentDialogResult.Primary && textFields[0].Text is { } json) if (result == ContentDialogResult.Primary && textFields[0].Text is { } json)
{ {
LoadViewState( LoadViewState(new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } });
new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } }
);
} }
} }
@ -226,9 +220,7 @@ public abstract partial class InferenceTabViewModelBase
if (this is IParametersLoadableState paramsLoadableVm) if (this is IParametersLoadableState paramsLoadableVm)
{ {
Dispatcher.UIThread.Invoke( Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters));
() => paramsLoadableVm.LoadStateFromParameters(parameters)
);
} }
else else
{ {

3
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -6,6 +6,7 @@ using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.Core; using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
@ -20,7 +21,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Singleton] [Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase public partial class CheckpointBrowserViewModel : PageViewModelBase
{ {
public override string Title => "Model Browser"; public override string Title => Resources.Label_ModelBrowser;
public override IconSource IconSource => public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true }; new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };

3
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Resources = StabilityMatrix.Avalonia.Languages.Resources;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip;
@ -43,7 +44,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService; private readonly IMetadataImportService metadataImportService;
public override string Title => "Checkpoints"; public override string Title => Resources.Label_Checkpoints;
public override IconSource IconSource => public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true }; new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true };

15
StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -30,6 +31,7 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly INavigationService<MainWindowViewModel> navigationService; private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly RunningPackageService runningPackageService;
[ObservableProperty] [ObservableProperty]
private string title = "Hello"; private string title = "Hello";
@ -58,12 +60,14 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
public InferenceConnectionHelpViewModel( public InferenceConnectionHelpViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
INavigationService<MainWindowViewModel> navigationService, INavigationService<MainWindowViewModel> navigationService,
IPackageFactory packageFactory IPackageFactory packageFactory,
RunningPackageService runningPackageService
) )
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.navigationService = navigationService; this.navigationService = navigationService;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.runningPackageService = runningPackageService;
// Get comfy type installed packages // Get comfy type installed packages
var comfyPackages = this.settingsManager.Settings.InstalledPackages.Where( var comfyPackages = this.settingsManager.Settings.InstalledPackages.Where(
@ -122,14 +126,11 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
/// Request launch of the selected package /// Request launch of the selected package
/// </summary> /// </summary>
[RelayCommand] [RelayCommand]
private void LaunchSelectedPackage() private async Task LaunchSelectedPackage()
{ {
if (SelectedPackage?.Id is { } id) if (SelectedPackage is not null)
{ {
Dispatcher.UIThread.Post(() => await runningPackageService.StartPackage(SelectedPackage);
{
EventManager.Instance.OnPackageLaunchRequested(id);
});
} }
} }

9
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
@ -27,11 +28,9 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge = private RefreshBadgeViewModel checkHardwareBadge =
new() new()
{ {
WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.", WorkingToolTipText = Resources.Label_CheckingHardware,
SuccessToolTipText = "Everything looks good!", SuccessToolTipText = Resources.Label_EverythingLooksGood,
FailToolTipText = FailToolTipText = Resources.Label_NvidiaGpuRecommended,
"We recommend a GPU with CUDA support for the best experience. "
+ "You can continue without one, but some packages may not work, and inference may be slower.",
FailColorBrush = ThemeColors.ThemeYellow, FailColorBrush = ThemeColors.ThemeYellow,
}; };

84
StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs

@ -1,19 +1,20 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -32,7 +33,17 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
[ObservableProperty] [ObservableProperty]
[Required] [Required]
private HybridModelFile? selectedPreprocessor; private ComfyAuxPreprocessor? selectedPreprocessor;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int width;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int height;
[ObservableProperty] [ObservableProperty]
[Required] [Required]
@ -62,6 +73,18 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
ClientManager = clientManager; ClientManager = clientManager;
SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>(); SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>();
// Update our width and height when the image changes
SelectImageCardViewModel
.WhenPropertyChanged(card => card.CurrentBitmapSize)
.Subscribe(propertyValue =>
{
if (!propertyValue.Value.IsEmpty)
{
Width = propertyValue.Value.Width;
Height = propertyValue.Value.Height;
}
});
} }
[RelayCommand] [RelayCommand]
@ -79,4 +102,57 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
confirmDialog.StartDownload(); confirmDialog.StartDownload();
} }
} }
[RelayCommand]
private async Task PreviewPreprocessor(ComfyAuxPreprocessor? preprocessor)
{
if (
preprocessor is null
|| SelectImageCardViewModel.ImageSource is not { } imageSource
|| SelectImageCardViewModel.IsImageFileNotFound
)
return;
var args = new InferenceQueueCustomPromptEventArgs();
var images = SelectImageCardViewModel.GetInputImages();
await ClientManager.UploadInputImageAsync(imageSource);
var image = args.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage
{
Name = args.Nodes.GetUniqueName("Preprocessor_LoadImage"),
Image =
SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? throw new ValidationException("No ImageSource")
}
).Output1;
var aioPreprocessor = args.Nodes.AddTypedNode(
new ComfyNodeBuilder.AIOPreprocessor
{
Name = args.Nodes.GetUniqueName("Preprocessor"),
Image = image,
Preprocessor = preprocessor.ToString(),
Resolution = Width is <= 2048 and > 0 ? Width : 512
}
);
args.Builder.Connections.OutputNodes.Add(
args.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = args.Nodes.GetUniqueName("Preprocessor_OutputImage"),
Images = aioPreprocessor.Output
}
)
);
// Queue
Dispatcher.UIThread.Post(() => EventManager.Instance.OnInferenceQueueCustomPrompt(args));
// We don't know when it's done so wait a bit?
await Task.Delay(1000);
}
} }

20
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs

@ -27,9 +27,17 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
INotificationService notificationService, INotificationService notificationService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IModelIndexService modelIndexService IModelIndexService modelIndexService,
RunningPackageService runningPackageService
) )
: base(notificationService, inferenceClientManager, settingsManager, vmFactory, modelIndexService) : base(
notificationService,
inferenceClientManager,
settingsManager,
vmFactory,
modelIndexService,
runningPackageService
)
{ {
SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>(); SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>();
@ -77,12 +85,12 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
var mainImages = SelectImageCardViewModel.GetInputImages(); var mainImages = SelectImageCardViewModel.GetInputImages();
var samplerImages = SamplerCardViewModel var samplerImages = SamplerCardViewModel
.ModulesCardViewModel .ModulesCardViewModel.Cards.OfType<IInputImageProvider>()
.Cards
.OfType<IInputImageProvider>()
.SelectMany(m => m.GetInputImages()); .SelectMany(m => m.GetInputImages());
var moduleImages = ModulesCardViewModel.Cards.OfType<IInputImageProvider>().SelectMany(m => m.GetInputImages()); var moduleImages = ModulesCardViewModel
.Cards.OfType<IInputImageProvider>()
.SelectMany(m => m.GetInputImages());
return mainImages.Concat(samplerImages).Concat(moduleImages); return mainImages.Concat(samplerImages).Concat(moduleImages);
} }

5
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs

@ -65,9 +65,10 @@ public partial class InferenceImageToVideoViewModel
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager, ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService IModelIndexService modelIndexService,
RunningPackageService runningPackageService
) )
: base(vmFactory, inferenceClientManager, notificationService, settingsManager) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.modelIndexService = modelIndexService; this.modelIndexService = modelIndexService;

15
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs

@ -59,9 +59,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
INotificationService notificationService, INotificationService notificationService,
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager, ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory ServiceManager<ViewModelBase> vmFactory,
RunningPackageService runningPackageService
) )
: base(vmFactory, inferenceClientManager, notificationService, settingsManager) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
@ -142,7 +143,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
} }
/// <inheritdoc /> /// <inheritdoc />
protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken) protected override async Task GenerateImageImpl(
GenerateOverrides overrides,
CancellationToken cancellationToken
)
{ {
if (!ClientManager.IsConnected) if (!ClientManager.IsConnected)
{ {
@ -169,7 +173,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
Client = ClientManager.Client, Client = ClientManager.Client,
Nodes = buildPromptArgs.Builder.ToNodeDictionary(), Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
Parameters = new GenerationParameters { ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name, }, Parameters = new GenerationParameters
{
ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name,
},
Project = InferenceProjectDocument.FromLoadable(this) Project = InferenceProjectDocument.FromLoadable(this)
}; };

5
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -59,9 +59,10 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager, ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService IModelIndexService modelIndexService,
RunningPackageService runningPackageService
) )
: base(vmFactory, inferenceClientManager, notificationService, settingsManager) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.modelIndexService = modelIndexService; this.modelIndexService = modelIndexService;

25
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs

@ -6,6 +6,7 @@ using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
@ -39,7 +40,7 @@ public class ControlNetModule : ModuleBase
{ {
var card = GetCard<ControlNetCardViewModel>(); var card = GetCard<ControlNetCardViewModel>();
var imageLoad = e.Nodes.AddTypedNode( var image = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage new ComfyNodeBuilder.LoadImage
{ {
Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"), Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"),
@ -47,7 +48,23 @@ public class ControlNetModule : ModuleBase
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? throw new ValidationException("No ImageSource") ?? throw new ValidationException("No ImageSource")
} }
); ).Output1;
if (card.SelectedPreprocessor is { } preprocessor && preprocessor != ComfyAuxPreprocessor.None)
{
var aioPreprocessor = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.AIOPreprocessor
{
Name = e.Nodes.GetUniqueName("ControlNet_Preprocessor"),
Image = image,
Preprocessor = preprocessor.ToString(),
// Use width if valid, else default of 512
Resolution = card.Width is <= 2048 and > 0 ? card.Width : 512
}
);
image = aioPreprocessor.Output;
}
var controlNetLoader = e.Nodes.AddTypedNode( var controlNetLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetLoader new ComfyNodeBuilder.ControlNetLoader
@ -62,7 +79,7 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetApplyAdvanced new ComfyNodeBuilder.ControlNetApplyAdvanced
{ {
Name = e.Nodes.GetUniqueName("ControlNetApply"), Name = e.Nodes.GetUniqueName("ControlNetApply"),
Image = imageLoad.Output1, Image = image,
ControlNet = controlNetLoader.Output, ControlNet = controlNetLoader.Output,
Positive = e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"), Positive = e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"), Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
@ -81,7 +98,7 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetApplyAdvanced new ComfyNodeBuilder.ControlNetApplyAdvanced
{ {
Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"), Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"),
Image = imageLoad.Output1, Image = image,
ControlNet = controlNetLoader.Output, ControlNet = controlNetLoader.Output,
Positive = e.Temp.RefinerConditioning.Positive, Positive = e.Temp.RefinerConditioning.Positive,
Negative = e.Temp.RefinerConditioning.Negative, Negative = e.Temp.RefinerConditioning.Negative,

144
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
@ -16,6 +18,7 @@ using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
@ -51,8 +54,10 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
private readonly ServiceManager<ViewModelBase> vmFactory; private readonly ServiceManager<ViewModelBase> vmFactory;
private readonly IModelIndexService modelIndexService; private readonly IModelIndexService modelIndexService;
private readonly ILiteDbContext liteDbContext; private readonly ILiteDbContext liteDbContext;
private readonly RunningPackageService runningPackageService;
private Guid? selectedPackageId;
public override string Title => "Inference"; public override string Title => Resources.Label_Inference;
public override IconSource IconSource => public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.AppGeneric, IsFilled = true }; new SymbolIconSource { Symbol = Symbol.AppGeneric, IsFilled = true };
@ -62,7 +67,7 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
State = ProgressState.Failed, State = ProgressState.Failed,
FailToolTipText = "Not connected", FailToolTipText = "Not connected",
FailIcon = FluentAvalonia.UI.Controls.Symbol.Refresh, FailIcon = FluentAvalonia.UI.Controls.Symbol.Refresh,
SuccessToolTipText = "Connected", SuccessToolTipText = Resources.Label_Connected,
}; };
public IInferenceClientManager ClientManager { get; } public IInferenceClientManager ClientManager { get; }
@ -86,6 +91,8 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
public bool IsComfyRunning => RunningPackage?.BasePackage is ComfyUI; public bool IsComfyRunning => RunningPackage?.BasePackage is ComfyUI;
private IDisposable? onStartupComplete;
public InferenceViewModel( public InferenceViewModel(
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
INotificationService notificationService, INotificationService notificationService,
@ -93,6 +100,7 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
ISettingsManager settingsManager, ISettingsManager settingsManager,
IModelIndexService modelIndexService, IModelIndexService modelIndexService,
ILiteDbContext liteDbContext, ILiteDbContext liteDbContext,
RunningPackageService runningPackageService,
SharedState sharedState SharedState sharedState
) )
{ {
@ -101,12 +109,13 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.modelIndexService = modelIndexService; this.modelIndexService = modelIndexService;
this.liteDbContext = liteDbContext; this.liteDbContext = liteDbContext;
this.runningPackageService = runningPackageService;
ClientManager = inferenceClientManager; ClientManager = inferenceClientManager;
SharedState = sharedState; SharedState = sharedState;
// Keep RunningPackage updated with the current package pair // Keep RunningPackage updated with the current package pair
EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged; runningPackageService.RunningPackages.CollectionChanged += RunningPackagesOnCollectionChanged;
// "Send to Inference" // "Send to Inference"
EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested; EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
@ -114,58 +123,112 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
EventManager.Instance.InferenceImageToImageRequested += OnInferenceImageToImageRequested; EventManager.Instance.InferenceImageToImageRequested += OnInferenceImageToImageRequested;
EventManager.Instance.InferenceImageToVideoRequested += OnInferenceImageToVideoRequested; EventManager.Instance.InferenceImageToVideoRequested += OnInferenceImageToVideoRequested;
// Global requests for custom prompt queueing
EventManager.Instance.InferenceQueueCustomPrompt += OnInferenceQueueCustomPromptRequested;
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService); MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService); MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
} }
private void DisconnectFromComfy()
{
RunningPackage = null;
// Cancel any pending connection
if (ConnectCancelCommand.CanExecute(null))
{
ConnectCancelCommand.Execute(null);
}
onStartupComplete?.Dispose();
onStartupComplete = null;
IsWaitingForConnection = false;
// Disconnect
Logger.Trace("On package close - disconnecting");
DisconnectCommand.Execute(null);
}
/// <summary> /// <summary>
/// Updates the RunningPackage property when the running package changes. /// Updates the RunningPackage property when the running package changes.
/// Also starts a connection to the backend if a new ComfyUI package is running. /// Also starts a connection to the backend if a new ComfyUI package is running.
/// And disconnects if the package is closed. /// And disconnects if the package is closed.
/// </summary> /// </summary>
private void OnRunningPackageStatusChanged(object? sender, RunningPackageStatusChangedEventArgs e) private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
RunningPackage = e.CurrentPackagePair; if (
e.NewItems?.OfType<KeyValuePair<Guid, RunningPackageViewModel>>().Select(x => x.Value)
is not { } newItems
)
{
if (RunningPackage != null)
{
DisconnectFromComfy();
}
return;
}
IDisposable? onStartupComplete = null; var comfyViewModel = newItems.FirstOrDefault(
vm =>
vm.RunningPackage.InstalledPackage.Id == selectedPackageId
|| vm.RunningPackage.BasePackage is ComfyUI
);
Dispatcher.UIThread.Post(() => if (comfyViewModel is null && RunningPackage?.BasePackage is ComfyUI)
{ {
if (e.CurrentPackagePair?.BasePackage is ComfyUI package) DisconnectFromComfy();
{ }
IsWaitingForConnection = true; else if (comfyViewModel != null && RunningPackage == null)
onStartupComplete = Observable {
.FromEventPattern<string>(package, nameof(package.StartupComplete)) IsWaitingForConnection = true;
.Take(1) RunningPackage = comfyViewModel.RunningPackage;
.Subscribe(_ => onStartupComplete = Observable
.FromEventPattern<string>(
comfyViewModel.RunningPackage.BasePackage,
nameof(comfyViewModel.RunningPackage.BasePackage.StartupComplete)
)
.Take(1)
.Subscribe(_ =>
{
Dispatcher.UIThread.Post(() =>
{ {
Dispatcher.UIThread.Post(() => if (ConnectCommand.CanExecute(null))
{ {
if (ConnectCommand.CanExecute(null)) Logger.Trace("On package launch - starting connection");
{ ConnectCommand.Execute(null);
Logger.Trace("On package launch - starting connection"); }
ConnectCommand.Execute(null);
} IsWaitingForConnection = false;
IsWaitingForConnection = false;
});
}); });
} });
else }
{ }
// Cancel any pending connection
if (ConnectCancelCommand.CanExecute(null)) private void OnInferenceQueueCustomPromptRequested(object? sender, InferenceQueueCustomPromptEventArgs e)
{
// Get currently selected tab
var currentTab = SelectedTab;
if (currentTab is InferenceGenerationViewModelBase generationViewModel)
{
Dispatcher
.UIThread.InvokeAsync(async () =>
{ {
ConnectCancelCommand.Execute(null); await generationViewModel.RunCustomGeneration(e);
} })
onStartupComplete?.Dispose(); .SafeFireAndForget(ex =>
onStartupComplete = null; {
IsWaitingForConnection = false; Logger.Error(ex, "Failed to queue prompt");
// Disconnect Dispatcher.UIThread.Post(() =>
Logger.Trace("On package close - disconnecting"); {
DisconnectCommand.Execute(null); notificationService.ShowPersistent(
} "Failed to queue prompt",
}); $"{ex.GetType().Name}: {ex.Message}",
NotificationType.Error
);
});
});
}
} }
public override void OnLoaded() public override void OnLoaded()
@ -390,7 +453,12 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
private async Task ShowConnectionHelp() private async Task ShowConnectionHelp()
{ {
var vm = vmFactory.Get<InferenceConnectionHelpViewModel>(); var vm = vmFactory.Get<InferenceConnectionHelpViewModel>();
await vm.CreateDialog().ShowAsync(); var result = await vm.CreateDialog().ShowAsync();
if (result != ContentDialogResult.Primary)
return;
selectedPackageId = vm.SelectedPackage?.Id;
} }
/// <summary> /// <summary>

8
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -139,14 +139,6 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new NewOneClickInstallDialog { DataContext = viewModel }, Content = new NewOneClickInstallDialog { DataContext = viewModel },
}; };
EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{
if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded();
};
var firstDialogResult = await dialog.ShowAsync(App.TopLevel); var firstDialogResult = await dialog.ShowAsync(App.TopLevel);
if (firstDialogResult != ContentDialogResult.Primary) if (firstDialogResult != ContentDialogResult.Primary)

3
StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs

@ -14,6 +14,7 @@ using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Refit; using Refit;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -47,7 +48,7 @@ public partial class NewCheckpointsPageViewModel(
IMetadataImportService metadataImportService IMetadataImportService metadataImportService
) : PageViewModelBase ) : PageViewModelBase
{ {
public override string Title => "Checkpoint Manager"; public override string Title => Resources.Label_CheckpointManager;
public override IconSource IconSource => public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Cellular5g, IsFilled = true }; new SymbolIconSource { Symbol = Symbol.Cellular5g, IsFilled = true };

7
StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData; using DynamicData;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -18,7 +19,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Singleton] [Singleton]
public partial class NewPackageManagerViewModel : PageViewModelBase public partial class NewPackageManagerViewModel : PageViewModelBase
{ {
public override string Title => "Packages"; public override string Title => Resources.Label_Packages;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true }; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true };
public IReadOnlyList<PageViewModelBase> SubPages { get; } public IReadOnlyList<PageViewModelBase> SubPages { get; }
@ -58,6 +59,10 @@ public partial class NewPackageManagerViewModel : PageViewModelBase
{ {
CurrentPagePath.Add(value); CurrentPagePath.Add(value);
} }
else if (value is RunningPackageViewModel)
{
CurrentPagePath.Add(value);
}
else else
{ {
CurrentPagePath.Clear(); CurrentPagePath.Clear();

55
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -86,6 +86,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private bool isConsolidating; private bool isConsolidating;
[ObservableProperty]
private bool showFolders;
public bool CanShowOutputTypes => SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false; public bool CanShowOutputTypes => SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
public string NumImagesSelected => public string NumImagesSelected =>
@ -136,6 +139,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
settings => settings.OutputsImageSize, settings => settings.OutputsImageSize,
delay: TimeSpan.FromMilliseconds(250) delay: TimeSpan.FromMilliseconds(250)
); );
settingsManager.RelayPropertyFor(
this,
vm => vm.ShowFolders,
settings => settings.IsOutputsTreeViewEnabled,
true
);
} }
protected override void OnInitialLoaded() protected override void OnInitialLoaded()
@ -273,8 +283,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
var confirmationDialog = new BetterContentDialog var confirmationDialog = new BetterContentDialog
{ {
Title = "Are you sure you want to delete this image?", Title = Resources.Label_AreYouSure,
Content = "This action cannot be undone.", Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete, PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel, SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary, DefaultButton = ContentDialogButton.Primary,
@ -352,8 +362,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
{ {
var confirmationDialog = new BetterContentDialog var confirmationDialog = new BetterContentDialog
{ {
Title = $"Are you sure you want to delete {NumItemsSelected} images?", Title = string.Format(Resources.Label_AreYouSureDeleteImages, NumItemsSelected),
Content = "This action cannot be undone.", Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete, PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel, SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary, DefaultButton = ContentDialogButton.Primary,
@ -559,7 +569,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
pair.InstalledPackage.FullPath!, pair.InstalledPackage.FullPath!,
pair.BasePackage.OutputFolderName pair.BasePackage.OutputFolderName
), ),
Name = pair.InstalledPackage.DisplayName ?? "" Name = pair.InstalledPackage.DisplayName ?? "",
SubDirectories = GetSubfolders(
Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName)
)
} }
) )
.ToList(); .ToList();
@ -569,18 +582,38 @@ public partial class OutputsPageViewModel : PageViewModelBase
new PackageOutputCategory new PackageOutputCategory
{ {
Path = settingsManager.ImagesDirectory, Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder" Name = "Shared Output Folder",
SubDirectories = GetSubfolders(settingsManager.ImagesDirectory)
} }
); );
packageCategories.Insert(
1,
new PackageOutputCategory { Path = settingsManager.ImagesInferenceDirectory, Name = "Inference" }
);
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories); Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
SelectedCategory = SelectedCategory =
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First(); Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
} }
private ObservableCollection<PackageOutputCategory> GetSubfolders(string strPath)
{
var subfolders = new ObservableCollection<PackageOutputCategory>();
if (!Directory.Exists(strPath))
return subfolders;
var directories = Directory.EnumerateDirectories(strPath, "*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
var category = new PackageOutputCategory { Name = Path.GetFileName(dir), Path = dir };
if (Directory.GetDirectories(dir, "*", SearchOption.TopDirectoryOnly).Length > 0)
{
category.SubDirectories = GetSubfolders(dir);
}
subfolders.Add(category);
}
return subfolders;
}
} }

271
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -1,11 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -26,7 +31,6 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -34,14 +38,17 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
[ManagedService] [ManagedService]
[Transient] [Transient]
public partial class PackageCardViewModel : ProgressViewModel public partial class PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService<NewPackageManagerViewModel> navigationService,
ServiceManager<ViewModelBase> vmFactory,
RunningPackageService runningPackageService
) : ProgressViewModel
{ {
private readonly ILogger<PackageCardViewModel> logger; private string webUiUrl = string.Empty;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty] [ObservableProperty]
private InstalledPackage? package; private InstalledPackage? package;
@ -82,21 +89,31 @@ public partial class PackageCardViewModel : ProgressViewModel
[ObservableProperty] [ObservableProperty]
private bool canUseExtensions; private bool canUseExtensions;
public PackageCardViewModel( [ObservableProperty]
ILogger<PackageCardViewModel> logger, private bool isRunning;
IPackageFactory packageFactory,
INotificationService notificationService, [ObservableProperty]
ISettingsManager settingsManager, private bool showWebUiButton;
INavigationService<MainWindowViewModel> navigationService,
ServiceManager<ViewModelBase> vmFactory private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
)
{ {
this.logger = logger; if (runningPackageService.RunningPackages.Select(x => x.Value) is not { } runningPackages)
this.packageFactory = packageFactory; return;
this.notificationService = notificationService;
this.settingsManager = settingsManager; var runningViewModel = runningPackages.FirstOrDefault(
this.navigationService = navigationService; x => x.RunningPackage.InstalledPackage.Id == Package?.Id
this.vmFactory = vmFactory; );
if (runningViewModel is not null)
{
IsRunning = true;
runningViewModel.RunningPackage.BasePackage.Exited += BasePackageOnExited;
runningViewModel.RunningPackage.BasePackage.StartupComplete += RunningPackageOnStartupComplete;
}
else if (runningViewModel is null && IsRunning)
{
IsRunning = false;
ShowWebUiButton = false;
}
} }
partial void OnPackageChanged(InstalledPackage? value) partial void OnPackageChanged(InstalledPackage? value)
@ -127,11 +144,30 @@ public partial class PackageCardViewModel : ProgressViewModel
UseSharedOutput = Package?.UseSharedOutputFolder ?? false; UseSharedOutput = Package?.UseSharedOutputFolder ?? false;
CanUseSharedOutput = basePackage?.SharedOutputFolders != null; CanUseSharedOutput = basePackage?.SharedOutputFolders != null;
CanUseExtensions = basePackage?.SupportsExtensions ?? false; CanUseExtensions = basePackage?.SupportsExtensions ?? false;
runningPackageService.RunningPackages.CollectionChanged += RunningPackagesOnCollectionChanged;
EventManager.Instance.PackageRelaunchRequested += InstanceOnPackageRelaunchRequested;
} }
} }
private void InstanceOnPackageRelaunchRequested(object? sender, InstalledPackage e)
{
if (e.Id != Package?.Id)
return;
navigationService.GoBack();
Launch().SafeFireAndForget();
}
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (Design.IsDesignMode && Package?.DisplayName == "Running Comfy")
{
IsRunning = true;
IsUpdateAvailable = true;
ShowWebUiButton = true;
}
if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet || Package is not { } currentPackage) if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet || Package is not { } currentPackage)
return; return;
@ -160,18 +196,122 @@ public partial class PackageCardViewModel : ProgressViewModel
} }
IsUpdateAvailable = await HasUpdate(); IsUpdateAvailable = await HasUpdate();
if (
Package != null
&& !IsRunning
&& runningPackageService.RunningPackages.TryGetValue(Package.Id, out var runningPackageVm)
)
{
IsRunning = true;
runningPackageVm.RunningPackage.BasePackage.Exited += BasePackageOnExited;
runningPackageVm.RunningPackage.BasePackage.StartupComplete +=
RunningPackageOnStartupComplete;
webUiUrl = runningPackageVm.WebUiUrl;
ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl);
}
}
}
public override void OnUnloaded()
{
EventManager.Instance.PackageRelaunchRequested -= InstanceOnPackageRelaunchRequested;
runningPackageService.RunningPackages.CollectionChanged -= RunningPackagesOnCollectionChanged;
}
public async Task Launch()
{
if (Package == null)
return;
var packagePair = await runningPackageService.StartPackage(Package);
if (packagePair != null)
{
IsRunning = true;
packagePair.BasePackage.Exited += BasePackageOnExited;
packagePair.BasePackage.StartupComplete += RunningPackageOnStartupComplete;
var vm = runningPackageService.GetRunningPackageViewModel(packagePair.InstalledPackage.Id);
if (vm != null)
{
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
} }
// settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
//
// navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition());
// EventManager.Instance.OnPackageLaunchRequested(Package.Id);
} }
public void Launch() public void NavToConsole()
{ {
if (Package == null) if (Package == null)
return; return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id); var vm = runningPackageService.GetRunningPackageViewModel(Package.Id);
if (vm != null)
{
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
public void LaunchWebUi()
{
if (string.IsNullOrEmpty(webUiUrl))
return;
notificationService.TryAsync(
Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)),
"Failed to open URL",
$"{webUiUrl}"
);
}
private void BasePackageOnExited(object? sender, int exitCode)
{
Dispatcher
.UIThread.InvokeAsync(async () =>
{
logger.LogTrace("Process exited ({Code}) at {Time:g}", exitCode, DateTimeOffset.Now);
// Need to wait for streams to finish before detaching handlers
if (sender is BaseGitPackage { VenvRunner: not null } package)
{
var process = package.VenvRunner.Process;
if (process is not null)
{
// Max 5 seconds
var ct = new CancellationTokenSource(5000).Token;
try
{
await process.WaitUntilOutputEOF(ct);
}
catch (OperationCanceledException e)
{
logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message);
}
}
}
navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition()); // Detach handlers
EventManager.Instance.OnPackageLaunchRequested(Package.Id); if (sender is BasePackage basePackage)
{
basePackage.Exited -= BasePackageOnExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete;
}
if (Package?.Id != null)
{
runningPackageService.RunningPackages.Remove(Package.Id);
}
IsRunning = false;
ShowWebUiButton = false;
})
.SafeFireAndForget();
} }
public async Task Uninstall() public async Task Uninstall()
@ -282,6 +422,21 @@ public partial class PackageCardViewModel : ProgressViewModel
versionOptions.CommitHash = latest.Sha; versionOptions.CommitHash = latest.Sha;
} }
var confirmationDialog = new BetterContentDialog
{
Title = Resources.Label_AreYouSure,
Content =
$"{Package.DisplayName} will be updated to the latest version ({versionOptions.GetReadableVersionString()})",
PrimaryButtonText = Resources.Action_Continue,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
var updatePackageStep = new UpdatePackageStep( var updatePackageStep = new UpdatePackageStep(
settingsManager, settingsManager,
Package, Package,
@ -435,6 +590,64 @@ public partial class PackageCardViewModel : ProgressViewModel
ProcessRunner.OpenUrl(basePackage.GithubUrl); ProcessRunner.OpenUrl(basePackage.GithubUrl);
} }
[RelayCommand]
private async Task Stop()
{
if (Package is null)
return;
await runningPackageService.StopPackage(Package.Id);
IsRunning = false;
ShowWebUiButton = false;
}
[RelayCommand]
private async Task Restart()
{
await Stop();
await Launch();
}
[RelayCommand]
private async Task ShowLaunchOptions()
{
var basePackage = packageFactory.FindPackageByName(Package?.PackageName);
if (basePackage == null)
{
logger.LogWarning("Package {Name} not found", Package?.PackageName);
return;
}
var viewModel = vmFactory.Get<LaunchOptionsViewModel>();
viewModel.Cards = LaunchOptionCard
.FromDefinitions(basePackage.LaunchOptions, Package?.LaunchArgs ?? [])
.ToImmutableArray();
logger.LogDebug("Launching config dialog with cards: {CardsCount}", viewModel.Cards.Count);
var dialog = new BetterContentDialog
{
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
IsPrimaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Save,
CloseButtonText = Resources.Action_Cancel,
FullSizeDesired = true,
DefaultButton = ContentDialogButton.Primary,
ContentMargin = new Thickness(32, 16),
Padding = new Thickness(0, 16),
Content = new LaunchOptionsDialog { DataContext = viewModel, }
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && Package != null)
{
// Save config
var args = viewModel.AsLaunchArgs();
settingsManager.SaveLaunchArgs(Package.Id, args);
}
}
private async Task<bool> HasUpdate() private async Task<bool> HasUpdate()
{ {
if (Package == null || IsUnknownPackage || Design.IsDesignMode) if (Package == null || IsUnknownPackage || Design.IsDesignMode)
@ -565,4 +778,10 @@ public partial class PackageCardViewModel : ProgressViewModel
IsSharedModelConfig = false; IsSharedModelConfig = false;
} }
} }
private void RunningPackageOnStartupComplete(object? sender, string e)
{
webUiUrl = e.Replace("0.0.0.0", "127.0.0.1");
ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl);
}
} }

2
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs

@ -281,7 +281,7 @@ public partial class PackageInstallDetailViewModel(
SelectedVersion = !IsReleaseMode SelectedVersion = !IsReleaseMode
? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch) ? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch)
?? AvailableVersions?.FirstOrDefault() ?? AvailableVersions?.FirstOrDefault()
: AvailableVersions?.FirstOrDefault(); : AvailableVersions?.FirstOrDefault(v => !v.IsPrerelease);
CanInstall = !ShowDuplicateWarning; CanInstall = !ShowDuplicateWarning;
} }

15
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -6,8 +6,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using DynamicData; using DynamicData;
@ -15,18 +13,15 @@ using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
@ -48,7 +43,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
private readonly INavigationService<NewPackageManagerViewModel> packageNavigationService; private readonly INavigationService<NewPackageManagerViewModel> packageNavigationService;
private readonly ILogger<PackageManagerViewModel> logger; private readonly ILogger<PackageManagerViewModel> logger;
public override string Title => "Packages"; public override string Title => Resources.Label_Packages;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true }; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true };
/// <summary> /// <summary>
@ -84,6 +79,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
this.logger = logger; this.logger = logger;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished;
var installed = installedPackages.Connect(); var installed = installedPackages.Connect();
var unknown = unknownInstalledPackages.Connect(); var unknown = unknownInstalledPackages.Connect();
@ -107,6 +103,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
timer.Tick += async (_, _) => await CheckPackagesForUpdates(); timer.Tick += async (_, _) => await CheckPackagesForUpdates();
} }
private void OnOneClickInstallFinished(object? sender, bool e)
{
OnLoadedAsync().SafeFireAndForget();
}
public void SetPackages(IEnumerable<InstalledPackage> packages) public void SetPackages(IEnumerable<InstalledPackage> packages)
{ {
installedPackages.Edit(s => s.Load(packages)); installedPackages.Edit(s => s.Load(packages));

161
StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs

@ -0,0 +1,161 @@
using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(ConsoleOutputPage))]
public partial class RunningPackageViewModel : PageViewModelBase, IDisposable, IAsyncDisposable
{
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly RunningPackageService runningPackageService;
public PackagePair RunningPackage { get; }
public ConsoleViewModel Console { get; }
public override string Title => RunningPackage.InstalledPackage.PackageName ?? "Running Package";
public override IconSource IconSource => new SymbolIconSource();
[ObservableProperty]
private bool autoScrollToEnd;
[ObservableProperty]
private bool showWebUiButton;
[ObservableProperty]
private string webUiUrl = string.Empty;
[ObservableProperty]
private bool isRunning = true;
[ObservableProperty]
private string consoleInput = string.Empty;
[ObservableProperty]
private bool showWebUiTeachingTip;
/// <inheritdoc/>
public RunningPackageViewModel(
ISettingsManager settingsManager,
INotificationService notificationService,
RunningPackageService runningPackageService,
PackagePair runningPackage,
ConsoleViewModel console
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.runningPackageService = runningPackageService;
RunningPackage = runningPackage;
Console = console;
Console.Document.LineCountChanged += DocumentOnLineCountChanged;
RunningPackage.BasePackage.StartupComplete += BasePackageOnStartupComplete;
RunningPackage.BasePackage.Exited += BasePackageOnExited;
settingsManager.RelayPropertyFor(
this,
vm => vm.AutoScrollToEnd,
settings => settings.AutoScrollLaunchConsoleToEnd,
true
);
}
private void BasePackageOnExited(object? sender, int e)
{
IsRunning = false;
ShowWebUiButton = false;
Console.Document.LineCountChanged -= DocumentOnLineCountChanged;
RunningPackage.BasePackage.StartupComplete -= BasePackageOnStartupComplete;
RunningPackage.BasePackage.Exited -= BasePackageOnExited;
runningPackageService.RunningPackages.Remove(RunningPackage.InstalledPackage.Id);
}
private void BasePackageOnStartupComplete(object? sender, string url)
{
WebUiUrl = url.Replace("0.0.0.0", "127.0.0.1");
ShowWebUiButton = !string.IsNullOrWhiteSpace(WebUiUrl);
if (settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.WebUiButtonMovedTip))
return;
ShowWebUiTeachingTip = true;
settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.WebUiButtonMovedTip));
}
private void DocumentOnLineCountChanged(object? sender, EventArgs e)
{
if (AutoScrollToEnd)
{
EventManager.Instance.OnScrollToBottomRequested();
}
}
[RelayCommand]
private void LaunchPackage()
{
EventManager.Instance.OnPackageRelaunchRequested(RunningPackage.InstalledPackage);
}
[RelayCommand]
private async Task Stop()
{
await runningPackageService.StopPackage(RunningPackage.InstalledPackage.Id);
Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}");
await Console.StopUpdatesAsync();
IsRunning = false;
}
[RelayCommand]
private void LaunchWebUi()
{
if (string.IsNullOrEmpty(WebUiUrl))
return;
notificationService.TryAsync(
Task.Run(() => ProcessRunner.OpenUrl(WebUiUrl)),
"Failed to open URL",
$"{WebUiUrl}"
);
}
[RelayCommand]
private async Task SendToConsole()
{
Console.PostLine(ConsoleInput);
if (RunningPackage?.BasePackage is BaseGitPackage gitPackage)
{
var venv = gitPackage.VenvRunner;
var process = venv?.Process;
if (process is not null)
{
await process.StandardInput.WriteLineAsync(ConsoleInput);
}
}
ConsoleInput = string.Empty;
}
public void Dispose()
{
Console.Dispose();
}
public async ValueTask DisposeAsync()
{
await Console.DisposeAsync();
}
}

93
StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml

@ -0,0 +1,93 @@
<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:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:RunningPackageViewModel"
x:Class="StabilityMatrix.Avalonia.Views.ConsoleOutputPage">
<Grid RowDefinitions="Auto, *, Auto">
<ui:CommandBar Grid.Row="0"
Margin="12,0,0,4"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton
IconSource="Stop"
IsVisible="{Binding IsRunning}"
Classes="danger"
Command="{Binding StopCommand}"
VerticalAlignment="Center"
Label="{x:Static lang:Resources.Action_Stop}" />
<ui:CommandBarButton
IconSource="Play"
IsVisible="{Binding !IsRunning}"
Classes="success"
Command="{Binding LaunchPackageCommand}"
VerticalAlignment="Center"
Label="{x:Static lang:Resources.Action_Launch}" />
<ui:CommandBarSeparator Margin="6,0,4,0"/>
<ui:CommandBarToggleButton
VerticalAlignment="Center"
IsChecked="{Binding AutoScrollToEnd}"
Label="{x:Static lang:Resources.Label_ToggleAutoScrolling}"
ToolTip.Tip="{x:Static lang:Resources.Label_AutoScrollToEnd}">
<ui:CommandBarToggleButton.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-arrow-down-wide-short" />
</ui:CommandBarToggleButton.IconSource>
</ui:CommandBarToggleButton>
<ui:CommandBarButton
Command="{Binding LaunchWebUiCommand}"
IsVisible="{Binding ShowWebUiButton}"
VerticalAlignment="Center"
x:Name="OpenWebUiButton"
Label="{x:Static lang:Resources.Action_OpenWebUI}">
<ui:CommandBarButton.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-up-right-from-square"/>
</ui:CommandBarButton.IconSource>
</ui:CommandBarButton>
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
<avaloniaEdit:TextEditor
Grid.Row="1"
x:Name="Console"
Margin="8,8,16,10"
DataContext="{Binding Console}"
Document="{Binding Document}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
IsReadOnly="True"
LineNumbersForeground="DarkSlateGray"
ShowLineNumbers="True"
VerticalScrollBarVisibility="Auto"
WordWrap="True" />
<Grid Grid.Row="2" ColumnDefinitions="*, Auto"
Margin="16,4,16,16">
<TextBox Grid.Column="0" Text="{Binding ConsoleInput, Mode=TwoWay}"
Margin="0,0,8,0"/>
<Button Grid.Column="1"
Classes="accent"
IsDefault="True"
Content="{x:Static lang:Resources.Action_Send}"
Command="{Binding SendToConsoleCommand}"/>
</Grid>
<ui:TeachingTip Grid.Row="0" Grid.Column="0" Name="TeachingTip1"
Target="{Binding #OpenWebUiButton}"
Title="{x:Static lang:Resources.TeachingTip_WebUiButtonMoved}"
PreferredPlacement="Bottom"
IsOpen="True"/>
</Grid>
</controls:UserControlBase>

54
StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml.cs

@ -0,0 +1,54 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Threading;
using AvaloniaEdit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.Views;
[Transient]
public partial class ConsoleOutputPage : UserControlBase
{
private const int LineOffset = 5;
public ConsoleOutputPage()
{
InitializeComponent();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
TextEditorConfigs.Configure(Console, TextEditorPreset.Console);
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
EventManager.Instance.ScrollToBottomRequested -= OnScrollToBottomRequested;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
EventManager.Instance.ScrollToBottomRequested += OnScrollToBottomRequested;
}
private void OnScrollToBottomRequested(object? sender, EventArgs e)
{
Dispatcher.UIThread.Invoke(() =>
{
var editor = this.FindControl<TextEditor>("Console");
if (editor?.Document == null)
return;
var line = Math.Max(editor.Document.LineCount - LineOffset, 1);
editor.ScrollToLine(line);
});
}
}

46
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading; using System.Threading;
using AsyncAwaitBestPractices;
using AsyncImageLoader; using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -193,13 +194,52 @@ public partial class MainWindow : AppWindowBase
protected override void OnClosing(WindowClosingEventArgs e) protected override void OnClosing(WindowClosingEventArgs e)
{ {
// Show confirmation if package running // Show confirmation if package running
var launchPageViewModel = App.Services.GetRequiredService<LaunchPageViewModel>(); var runningPackageService = App.Services.GetRequiredService<RunningPackageService>();
if (
launchPageViewModel.OnMainWindowClosing(e); runningPackageService.RunningPackages.Count > 0
&& e.CloseReason is WindowCloseReason.WindowClosing
)
{
e.Cancel = true;
var dialog = CreateExitConfirmDialog();
Dispatcher
.UIThread.InvokeAsync(async () =>
{
if (
(TaskDialogStandardResult)await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes
)
{
App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown();
}
})
.SafeFireAndForget();
}
base.OnClosing(e); base.OnClosing(e);
} }
private static TaskDialog CreateExitConfirmDialog()
{
var dialog = DialogHelper.CreateTaskDialog(
Languages.Resources.Label_ConfirmExit,
Languages.Resources.Label_ConfirmExitDetail
);
dialog.ShowProgressBar = false;
dialog.FooterVisibility = TaskDialogFooterVisibility.Never;
dialog.Buttons = new List<TaskDialogButton>
{
new("Exit", TaskDialogStandardResult.Yes),
TaskDialogButton.CancelButton
};
dialog.Buttons[0].IsDefault = true;
return dialog;
}
/// <inheritdoc /> /// <inheritdoc />
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {

110
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -23,13 +23,29 @@
x:DataType="vm:OutputsPageViewModel" x:DataType="vm:OutputsPageViewModel"
Focusable="True" Focusable="True"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid Margin="16" RowDefinitions="Auto,*"> <Grid Margin="16" RowDefinitions="Auto,*"
<Grid ColumnDefinitions="Auto, *">
Grid.Row="0"
Margin="0,0,0,16" <TreeView Grid.Row="1"
ColumnDefinitions="Auto,Auto,*,Auto" Grid.Column="0"
RowDefinitions="Auto,*"> IsVisible="{Binding ShowFolders}"
Margin="0,0,12,0"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding SubDirectories}">
<TextBlock Text="{Binding Name}" />
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Grid Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Margin="0,0,0,16"
ColumnDefinitions="Auto,Auto,*,Auto"
RowDefinitions="Auto,*">
<Grid.Styles> <Grid.Styles>
<Style Selector="TextBox"> <Style Selector="TextBox">
<Setter Property="MinHeight" Value="40" /> <Setter Property="MinHeight" Value="40" />
@ -39,56 +55,6 @@
</Style> </Style>
</Grid.Styles> </Grid.Styles>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="4"
Text="{x:Static lang:Resources.Label_OutputFolder}" />
<ComboBox
Grid.Row="1"
Grid.Column="0"
MinWidth="150"
Margin="4,0"
VerticalAlignment="Center"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}">
<ComboBox.Styles>
<Style Selector="ComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ComboBox.Styles>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models1:PackageOutputCategory}">
<StackPanel>
<TextBlock Margin="0,4,0,4" Text="{Binding Name, Mode=OneWay}" />
<TextBlock Text="{Binding Path, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="4"
IsVisible="{Binding CanShowOutputTypes}"
Text="{x:Static lang:Resources.Label_OutputType}" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
MinWidth="150"
Margin="4,0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
IsVisible="{Binding CanShowOutputTypes}"
ItemsSource="{Binding OutputTypes}"
SelectedItem="{Binding SelectedOutputType}" />
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="4"
Text="{x:Static lang:Resources.Action_Search}" />
<TextBox <TextBox
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
@ -104,8 +70,8 @@
<Button Classes="transparent-full" <Button Classes="transparent-full"
IsVisible="{Binding SearchQuery.Length}" IsVisible="{Binding SearchQuery.Length}"
Command="{Binding ClearSearchQuery}"> Command="{Binding ClearSearchQuery}">
<ui:SymbolIcon Symbol="Cancel" /> <ui:SymbolIcon Symbol="Cancel" />
</Button> </Button>
<ui:SymbolIcon <ui:SymbolIcon
IsVisible="{Binding !SearchQuery.Length}" IsVisible="{Binding !SearchQuery.Length}"
Margin="0,0,10,0" Margin="0,0,10,0"
@ -129,27 +95,31 @@
VerticalAlignment="Center" VerticalAlignment="Center"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
DefaultLabelPosition="Right"> DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands> <ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton <ui:CommandBarButton
IsEnabled="{Binding !!NumItemsSelected}" IsEnabled="{Binding !!NumItemsSelected}"
Command="{Binding DeleteAllSelected}" Command="{Binding DeleteAllSelected}"
IconSource="Delete" /> IconSource="Delete" />
<ui:CommandBarSeparator /> <ui:CommandBarSeparator />
<ui:CommandBarButton <ui:CommandBarButton
Command="{Binding SelectAll}" Command="{Binding SelectAll}"
IconSource="SelectAll" IconSource="SelectAll"
Label="{x:Static lang:Resources.Action_SelectAll}" /> Label="{x:Static lang:Resources.Action_SelectAll}" />
<ui:CommandBarButton <ui:CommandBarButton
Command="{Binding ClearSelection}" Command="{Binding ClearSelection}"
IconSource="ClearSelection" IconSource="ClearSelection"
IsEnabled="{Binding !!NumItemsSelected}" IsEnabled="{Binding !!NumItemsSelected}"
Label="{x:Static lang:Resources.Action_ClearSelection}" /> Label="{x:Static lang:Resources.Action_ClearSelection}" />
<ui:CommandBarSeparator /> <ui:CommandBarSeparator />
<ui:CommandBarButton <ui:CommandBarButton
IconSource="Refresh" IconSource="Refresh"
Command="{Binding Refresh}" Command="{Binding Refresh}"
Label="{x:Static lang:Resources.Action_Refresh}" /> Label="{x:Static lang:Resources.Action_Refresh}" />
<ui:CommandBarToggleButton
IconSource="List"
IsChecked="{Binding ShowFolders}"
Label="Show Folders"/>
</ui:CommandBar.PrimaryCommands> </ui:CommandBar.PrimaryCommands>
<ui:CommandBar.SecondaryCommands> <ui:CommandBar.SecondaryCommands>
@ -161,7 +131,8 @@
</ui:CommandBar> </ui:CommandBar>
</Grid> </Grid>
<scroll:BetterScrollViewer Grid.Row="1" PointerWheelChanged="ScrollViewer_MouseWheelChanged"> <scroll:BetterScrollViewer Grid.Column="1"
Grid.Row="1" PointerWheelChanged="ScrollViewer_MouseWheelChanged">
<ItemsRepeater <ItemsRepeater
x:Name="ImageRepeater" x:Name="ImageRepeater"
VerticalAlignment="Top" VerticalAlignment="Top"
@ -206,7 +177,8 @@
IconSource="Delete" IconSource="Delete"
Text="{x:Static lang:Resources.Action_Delete}" /> Text="{x:Static lang:Resources.Action_Delete}" />
<ui:MenuFlyoutSeparator IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" /> <ui:MenuFlyoutSeparator
IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ui:MenuFlyoutSubItem <ui:MenuFlyoutSubItem
IconSource="Share" IconSource="Share"

276
StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml

@ -40,151 +40,151 @@
CornerRadius="4" CornerRadius="4"
Source="{Binding PreviewImageUri}" /> Source="{Binding PreviewImageUri}" />
<StackPanel Grid.Column="1" Orientation="Vertical"> <StackPanel Grid.Column="1" Orientation="Vertical">
<TextBlock Text="{Binding DisplayName}" <TextBlock Text="{Binding DisplayName}"
FontWeight="Light" FontWeight="Light"
Margin="16,0,0,0" Margin="16,0,0,0"
FontSize="20" /> FontSize="20" />
<TextBlock Text="{Binding ByAuthor}" <TextBlock Text="{Binding ByAuthor}"
FontWeight="Light" FontWeight="Light"
Margin="16,0,0,0" Margin="16,0,0,0"
FontSize="13" /> FontSize="13" />
<TextBlock TextWrapping="Wrap" <TextBlock TextWrapping="Wrap"
Text="{Binding Blurb}" Text="{Binding Blurb}"
FontWeight="Light" FontWeight="Light"
Margin="16,8,0,8" Margin="16,8,0,8"
FontSize="16" /> FontSize="16" />
<TextBlock TextWrapping="Wrap" <TextBlock TextWrapping="Wrap"
Text="{Binding Disclaimer}" Text="{Binding Disclaimer}"
IsVisible="{Binding Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" IsVisible="{Binding Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Foreground="OrangeRed" Foreground="OrangeRed"
FontWeight="Light" FontWeight="Light"
Margin="16,-4,0,4" Margin="16,-4,0,4"
FontSize="14" /> FontSize="14" />
<ItemsRepeater Margin="16, 8, 0, 0" <ItemsRepeater Margin="16, 8, 0, 0"
ItemsSource="{Binding AvailableTorchVersions}"> ItemsSource="{Binding AvailableTorchVersions}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<StackLayout Orientation="Horizontal" /> <StackLayout Orientation="Horizontal" />
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type models:TorchVersion}"> <DataTemplate DataType="{x:Type models:TorchVersion}">
<controls:Card <controls:Card
Tag="{Binding }" Tag="{Binding }"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Padding="4" Padding="4"
Margin="0,0,8,0" Margin="0,0,8,0"
VerticalAlignment="Top"> VerticalAlignment="Top">
<controls:Card.Styles> <controls:Card.Styles>
<Style Selector="controls|Card[Tag=Cuda]"> <Style Selector="controls|Card[Tag=Cuda]">
<Setter Property="Background" <Setter Property="Background"
Value="{DynamicResource ThemeGreenColorTransparent}" /> Value="{DynamicResource ThemeGreenColorTransparent}" />
<Setter Property="BorderBrush" <Setter Property="BorderBrush"
Value="{DynamicResource ThemeGreenColorTransparent}" /> Value="{DynamicResource ThemeGreenColorTransparent}" />
<Style <Style
Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" <Setter Property="Foreground"
Value="{DynamicResource ButtonForeground}" /> Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="NVIDIA"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style> </Style>
<Setter Property="Content"> <Style Selector="controls|Card[Tag=Rocm]">
<Template> <Setter Property="Background"
<TextBlock Value="{DynamicResource ThemeDarkRedColor}" />
FontWeight="Medium" <Setter Property="BorderBrush"
HorizontalAlignment="Center" Value="{DynamicResource ThemeDarkRedColor}" />
Text="NVIDIA" <Style
TextAlignment="Center" Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
VerticalAlignment="Center" /> <Setter Property="Foreground"
</Template> Value="{DynamicResource ButtonForeground}" />
</Setter> </Style>
</Style> <Setter Property="Content">
<Style Selector="controls|Card[Tag=Rocm]"> <Template>
<Setter Property="Background" <TextBlock
Value="{DynamicResource ThemeDarkRedColor}" /> FontWeight="Medium"
<Setter Property="BorderBrush" HorizontalAlignment="Center"
Value="{DynamicResource ThemeDarkRedColor}" /> Text="AMD (Linux)"
<Style TextAlignment="Center"
Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> VerticalAlignment="Center"
<Setter Property="Foreground" ToolTip.Tip="For AMD GPUs that support ROCm on Linux" />
Value="{DynamicResource ButtonForeground}" /> </Template>
</Setter>
</Style> </Style>
<Setter Property="Content"> <Style Selector="controls|Card[Tag=DirectMl]">
<Template> <Setter Property="Background"
<TextBlock Value="{DynamicResource ThemeDarkBlueColor}" />
FontWeight="Medium" <Setter Property="BorderBrush"
HorizontalAlignment="Center" Value="{DynamicResource ThemeDarkBlueColor}" />
Text="AMD (Linux)" <Setter Property="Content">
TextAlignment="Center" <Template>
VerticalAlignment="Center" <TextBlock
ToolTip.Tip="For AMD GPUs that support ROCm on Linux" /> FontWeight="Medium"
</Template> HorizontalAlignment="Center"
</Setter> Text="DirectML"
</Style> TextAlignment="Center"
<Style Selector="controls|Card[Tag=DirectMl]"> VerticalAlignment="Center"
<Setter Property="Background" ToolTip.Tip="For any DirectX compatible GPU on Windows" />
Value="{DynamicResource ThemeDarkBlueColor}" /> </Template>
<Setter Property="BorderBrush" </Setter>
Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="DirectML"
TextAlignment="Center"
VerticalAlignment="Center"
ToolTip.Tip="For any DirectX compatible GPU on Windows" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Mps]">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="White" />
<Style
Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground"
Value="{DynamicResource ButtonForeground}" />
</Style> </Style>
<Setter Property="Content"> <Style Selector="controls|Card[Tag=Mps]">
<Template> <Setter Property="Background" Value="White" />
<TextBlock <Setter Property="BorderBrush" Value="White" />
FontWeight="Medium" <Style
HorizontalAlignment="Center" Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
Text="macOS" <Setter Property="Foreground"
TextAlignment="Center" Value="{DynamicResource ButtonForeground}" />
Foreground="Black" </Style>
VerticalAlignment="Center" /> <Setter Property="Content">
</Template> <Template>
</Setter> <TextBlock
</Style> FontWeight="Medium"
<Style Selector="controls|Card[Tag=Cpu]"> HorizontalAlignment="Center"
<Setter Property="Background" Text="macOS"
Value="{DynamicResource ThemeBlueGreyColor}" /> TextAlignment="Center"
<Setter Property="BorderBrush" Foreground="Black"
Value="{DynamicResource ThemeBlueGreyColor}" /> VerticalAlignment="Center" />
<Style </Template>
Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> </Setter>
<Setter Property="Foreground"
Value="{DynamicResource ButtonForeground}" />
</Style> </Style>
<Setter Property="Content"> <Style Selector="controls|Card[Tag=Cpu]">
<Template> <Setter Property="Background"
<TextBlock Value="{DynamicResource ThemeBlueGreyColor}" />
FontWeight="Medium" <Setter Property="BorderBrush"
HorizontalAlignment="Center" Value="{DynamicResource ThemeBlueGreyColor}" />
Text="CPU" <Style
TextAlignment="Center" Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
VerticalAlignment="Center" /> <Setter Property="Foreground"
</Template> Value="{DynamicResource ButtonForeground}" />
</Setter> </Style>
</Style> <Setter Property="Content">
</controls:Card.Styles> <Template>
</controls:Card> <TextBlock
</DataTemplate> FontWeight="Medium"
</ItemsRepeater.ItemTemplate> HorizontalAlignment="Center"
</ItemsRepeater> Text="CPU"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
</controls:Card.Styles>
</controls:Card>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel> </StackPanel>

595
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -10,15 +10,23 @@
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:system="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:avalonia="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:markupExtensions="clr-namespace:StabilityMatrix.Avalonia.MarkupExtensions"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450"
x:DataType="viewModels:PackageManagerViewModel" x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True" x:CompileBindings="True"
d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}" d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage"> x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage">
<controls:UserControlBase.Resources>
<converters:BooleanChoiceMultiConverter x:Key="BoolChoiceMultiConverter" />
</controls:UserControlBase.Resources>
<Grid Margin="16" RowDefinitions="Auto,*,Auto"> <Grid Margin="16" RowDefinitions="Auto,*,Auto">
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1">
<ItemsRepeater <ItemsRepeater
x:Name="PackageCardsRepeater"
ItemsSource="{Binding PackageCards}"> ItemsSource="{Binding PackageCards}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" /> <UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" />
@ -26,185 +34,34 @@
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}"> <DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}">
<controls:Card Padding="8" <controls:Card Padding="8"
MinWidth="400"
CornerRadius="8"> CornerRadius="8">
<Grid RowDefinitions="Auto, Auto, Auto, Auto" <controls:Card.Background>
ColumnDefinitions="*,Auto"> <MultiBinding Converter="{StaticResource BoolChoiceMultiConverter}">
<Binding Path="IsRunning" />
<TextBlock Grid.Row="0" <DynamicResource ResourceKey="ThemeDarkDarkGreenColorBrush" />
Grid.Column="0" <DynamicResource ResourceKey="ButtonBackground" />
ToolTip.Tip="{Binding Package.DisplayName, FallbackValue=''}" </MultiBinding>
Text="{Binding Package.DisplayName, FallbackValue=''}" </controls:Card.Background>
TextTrimming="WordEllipsis" <Grid ColumnDefinitions="Auto, *">
TextAlignment="Left" />
<Button
Grid.Row="0"
Grid.Column="1"
HorizontalContentAlignment="Right"
HorizontalAlignment="Right"
VerticalContentAlignment="Top"
VerticalAlignment="Top"
Padding="4,1"
Margin="4,0,0,0"
Classes="transparent"
Width="24"
BorderThickness="0">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{x:Static lang:Resources.Action_CheckForUpdates}"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OnLoadedAsync}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Refresh" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Header="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer}, macOS={x:Static lang:Resources.Action_OpenInFinder}}"
Command="{Binding OpenFolder}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="OpenFolder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Action_OpenGithub}"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OpenOnGitHubCommand}">
<MenuItem.Icon>
<icons:Icon
Value="fa-brands fa-github" />
</MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem
Header="{x:Static lang:Resources.Label_PythonPackages}"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OpenPythonPackagesDialogCommand}">
<MenuItem.Icon>
<icons:Icon
Value="fa-brands fa-python" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Header="Extensions"
IsVisible="{Binding CanUseExtensions}"
Command="{Binding OpenExtensionsDialogCommand}">
<MenuItem.Icon>
<icons:Icon
Value="fa-solid fa-puzzle-piece" />
</MenuItem.Icon>
</MenuItem>
<Separator IsVisible="{Binding CanUseSharedOutput}" />
<MenuItem Header="{x:Static lang:Resources.Label_SharedModelStrategyShort}"
IsVisible="{Binding !IsUnknownPackage}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="FolderLink" />
</MenuItem.Icon>
<!-- ReSharper disable Xaml.RedundantResource -->
<MenuItem Header="Symlink"
Command="{Binding ToggleSharedModelSymlink}"
IsVisible="{Binding CanUseSymlinkMethod}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelSymlink}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Config"
Command="{Binding ToggleSharedModelConfig}"
IsVisible="{Binding CanUseConfigMethod}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding IsSharedModelConfig}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="None"
Command="{Binding ToggleSharedModelNone}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelDisabled}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<!-- ReSharper enable Xaml.RedundantResource -->
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Label_UseSharedOutputFolder}"
Command="{Binding ToggleSharedOutput}"
IsVisible="{Binding CanUseSharedOutput}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding UseSharedOutput}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{x:Static lang:Resources.Action_Uninstall}"
Command="{Binding Uninstall}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
<TextBlock Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="2,0,0,0"
VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<!-- Normal packages shows image -->
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2" Height="150"
Margin="0,8,0,8" Width="150"
Height="180" Margin="4, 8"
Width="225" CornerRadius="8"
CornerRadius="4"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalContentAlignment="Top" VerticalContentAlignment="Top"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Source="{Binding CardImageSource}" Source="{Binding CardImageSource}"
IsVisible="{Binding !IsUnknownPackage}" IsVisible="{Binding !IsUnknownPackage}"
Stretch="UniformToFill" /> Stretch="UniformToFill" />
<!-- Unknown packages panel -->
<Border <Border
Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2" Height="150"
Margin="0,8,0,8" Width="150"
Height="180" Margin="4, 8"
Width="225" CornerRadius="8"
CornerRadius="4"
HorizontalAlignment="Center" HorizontalAlignment="Center"
IsVisible="{Binding IsUnknownPackage}" IsVisible="{Binding IsUnknownPackage}"
Background="#202020"> Background="#202020">
@ -215,99 +72,340 @@
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{x:Static lang:Resources.Label_UnknownPackage}" /> Text="{x:Static lang:Resources.Label_UnknownPackage}" />
</Border> </Border>
<Grid Grid.Column="1" RowDefinitions="Auto, Auto, Auto, *"
ColumnDefinitions="Auto, *">
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="{Binding Package.DisplayName}"
Margin="8,0,8,0"
FontWeight="SemiBold"
MaxWidth="200"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding Package.DisplayName}"
FontSize="16" />
<TextBlock Grid.Column="0"
Grid.Row="1"
FontSize="13"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Margin="8,0,8,0"
VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<Button
Grid.Row="0"
Grid.Column="1"
HorizontalContentAlignment="Right"
HorizontalAlignment="Right"
VerticalContentAlignment="Top"
VerticalAlignment="Top"
Padding="4"
Margin="4,0,0,0"
Classes="transparent"
Width="24"
BorderThickness="0">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{x:Static lang:Resources.Action_CheckForUpdates}"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OnLoadedAsync}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Refresh" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Header="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer}, macOS={x:Static lang:Resources.Action_OpenInFinder}}"
Command="{Binding OpenFolder}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="OpenFolder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Action_OpenGithub}"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OpenOnGitHubCommand}">
<MenuItem.Icon>
<icons:Icon
Value="fa-brands fa-github" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<Grid <MenuItem
Grid.Row="3" Header="{x:Static lang:Resources.Label_PythonPackages}"
Grid.Column="0" IsVisible="{Binding !IsUnknownPackage}"
Grid.ColumnSpan="2" Command="{Binding OpenPythonPackagesDialogCommand}">
IsVisible="{Binding IsUpdateAvailable}" <MenuItem.Icon>
ColumnDefinitions="*, *"> <icons:Icon
<!-- Launch and update buttons --> Value="fa-brands fa-python" />
<Button Grid.Column="0" Classes="accent" </MenuItem.Icon>
VerticalAlignment="Bottom" </MenuItem>
HorizontalAlignment="Stretch" <MenuItem
Command="{Binding Launch}"> Header="Extensions"
<StackPanel Orientation="Horizontal" Margin="0,2,0,2"> IsVisible="{Binding CanUseExtensions}"
<icons:Icon Value="fa-solid fa-rocket" Command="{Binding OpenExtensionsDialogCommand}">
Margin="0,0,8,0" <MenuItem.Icon>
FontSize="14" /> <icons:Icon
<TextBlock Text="{x:Static lang:Resources.Action_Launch}" /> Value="fa-solid fa-puzzle-piece" />
</StackPanel> </MenuItem.Icon>
</Button> </MenuItem>
<Button Grid.Column="1" Classes="accent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Update}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<icons:Icon Value="fa-solid fa-download"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Update}" />
</StackPanel>
</Button>
</Grid>
<!-- Big launch button --> <Separator IsVisible="{Binding CanUseSharedOutput}" />
<Button
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Classes="accent"
VerticalAlignment="Bottom"
Command="{Binding Launch}"
HorizontalAlignment="Stretch">
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsUpdateAvailable" />
<Binding Path="!IsUnknownPackage" />
</MultiBinding>
</Button.IsVisible>
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<icons:Icon Value="fa-solid fa-rocket"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Launch}" />
</StackPanel>
</Button>
<!-- Import button (for unknown) --> <MenuItem
<Button Grid.Row="3" Grid.Column="0" Classes="transparent-info" Header="{x:Static lang:Resources.Label_SharedModelStrategyShort}"
Grid.ColumnSpan="2" IsVisible="{Binding !IsUnknownPackage}">
VerticalAlignment="Bottom" <MenuItem.Icon>
HorizontalAlignment="Stretch" <ui:SymbolIcon Symbol="FolderLink" />
Command="{Binding Import}" </MenuItem.Icon>
IsVisible="{Binding IsUnknownPackage}"> <!-- ReSharper disable Xaml.RedundantResource -->
<StackPanel Orientation="Horizontal" Margin="0,2,0,2"> <MenuItem Header="Symlink"
<icons:Icon Value="fa-solid fa-circle-question" Command="{Binding ToggleSharedModelSymlink}"
Margin="0,0,8,0" IsVisible="{Binding CanUseSymlinkMethod}">
FontSize="14" /> <MenuItem.Icon>
<TextBlock Text="{x:Static lang:Resources.Action_Import}" /> <CheckBox IsChecked="{Binding IsSharedModelSymlink}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Config"
Command="{Binding ToggleSharedModelConfig}"
IsVisible="{Binding CanUseConfigMethod}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding IsSharedModelConfig}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="None"
Command="{Binding ToggleSharedModelNone}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelDisabled}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<!-- ReSharper enable Xaml.RedundantResource -->
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Label_UseSharedOutputFolder}"
Command="{Binding ToggleSharedOutput}"
IsVisible="{Binding CanUseSharedOutput}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding UseSharedOutput}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{x:Static lang:Resources.Action_Uninstall}"
Command="{Binding Uninstall}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
<StackPanel Grid.Row="2"
Grid.Column="0"
Margin="0,4,0,0"
Orientation="Horizontal">
<Button Classes="transparent-full"
Margin="4,4,0,0"
Padding="4"
ToolTip.Tip="{x:Static lang:Resources.Action_CheckForUpdates}"
Command="{Binding OnLoadedAsync}"
>
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsUnknownPackage" />
<Binding Path="!IsUpdateAvailable" />
</MultiBinding>
</Button.IsVisible>
<ui:SymbolIcon Symbol="Refresh"
FontSize="20"/>
</Button>
<Button Classes="transparent-full"
Margin="4,4,0,0"
Padding="4"
IsEnabled="{Binding !IsRunning}"
ToolTip.Tip="{x:Static lang:Resources.Label_UpdateAvailable}"
markupExtensions:ShowDisabledTooltipExtension.ShowOnDisabled="True"
Command="{Binding Update}">
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsUnknownPackage" />
<Binding Path="IsUpdateAvailable" />
</MultiBinding>
</Button.IsVisible>
<ui:SymbolIcon Symbol="CloudDownload"
IsEnabled="{Binding !IsRunning}"
FontSize="24">
<ui:SymbolIcon.Styles>
<Style Selector="ui|SymbolIcon">
<Setter Property="Foreground" Value="Lime" />
</Style>
<Style Selector="ui|SymbolIcon:disabled">
<Setter Property="Foreground" Value="Gray" />
</Style>
</ui:SymbolIcon.Styles>
</ui:SymbolIcon>
</Button>
<Button Classes="transparent-full"
Margin="4,4,0,0"
Padding="4"
ToolTip.Tip="Launch Options"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding ShowLaunchOptionsCommand}">
<ui:SymbolIcon Symbol="SettingsFilled"
FontSize="20"/>
</Button>
<Button Classes="transparent-full"
Margin="6,2,0,0"
Padding="4"
ToolTip.Tip="Extensions"
IsVisible="{Binding CanUseExtensions}"
Command="{Binding OpenExtensionsDialogCommand}">
<icons:Icon FontSize="18"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Value="fa-solid fa-puzzle-piece" />
</Button>
</StackPanel> </StackPanel>
</Button> <UniformGrid Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="3"
VerticalAlignment="Bottom"
Margin="8,8,8,0">
<Button Classes="accent"
Name="LaunchButton"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Launch}">
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsRunning" />
<Binding Path="!IsUnknownPackage" />
</MultiBinding>
</Button.IsVisible>
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<icons:Icon Value="fa-solid fa-rocket"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Launch}" />
</StackPanel>
</Button>
<Button Classes="borderless-danger"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
IsVisible="{Binding IsRunning}"
Command="{Binding StopCommand}">
<StackPanel Orientation="Horizontal"
Margin="0,2">
<icons:Icon Value="fa-solid fa-stop"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Stop}" />
</StackPanel>
</Button>
<Button Classes="borderless-info"
BorderBrush="Transparent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
IsVisible="{Binding IsRunning}"
Command="{Binding RestartCommand}">
<StackPanel Orientation="Horizontal"
Margin="0,2">
<icons:Icon Value="fa-solid fa-arrow-rotate-left"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Restart}" />
</StackPanel>
</Button>
<Button Classes="accent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
IsVisible="{Binding IsRunning}"
Command="{Binding NavToConsole}">
<StackPanel Orientation="Horizontal"
Margin="0,2,0,2">
<icons:Icon Value="fa-solid fa-terminal"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Label_Console}" />
</StackPanel>
</Button>
<Button Classes="accent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
IsVisible="{Binding ShowWebUiButton}"
Command="{Binding LaunchWebUi}">
<StackPanel Orientation="Horizontal" Margin="8,2,8,2">
<icons:Icon Value="fa-solid fa-up-right-from-square"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Label_WebUi}" />
</StackPanel>
</Button>
<Button Classes="transparent-info"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Import}"
IsVisible="{Binding IsUnknownPackage}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<icons:Icon Value="fa-solid fa-circle-question"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Import}" />
</StackPanel>
</Button>
</UniformGrid>
</Grid>
<!-- Update overlay --> <!-- Update overlay -->
<Border <Border
Grid.Row="0" Grid.RowSpan="4"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Background="#DD000000" Background="#DD000000"
CornerRadius="4" CornerRadius="8"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
IsVisible="{Binding IsProgressVisible}" /> IsVisible="{Binding IsProgressVisible}"/>
<Grid Grid.Row="0" Grid.RowSpan="4" <Grid Grid.Column="0"
Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
RowDefinitions="Auto, *" IsVisible="{Binding IsProgressVisible}"
IsVisible="{Binding IsProgressVisible}"> RowDefinitions="Auto, *">
<controls:ProgressRing <controls:ProgressRing
HorizontalAlignment="Center" HorizontalAlignment="Center"
IsIndeterminate="{Binding IsIndeterminate}" IsIndeterminate="{Binding IsIndeterminate}"
Width="150" Width="120"
Height="150" Height="120"
StartAngle="90" StartAngle="90"
EndAngle="450" EndAngle="450"
Value="{Binding Value}" Value="{Binding Value}"
@ -334,6 +432,15 @@
Title="{x:Static lang:Resources.TeachingTip_AddPackageToGetStarted}" Title="{x:Static lang:Resources.TeachingTip_AddPackageToGetStarted}"
PreferredPlacement="Top" PreferredPlacement="Top"
IsOpen="{Binding !Packages.Count}" /> IsOpen="{Binding !Packages.Count}" />
<ui:TeachingTip Grid.Row="0"
Name="LaunchTeachingTip"
MinWidth="100"
Title="{x:Static lang:Resources.TeachingTip_ClickLaunchToGetStarted}"
PreferredPlacement="Bottom"
Margin="8,0,0,0"
PlacementMargin="0,0,0,0"
TailVisibility="Auto"/>
<!-- Add Packages Button --> <!-- Add Packages Button -->
<Button Grid.Row="2" <Button Grid.Row="2"

33
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs

@ -1,12 +1,18 @@
using Avalonia.Interactivity; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation; using FluentAvalonia.UI.Navigation;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;
@ -19,6 +25,31 @@ public partial class PackageManagerPage : UserControlBase
InitializeComponent(); InitializeComponent();
AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct); AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct);
EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished;
}
private void OnOneClickInstallFinished(object? sender, bool skipped)
{
if (skipped)
return;
Dispatcher.UIThread.Invoke(() =>
{
var target = this.FindDescendantOfType<UniformGrid>()
?.GetVisualChildren()
.OfType<Button>()
.FirstOrDefault(x => x is { Name: "LaunchButton" });
if (target == null)
return;
var teachingTip = this.FindControl<TeachingTip>("LaunchTeachingTip");
if (teachingTip == null)
return;
teachingTip.Target = target;
teachingTip.IsOpen = true;
});
} }
private void InitializeComponent() private void InitializeComponent()

6
StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs

@ -1,4 +1,5 @@
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Packages.Extensions;
namespace StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Core.Attributes;
@ -11,4 +12,9 @@ public class TypedNodeOptionsAttribute : Attribute
public string? Name { get; init; } public string? Name { get; init; }
public string[]? RequiredExtensions { get; init; } public string[]? RequiredExtensions { get; init; }
public IEnumerable<ExtensionSpecifier> GetRequiredExtensionSpecifiers()
{
return RequiredExtensions?.Select(ExtensionSpecifier.Parse) ?? Enumerable.Empty<ExtensionSpecifier>();
}
} }

8
StabilityMatrix.Core/Helper/EventManager.cs

@ -23,6 +23,7 @@ public class EventManager
public event EventHandler<bool>? DevModeSettingChanged; public event EventHandler<bool>? DevModeSettingChanged;
public event EventHandler<UpdateInfo>? UpdateAvailable; public event EventHandler<UpdateInfo>? UpdateAvailable;
public event EventHandler<Guid>? PackageLaunchRequested; public event EventHandler<Guid>? PackageLaunchRequested;
public event EventHandler<InstalledPackage>? PackageRelaunchRequested;
public event EventHandler? ScrollToBottomRequested; public event EventHandler? ScrollToBottomRequested;
public event EventHandler<ProgressItem>? ProgressChanged; public event EventHandler<ProgressItem>? ProgressChanged;
public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged; public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged;
@ -41,6 +42,7 @@ public class EventManager
public event EventHandler<LocalImageFile>? InferenceUpscaleRequested; public event EventHandler<LocalImageFile>? InferenceUpscaleRequested;
public event EventHandler<LocalImageFile>? InferenceImageToImageRequested; public event EventHandler<LocalImageFile>? InferenceImageToImageRequested;
public event EventHandler<LocalImageFile>? InferenceImageToVideoRequested; public event EventHandler<LocalImageFile>? InferenceImageToVideoRequested;
public event EventHandler<InferenceQueueCustomPromptEventArgs>? InferenceQueueCustomPrompt;
public event EventHandler<int>? NavigateAndFindCivitModelRequested; public event EventHandler<int>? NavigateAndFindCivitModelRequested;
public event EventHandler? DownloadsTeachingTipRequested; public event EventHandler? DownloadsTeachingTipRequested;
public event EventHandler? RecommendedModelsDialogClosed; public event EventHandler? RecommendedModelsDialogClosed;
@ -92,6 +94,9 @@ public class EventManager
public void OnInferenceImageToVideoRequested(LocalImageFile imageFile) => public void OnInferenceImageToVideoRequested(LocalImageFile imageFile) =>
InferenceImageToVideoRequested?.Invoke(this, imageFile); InferenceImageToVideoRequested?.Invoke(this, imageFile);
public void OnInferenceQueueCustomPrompt(InferenceQueueCustomPromptEventArgs e) =>
InferenceQueueCustomPrompt?.Invoke(this, e);
public void OnNavigateAndFindCivitModelRequested(int modelId) => public void OnNavigateAndFindCivitModelRequested(int modelId) =>
NavigateAndFindCivitModelRequested?.Invoke(this, modelId); NavigateAndFindCivitModelRequested?.Invoke(this, modelId);
@ -100,4 +105,7 @@ public class EventManager
public void OnRecommendedModelsDialogClosed() => public void OnRecommendedModelsDialogClosed() =>
RecommendedModelsDialogClosed?.Invoke(this, EventArgs.Empty); RecommendedModelsDialogClosed?.Invoke(this, EventArgs.Empty);
public void OnPackageRelaunchRequested(InstalledPackage package) =>
PackageRelaunchRequested?.Invoke(this, package);
} }

1
StabilityMatrix.Core/Helper/Factory/IPackageFactory.cs

@ -10,4 +10,5 @@ public interface IPackageFactory
BasePackage? this[string packageName] { get; } BasePackage? this[string packageName] { get; }
PackagePair? GetPackagePair(InstalledPackage? installedPackage); PackagePair? GetPackagePair(InstalledPackage? installedPackage);
IEnumerable<BasePackage> GetPackagesByType(PackageType packageType); IEnumerable<BasePackage> GetPackagesByType(PackageType packageType);
BasePackage GetNewBasePackage(InstalledPackage installedPackage);
} }

74
StabilityMatrix.Core/Helper/Factory/PackageFactory.cs

@ -1,22 +1,94 @@
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Helper.Factory; namespace StabilityMatrix.Core.Helper.Factory;
[Singleton(typeof(IPackageFactory))] [Singleton(typeof(IPackageFactory))]
public class PackageFactory : IPackageFactory public class PackageFactory : IPackageFactory
{ {
private readonly IGithubApiCache githubApiCache;
private readonly ISettingsManager settingsManager;
private readonly IDownloadService downloadService;
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly IPyRunner pyRunner;
/// <summary> /// <summary>
/// Mapping of package.Name to package /// Mapping of package.Name to package
/// </summary> /// </summary>
private readonly Dictionary<string, BasePackage> basePackages; private readonly Dictionary<string, BasePackage> basePackages;
public PackageFactory(IEnumerable<BasePackage> basePackages) public PackageFactory(
IEnumerable<BasePackage> basePackages,
IGithubApiCache githubApiCache,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner
)
{ {
this.githubApiCache = githubApiCache;
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.prerequisiteHelper = prerequisiteHelper;
this.pyRunner = pyRunner;
this.basePackages = basePackages.ToDictionary(x => x.Name); this.basePackages = basePackages.ToDictionary(x => x.Name);
} }
public BasePackage GetNewBasePackage(InstalledPackage installedPackage)
{
return installedPackage.PackageName switch
{
"ComfyUI" => new ComfyUI(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"Fooocus" => new Fooocus(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"stable-diffusion-webui"
=> new A3WebUI(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"Fooocus-ControlNet-SDXL"
=> new FocusControlNet(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"Fooocus-MRE"
=> new FooocusMre(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"InvokeAI" => new InvokeAI(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"kohya_ss"
=> new KohyaSs(
githubApiCache,
settingsManager,
downloadService,
prerequisiteHelper,
pyRunner
),
"OneTrainer"
=> new OneTrainer(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"RuinedFooocus"
=> new RuinedFooocus(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"stable-diffusion-webui-forge"
=> new SDWebForge(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"stable-diffusion-webui-directml"
=> new StableDiffusionDirectMl(
githubApiCache,
settingsManager,
downloadService,
prerequisiteHelper
),
"stable-diffusion-webui-ux"
=> new StableDiffusionUx(
githubApiCache,
settingsManager,
downloadService,
prerequisiteHelper
),
"StableSwarmUI"
=> new StableSwarm(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"automatic"
=> new VladAutomatic(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
"voltaML-fast-stable-diffusion"
=> new VoltaML(githubApiCache, settingsManager, downloadService, prerequisiteHelper),
_ => throw new ArgumentOutOfRangeException(nameof(installedPackage))
};
}
public IEnumerable<BasePackage> GetAllAvailablePackages() public IEnumerable<BasePackage> GetAllAvailablePackages()
{ {
return basePackages.Values.OrderBy(p => p.InstallerSortOrder).ThenBy(p => p.DisplayName); return basePackages.Values.OrderBy(p => p.InstallerSortOrder).ThenBy(p => p.DisplayName);

16
StabilityMatrix.Core/Inference/ComfyClient.cs

@ -460,6 +460,22 @@ public class ComfyClient : InferenceClientBase
return info.Input.GetRequiredValueAsNestedList(optionName); return info.Input.GetRequiredValueAsNestedList(optionName);
} }
/// <summary>
/// Get a list of strings representing available optional options of a given node
/// </summary>
public async Task<List<string>?> GetOptionalNodeOptionNamesAsync(
string nodeName,
string optionName,
CancellationToken cancellationToken = default
)
{
var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false);
var info = response.GetValueOrDefault(nodeName);
return info?.Input.GetOptionalValueAsNestedList(optionName);
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (isDisposed) if (isDisposed)

124
StabilityMatrix.Core/Models/Api/Comfy/ComfyAuxPreprocessor.cs

@ -0,0 +1,124 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
namespace StabilityMatrix.Core.Models.Api.Comfy;
/// <summary>
/// Collection of preprocessors included in
/// </summary>
/// <param name="Value"></param>
[PublicAPI]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public record ComfyAuxPreprocessor(string Value) : StringValue(Value)
{
public static ComfyAuxPreprocessor None { get; } = new("none");
public static ComfyAuxPreprocessor AnimeFaceSemSegPreprocessor { get; } =
new("AnimeFace_SemSegPreprocessor");
public static ComfyAuxPreprocessor BinaryPreprocessor { get; } = new("BinaryPreprocessor");
public static ComfyAuxPreprocessor CannyEdgePreprocessor { get; } = new("CannyEdgePreprocessor");
public static ComfyAuxPreprocessor ColorPreprocessor { get; } = new("ColorPreprocessor");
public static ComfyAuxPreprocessor DensePosePreprocessor { get; } = new("DensePosePreprocessor");
public static ComfyAuxPreprocessor DepthAnythingPreprocessor { get; } = new("DepthAnythingPreprocessor");
public static ComfyAuxPreprocessor ZoeDepthAnythingPreprocessor { get; } =
new("Zoe_DepthAnythingPreprocessor");
public static ComfyAuxPreprocessor DiffusionEdgePreprocessor { get; } = new("DiffusionEdge_Preprocessor");
public static ComfyAuxPreprocessor DWPreprocessor { get; } = new("DWPreprocessor");
public static ComfyAuxPreprocessor AnimalPosePreprocessor { get; } = new("AnimalPosePreprocessor");
public static ComfyAuxPreprocessor HEDPreprocessor { get; } = new("HEDPreprocessor");
public static ComfyAuxPreprocessor FakeScribblePreprocessor { get; } = new("FakeScribblePreprocessor");
public static ComfyAuxPreprocessor LeReSDepthMapPreprocessor { get; } = new("LeReS-DepthMapPreprocessor");
public static ComfyAuxPreprocessor LineArtPreprocessor { get; } = new("LineArtPreprocessor");
public static ComfyAuxPreprocessor AnimeLineArtPreprocessor { get; } = new("AnimeLineArtPreprocessor");
public static ComfyAuxPreprocessor LineartStandardPreprocessor { get; } =
new("LineartStandardPreprocessor");
public static ComfyAuxPreprocessor Manga2AnimeLineArtPreprocessor { get; } =
new("Manga2Anime_LineArt_Preprocessor");
public static ComfyAuxPreprocessor MediaPipeFaceMeshPreprocessor { get; } =
new("MediaPipe-FaceMeshPreprocessor");
public static ComfyAuxPreprocessor MeshGraphormerDepthMapPreprocessor { get; } =
new("MeshGraphormer-DepthMapPreprocessor");
public static ComfyAuxPreprocessor MiDaSNormalMapPreprocessor { get; } =
new("MiDaS-NormalMapPreprocessor");
public static ComfyAuxPreprocessor MiDaSDepthMapPreprocessor { get; } = new("MiDaS-DepthMapPreprocessor");
public static ComfyAuxPreprocessor MLSDPreprocessor { get; } = new("M-LSDPreprocessor");
public static ComfyAuxPreprocessor BAENormalMapPreprocessor { get; } = new("BAE-NormalMapPreprocessor");
public static ComfyAuxPreprocessor OneFormerCOCOSemSegPreprocessor { get; } =
new("OneFormer-COCO-SemSegPreprocessor");
public static ComfyAuxPreprocessor OneFormerADE20KSemSegPreprocessor { get; } =
new("OneFormer-ADE20K-SemSegPreprocessor");
public static ComfyAuxPreprocessor OpenposePreprocessor { get; } = new("OpenposePreprocessor");
public static ComfyAuxPreprocessor PiDiNetPreprocessor { get; } = new("PiDiNetPreprocessor");
public static ComfyAuxPreprocessor SavePoseKpsAsJsonFile { get; } = new("SavePoseKpsAsJsonFile");
public static ComfyAuxPreprocessor FacialPartColoringFromPoseKps { get; } =
new("FacialPartColoringFromPoseKps");
public static ComfyAuxPreprocessor ImageLuminanceDetector { get; } = new("ImageLuminanceDetector");
public static ComfyAuxPreprocessor ImageIntensityDetector { get; } = new("ImageIntensityDetector");
public static ComfyAuxPreprocessor ScribblePreprocessor { get; } = new("ScribblePreprocessor");
public static ComfyAuxPreprocessor ScribbleXDoGPreprocessor { get; } = new("Scribble_XDoG_Preprocessor");
public static ComfyAuxPreprocessor SAMPreprocessor { get; } = new("SAMPreprocessor");
public static ComfyAuxPreprocessor ShufflePreprocessor { get; } = new("ShufflePreprocessor");
public static ComfyAuxPreprocessor TEEDPreprocessor { get; } = new("TEEDPreprocessor");
public static ComfyAuxPreprocessor TilePreprocessor { get; } = new("TilePreprocessor");
public static ComfyAuxPreprocessor UniFormerSemSegPreprocessor { get; } =
new("UniFormer-SemSegPreprocessor");
public static ComfyAuxPreprocessor SemSegPreprocessor { get; } = new("SemSegPreprocessor");
public static ComfyAuxPreprocessor UnimatchOptFlowPreprocessor { get; } =
new("Unimatch_OptFlowPreprocessor");
public static ComfyAuxPreprocessor MaskOptFlow { get; } = new("MaskOptFlow");
public static ComfyAuxPreprocessor ZoeDepthMapPreprocessor { get; } = new("Zoe-DepthMapPreprocessor");
private static Dictionary<ComfyAuxPreprocessor, string> DisplayNamesMapping { get; } =
new()
{
[None] = "None",
[AnimeFaceSemSegPreprocessor] = "Anime Face SemSeg Preprocessor",
[BinaryPreprocessor] = "Binary Preprocessor",
[CannyEdgePreprocessor] = "Canny Edge Preprocessor",
[ColorPreprocessor] = "Color Preprocessor",
[DensePosePreprocessor] = "DensePose Preprocessor",
[DepthAnythingPreprocessor] = "Depth Anything Preprocessor",
[ZoeDepthAnythingPreprocessor] = "Zoe Depth Anything Preprocessor",
[DiffusionEdgePreprocessor] = "Diffusion Edge Preprocessor",
[DWPreprocessor] = "DW Preprocessor",
[AnimalPosePreprocessor] = "Animal Pose Preprocessor",
[HEDPreprocessor] = "HED Preprocessor",
[FakeScribblePreprocessor] = "Fake Scribble Preprocessor",
[LeReSDepthMapPreprocessor] = "LeReS-DepthMap Preprocessor",
[LineArtPreprocessor] = "LineArt Preprocessor",
[AnimeLineArtPreprocessor] = "Anime LineArt Preprocessor",
[LineartStandardPreprocessor] = "Lineart Standard Preprocessor",
[Manga2AnimeLineArtPreprocessor] = "Manga2Anime LineArt Preprocessor",
[MediaPipeFaceMeshPreprocessor] = "MediaPipe FaceMesh Preprocessor",
[MeshGraphormerDepthMapPreprocessor] = "MeshGraphormer DepthMap Preprocessor",
[MiDaSNormalMapPreprocessor] = "MiDaS NormalMap Preprocessor",
[MiDaSDepthMapPreprocessor] = "MiDaS DepthMap Preprocessor",
[MLSDPreprocessor] = "M-LSD Preprocessor",
[BAENormalMapPreprocessor] = "BAE NormalMap Preprocessor",
[OneFormerCOCOSemSegPreprocessor] = "OneFormer COCO SemSeg Preprocessor",
[OneFormerADE20KSemSegPreprocessor] = "OneFormer ADE20K SemSeg Preprocessor",
[OpenposePreprocessor] = "Openpose Preprocessor",
[PiDiNetPreprocessor] = "PiDiNet Preprocessor",
[SavePoseKpsAsJsonFile] = "Save Pose Kps As Json File",
[FacialPartColoringFromPoseKps] = "Facial Part Coloring From Pose Kps",
[ImageLuminanceDetector] = "Image Luminance Detector",
[ImageIntensityDetector] = "Image Intensity Detector",
[ScribblePreprocessor] = "Scribble Preprocessor",
[ScribbleXDoGPreprocessor] = "Scribble XDoG Preprocessor",
[SAMPreprocessor] = "SAM Preprocessor",
[ShufflePreprocessor] = "Shuffle Preprocessor",
[TEEDPreprocessor] = "TEED Preprocessor",
[TilePreprocessor] = "Tile Preprocessor",
[UniFormerSemSegPreprocessor] = "UniFormer SemSeg Preprocessor",
[SemSegPreprocessor] = "SemSeg Preprocessor",
[UnimatchOptFlowPreprocessor] = "Unimatch OptFlow Preprocessor",
[MaskOptFlow] = "Mask OptFlow",
[ZoeDepthMapPreprocessor] = "Zoe DepthMap Preprocessor"
};
public static IEnumerable<ComfyAuxPreprocessor> Defaults => DisplayNamesMapping.Keys;
public string DisplayName => DisplayNamesMapping.GetValueOrDefault(this, Value);
/// <inheritdoc />
public override string ToString() => Value;
}

17
StabilityMatrix.Core/Models/Api/Comfy/ComfyInputInfo.cs

@ -9,13 +9,24 @@ public class ComfyInputInfo
[JsonPropertyName("required")] [JsonPropertyName("required")]
public Dictionary<string, JsonValue>? Required { get; set; } public Dictionary<string, JsonValue>? Required { get; set; }
[JsonPropertyName("optional")]
public Dictionary<string, JsonValue>? Optional { get; set; }
public List<string>? GetRequiredValueAsNestedList(string key) public List<string>? GetRequiredValueAsNestedList(string key)
{ {
var value = Required?[key]; var value = Required?[key];
if (value is null) return null;
var nested = value.Deserialize<List<List<string>>>(); var nested = value?.Deserialize<List<List<string>>>();
return nested?.SelectMany(x => x).ToList(); return nested?.SelectMany(x => x).ToList();
} }
public List<string>? GetOptionalValueAsNestedList(string key)
{
var value = Optional?[key];
var nested = value?.Deserialize<JsonArray>()?[0].Deserialize<List<string>>();
return nested;
}
} }

16
StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs

@ -332,7 +332,7 @@ public class ComfyNodeBuilder
[TypedNodeOptions( [TypedNodeOptions(
Name = "Inference_Core_PromptExpansion", Name = "Inference_Core_PromptExpansion",
RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes"] RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.2.0"]
)] )]
public record PromptExpansion : ComfyTypedNodeBase<StringNodeConnection> public record PromptExpansion : ComfyTypedNodeBase<StringNodeConnection>
{ {
@ -342,6 +342,20 @@ public class ComfyNodeBuilder
public bool LogPrompt { get; init; } public bool LogPrompt { get; init; }
} }
[TypedNodeOptions(
Name = "Inference_Core_AIO_Preprocessor",
RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.2.0"]
)]
public record AIOPreprocessor : ComfyTypedNodeBase<ImageNodeConnection>
{
public required ImageNodeConnection Image { get; init; }
public required string Preprocessor { get; init; }
[Range(64, 2048)]
public int Resolution { get; init; } = 512;
}
public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae) public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae)
{ {
var name = GetUniqueName("VAEDecode"); var name = GetUniqueName("VAEDecode");

13
StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs

@ -1,10 +1,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using KGySoft.CoreLibraries;
using OneOf; using OneOf;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Packages.Extensions;
namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes;
@ -19,7 +21,10 @@ public class NodeDictionary : Dictionary<string, ComfyNode>
/// When inserting TypedNodes, this holds a mapping of ClassType to required extensions /// When inserting TypedNodes, this holds a mapping of ClassType to required extensions
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public Dictionary<string, string[]> ClassTypeRequiredExtensions { get; } = new(); public Dictionary<string, ExtensionSpecifier[]> ClassTypeRequiredExtensions { get; } = new();
public IEnumerable<ExtensionSpecifier> RequiredExtensions =>
ClassTypeRequiredExtensions.Values.SelectMany(x => x);
/// <summary> /// <summary>
/// Finds a unique node name given a base name, by appending _2, _3, etc. /// Finds a unique node name given a base name, by appending _2, _3, etc.
@ -63,7 +68,11 @@ public class NodeDictionary : Dictionary<string, ComfyNode>
{ {
if (options.RequiredExtensions != null) if (options.RequiredExtensions != null)
{ {
ClassTypeRequiredExtensions[namedNode.ClassType] = options.RequiredExtensions; ClassTypeRequiredExtensions.AddOrUpdate(
namedNode.ClassType,
_ => options.GetRequiredExtensionSpecifiers().ToArray(),
(_, specifiers) => options.GetRequiredExtensionSpecifiers().Concat(specifiers).ToArray()
);
} }
} }

3
StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs

@ -7,4 +7,7 @@ public class DownloadPackageVersionOptions
public string? VersionTag { get; set; } public string? VersionTag { get; set; }
public bool IsLatest { get; set; } public bool IsLatest { get; set; }
public bool IsPrerelease { get; set; } public bool IsPrerelease { get; set; }
public string GetReadableVersionString() =>
!string.IsNullOrWhiteSpace(VersionTag) ? VersionTag : $"{BranchName}@{CommitHash?[..7]}";
} }

2
StabilityMatrix.Core/Models/HybridModelFile.cs

@ -20,7 +20,7 @@ public record HybridModelFile
/// </summary> /// </summary>
public static HybridModelFile None { get; } = FromRemote("@none"); public static HybridModelFile None { get; } = FromRemote("@none");
private string? RemoteName { get; init; } public string? RemoteName { get; init; }
public LocalModelFile? Local { get; init; } public LocalModelFile? Local { get; init; }

14
StabilityMatrix.Core/Models/InferenceRunCustomPromptEventArgs.cs

@ -0,0 +1,14 @@
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Core.Models;
public class InferenceQueueCustomPromptEventArgs : EventArgs
{
public ComfyNodeBuilder Builder { get; } = new();
public NodeDictionary Nodes => Builder.Nodes;
public long? SeedOverride { get; init; }
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
}

105
StabilityMatrix.Core/Models/Packages/Extensions/ExtensionSpecifier.cs

@ -0,0 +1,105 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Semver;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
/// <summary>
/// Extension specifier with optional version constraints.
/// </summary>
[PublicAPI]
public partial class ExtensionSpecifier
{
public required string Name { get; init; }
public string? Constraint { get; init; }
public string? Version { get; init; }
public string? VersionConstraint => Constraint is null || Version is null ? null : Constraint + Version;
public bool TryGetSemVersionRange([NotNullWhen(true)] out SemVersionRange? semVersionRange)
{
if (!string.IsNullOrEmpty(VersionConstraint))
{
return SemVersionRange.TryParse(
VersionConstraint,
SemVersionRangeOptions.Loose,
out semVersionRange
);
}
semVersionRange = null;
return false;
}
public static ExtensionSpecifier Parse(string value)
{
TryParse(value, true, out var packageSpecifier);
return packageSpecifier!;
}
public static bool TryParse(string value, [NotNullWhen(true)] out ExtensionSpecifier? extensionSpecifier)
{
return TryParse(value, false, out extensionSpecifier);
}
private static bool TryParse(
string value,
bool throwOnFailure,
[NotNullWhen(true)] out ExtensionSpecifier? packageSpecifier
)
{
var match = ExtensionSpecifierRegex().Match(value);
if (!match.Success)
{
if (throwOnFailure)
{
throw new ArgumentException($"Invalid extension specifier: {value}");
}
packageSpecifier = null;
return false;
}
packageSpecifier = new ExtensionSpecifier
{
Name = match.Groups["extension_name"].Value,
Constraint = match.Groups["version_constraint"].Value,
Version = match.Groups["version"].Value
};
return true;
}
/// <inheritdoc />
public override string ToString()
{
return Name + VersionConstraint;
}
public static implicit operator Argument(ExtensionSpecifier specifier)
{
return specifier.VersionConstraint is null
? new Argument(specifier.Name)
: new Argument((specifier.Name, specifier.VersionConstraint));
}
public static implicit operator ExtensionSpecifier(string specifier)
{
return Parse(specifier);
}
/// <summary>
/// Regex to match a pip package specifier.
/// </summary>
[GeneratedRegex(
@"(?<extension_name>\S+)\s*(?<version_constraint>==|>=|<=|>|<|~=|!=)?\s*(?<version>[a-zA-Z0-9_.]+)?",
RegexOptions.CultureInvariant,
5000
)]
private static partial Regex ExtensionSpecifierRegex();
}

25
StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs

@ -201,6 +201,31 @@ public abstract partial class GitPackageExtensionManager(IPrerequisiteHelper pre
return extensions; return extensions;
} }
public virtual async Task<InstalledPackageExtension> GetInstalledExtensionInfoAsync(
InstalledPackageExtension installedExtension
)
{
if (installedExtension.PrimaryPath is not DirectoryPath extensionDirectory)
{
return installedExtension;
}
// Get git version
var version = await prerequisiteHelper
.GetGitRepositoryVersion(extensionDirectory)
.ConfigureAwait(false);
return installedExtension with
{
Version = new PackageExtensionVersion
{
Tag = version.Tag,
Branch = version.Branch,
CommitSha = version.CommitSha
}
};
}
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task InstallExtensionAsync( public virtual async Task InstallExtensionAsync(
PackageExtension extension, PackageExtension extension,

7
StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs

@ -89,6 +89,13 @@ public interface IPackageExtensionManager
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
/// <summary>
/// Get updated info (version) for an installed extension.
/// </summary>
Task<InstalledPackageExtension> GetInstalledExtensionInfoAsync(
InstalledPackageExtension installedExtension
);
/// <summary> /// <summary>
/// Install an extension to the provided package. /// Install an extension to the provided package.
/// </summary> /// </summary>

1
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -120,6 +120,7 @@ public class Settings
public Size InferenceImageSize { get; set; } = new(150, 190); public Size InferenceImageSize { get; set; } = new(150, 190);
public Size OutputsImageSize { get; set; } = new(300, 300); public Size OutputsImageSize { get; set; } = new(300, 300);
public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic; public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic;
public bool IsOutputsTreeViewEnabled { get; set; } = true;
[JsonIgnore] [JsonIgnore]
public bool IsHolidayModeActive => public bool IsHolidayModeActive =>

1
StabilityMatrix.Core/Models/Settings/TeachingTip.cs

@ -13,6 +13,7 @@ public record TeachingTip(string Value) : StringValue(Value)
public static TeachingTip CheckpointCategoriesTip => new("CheckpointCategoriesTip"); public static TeachingTip CheckpointCategoriesTip => new("CheckpointCategoriesTip");
public static TeachingTip PackageExtensionsInstallNotice => new("PackageExtensionsInstallNotice"); public static TeachingTip PackageExtensionsInstallNotice => new("PackageExtensionsInstallNotice");
public static TeachingTip DownloadsTip => new("DownloadsTip"); public static TeachingTip DownloadsTip => new("DownloadsTip");
public static TeachingTip WebUiButtonMovedTip => new("WebUiButtonMovedTip");
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()

15
StabilityMatrix.Tests/Helper/PackageFactoryTests.cs

@ -8,31 +8,28 @@ public class PackageFactoryTests
{ {
private PackageFactory packageFactory = null!; private PackageFactory packageFactory = null!;
private IEnumerable<BasePackage> fakeBasePackages = null!; private IEnumerable<BasePackage> fakeBasePackages = null!;
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
fakeBasePackages = new List<BasePackage> fakeBasePackages = new List<BasePackage> { new DankDiffusion(null!, null!, null!, null!) };
{ packageFactory = new PackageFactory(fakeBasePackages, null, null, null, null, null);
new DankDiffusion(null!, null!, null!, null!)
};
packageFactory = new PackageFactory(fakeBasePackages);
} }
[TestMethod] [TestMethod]
public void GetAllAvailablePackages_ReturnsAllPackages() public void GetAllAvailablePackages_ReturnsAllPackages()
{ {
var result = packageFactory.GetAllAvailablePackages(); var result = packageFactory.GetAllAvailablePackages();
Assert.AreEqual(1, result.Count()); Assert.AreEqual(1, result.Count());
} }
[TestMethod] [TestMethod]
public void FindPackageByName_ReturnsPackage() public void FindPackageByName_ReturnsPackage()
{ {
var result = packageFactory.FindPackageByName("dank-diffusion"); var result = packageFactory.FindPackageByName("dank-diffusion");
Assert.IsNotNull(result); Assert.IsNotNull(result);
} }
[TestMethod] [TestMethod]
public void FindPackageByName_ReturnsNull() public void FindPackageByName_ReturnsNull()
{ {

Loading…
Cancel
Save