Browse Source

Merge branch 'dev' into openart

pull/629/head
JT 8 months ago committed by GitHub
parent
commit
d2ceefdd19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 84
      CHANGELOG.md
  2. 3
      StabilityMatrix.Avalonia/App.axaml
  3. 9
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 11
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  5. 80
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  6. 5
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  7. 8
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  8. 28
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  9. 3
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  10. 93
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  11. 9
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  12. 144
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  13. 4
      StabilityMatrix.Avalonia/Languages/Resources.de.resx
  14. 181
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  15. 38
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  16. 31
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  17. 24
      StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx
  18. 48
      StabilityMatrix.Avalonia/Languages/Resources.resx
  19. 58
      StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx
  20. 41
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  21. 5
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  22. 1
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  23. 8
      StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs
  24. 20
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  25. 248
      StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs
  26. 148
      StabilityMatrix.Avalonia/Services/RunningPackageService.cs
  27. 6
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  28. 73
      StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml
  29. 403
      StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml
  30. 1
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  31. 119
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  32. 16
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  33. 178
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  34. 8
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  35. 12
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  36. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  37. 21
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs
  38. 15
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
  39. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs
  40. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  41. 6
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs
  42. 19
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  43. 9
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  44. 84
      StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs
  45. 20
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs
  46. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  47. 15
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
  48. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  49. 119
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  50. 57
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
  51. 68
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  52. 13
      StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
  53. 144
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  54. 33
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  55. 3
      StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
  56. 7
      StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs
  57. 176
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  58. 287
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  59. 2
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs
  60. 17
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  61. 161
      StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs
  62. 60
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  63. 86
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  64. 16
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  65. 93
      StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml
  66. 54
      StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml.cs
  67. 55
      StabilityMatrix.Avalonia/Views/Dialogs/ConfirmPackageDeleteDialog.axaml
  68. 13
      StabilityMatrix.Avalonia/Views/Dialogs/ConfirmPackageDeleteDialog.axaml.cs
  69. 39
      StabilityMatrix.Avalonia/Views/Dialogs/NewOneClickInstallDialog.axaml
  70. 4
      StabilityMatrix.Avalonia/Views/Dialogs/RecommendedModelsDialog.axaml
  71. 4
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  72. 46
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  73. 165
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  74. 276
      StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml
  75. 595
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  76. 33
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs
  77. 6
      StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs
  78. 15
      StabilityMatrix.Core/Extensions/NullableExtensions.cs
  79. 8
      StabilityMatrix.Core/Helper/EventManager.cs
  80. 1
      StabilityMatrix.Core/Helper/Factory/IPackageFactory.cs
  81. 74
      StabilityMatrix.Core/Helper/Factory/PackageFactory.cs
  82. 8
      StabilityMatrix.Core/Helper/RemoteModels.cs
  83. 16
      StabilityMatrix.Core/Inference/ComfyClient.cs
  84. 16
      StabilityMatrix.Core/Models/Api/CivitBaseModelType.cs
  85. 14
      StabilityMatrix.Core/Models/Api/CivitMetadata.cs
  86. 7
      StabilityMatrix.Core/Models/Api/CivitModel.cs
  87. 5
      StabilityMatrix.Core/Models/Api/CivitModelStats.cs
  88. 21
      StabilityMatrix.Core/Models/Api/CivitModelVersion.cs
  89. 60
      StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs
  90. 124
      StabilityMatrix.Core/Models/Api/Comfy/ComfyAuxPreprocessor.cs
  91. 17
      StabilityMatrix.Core/Models/Api/Comfy/ComfyInputInfo.cs
  92. 9
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
  93. 83
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
  94. 13
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs
  95. 3
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  96. 8
      StabilityMatrix.Core/Models/HybridModelFile.cs
  97. 46
      StabilityMatrix.Core/Models/Inference/ModuleApplyStepTemporaryArgs.cs
  98. 14
      StabilityMatrix.Core/Models/InferenceRunCustomPromptEventArgs.cs
  99. 12
      StabilityMatrix.Core/Models/PackageDifficulty.cs
  100. 2
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  101. Some files were not shown because too many files have changed in this diff Show More

84
CHANGELOG.md

@ -5,17 +5,99 @@ 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
## v2.10.0-preview.1
### Added ### Added
- Added OpenArt.AI workflow browser for ComfyUI workflows - Added OpenArt.AI workflow browser for ComfyUI workflows
## v2.10.0-dev.3
### Added
- Added support for deep links from the new Stability Matrix Chrome extension
### Changed
- Due to changes on the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser
- Due to the above API changes, new pages are now loaded via "infinite scrolling"
### Fixed
- Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked
- Fixed model download location options for VAEs in the CivitAI Model Browser
- Fixed crash on startup when library directory is not set
- Fixed One-Click install progress dialog not disappearing after completion
- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area
- Fixed no packages being available for one-click install on PCs without a GPU
- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page
- Fixed missing ratings on some models in the CivitAI Model Browser
- Fixed missing favorite count in the CivitAI Model Browser
- Fixed recommended models not showing all SDXL models
## v2.10.0-dev.2
### Added
- Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting.
### Changed
- Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option.
- Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading
### Fixed
- Fixed Outputs page not remembering where the user last was in the TreeView in certain circumstances
- Fixed Inference extension upgrades not being added to missing extensions list for prompted install
- Fixed "The Open Web UI button has moved" teaching tip spam
## v2.10.0-dev.1
### Added
- Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image.
- Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too.
### 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.1
### Added
- Fixed [#498](https://github.com/LykosAI/StabilityMatrix/issues/498) Added "Pony" category to CivitAI Model Browser
### Changed
- Changed package deletion warning dialog to require additional confirmation
### Fixed
- Fixed [#502](https://github.com/LykosAI/StabilityMatrix/issues/502) - missing launch options for Forge
- Fixed [#500](https://github.com/LykosAI/StabilityMatrix/issues/500) - missing output images in Forge when using output sharing
- Fixed [#490](https://github.com/LykosAI/StabilityMatrix/issues/490) - `mpmath has no attribute 'rational'` error on macOS
- Fixed [#510](https://github.com/ionite34/StabilityMatrix/pull/564/files) - kohya_ss packages with v23.0.x failing to install due to missing 'packaging' dependency
- Fixed incorrect progress text when deleting a checkpoint from the Checkpoints page
- Fixed incorrect icon colors on macOS
## v2.9.0
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
#### Inference
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Changed
- Updated translations for German, Spanish, French, Japanese, Portuguese, and Turkish
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed [#335](https://github.com/LykosAI/StabilityMatrix/issues/335) Update hanging indefinitely after git step for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
- Fixed unknown model types not showing on checkpoints page (thanks Jerry!)
- Improved error handling for Inference Select Image hash calculation in case file is being written to while being read
## v2.9.0-pre.2 ## v2.9.0-pre.2
### Added ### Added
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`) - Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
### Changed ### Changed
- (Internal) Updated to Avalonia 11.0.9 - (Internal) Updated to Avalonia 11.0.9
### Fixed ### Fixed
- Fixed image viewer dialog arrow key navigation not working - Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
## v2.9.0-pre.1 ## v2.9.0-pre.1
### Added ### Added

3
StabilityMatrix.Avalonia/App.axaml

@ -4,6 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia" xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling" xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
xmlns:controls="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
Name="Stability Matrix" Name="Stability Matrix"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -54,6 +55,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"/>
@ -79,6 +81,7 @@
<StyleInclude Source="Controls/Inference/FreeUCard.axaml"/> <StyleInclude Source="Controls/Inference/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/> <StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/>
<StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/> <StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/>
<controls:ControlThemes/>
<Style Selector="DockControl"> <Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" /> <Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

9
StabilityMatrix.Avalonia/App.axaml.cs

@ -40,7 +40,6 @@ using Polly.Extensions.Http;
using Polly.Timeout; using Polly.Timeout;
using Refit; using Refit;
using Sentry; using Sentry;
using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
@ -54,14 +53,12 @@ using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
#if DEBUG #if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer; using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
@ -323,14 +320,14 @@ public sealed class App : Application
provider.GetRequiredService<IDiscordRichPresenceService>(), provider.GetRequiredService<IDiscordRichPresenceService>(),
provider.GetRequiredService<ServiceManager<ViewModelBase>>(), provider.GetRequiredService<ServiceManager<ViewModelBase>>(),
provider.GetRequiredService<ITrackedDownloadService>(), provider.GetRequiredService<ITrackedDownloadService>(),
provider.GetRequiredService<IModelIndexService>() provider.GetRequiredService<IModelIndexService>(),
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>()
) )
{ {
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>(),

11
StabilityMatrix.Avalonia/Assets/hf-packages.json

@ -58,6 +58,17 @@
"LicenseType": "Stable-Video-Diffusion-NC-Community", "LicenseType": "Stable-Video-Diffusion-NC-Community",
"LicensePath": "LICENSE" "LicensePath": "LICENSE"
}, },
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Cascade",
"RepositoryPath": "stabilityai/stable-cascade",
"Files": [
"comfyui_checkpoints/stable_cascade_stage_b.safetensors",
"comfyui_checkpoints/stable_cascade_stage_c.safetensors"
],
"LicenseType": "stable-cascade-nc-community",
"LicensePath": "LICENSE"
},
{ {
"ModelCategory": "ControlNet", "ModelCategory": "ControlNet",
"ModelName": "Canny", "ModelName": "Canny",

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

5
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -1,7 +1,8 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard" xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"> xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<!-- Add Controls for Previewer Here --> <!-- Add Controls for Previewer Here -->
@ -38,7 +39,7 @@
CornerRadius="12" CornerRadius="12"
Command="{TemplateBinding Command}" Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"> CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage <controls1:AsyncImage
Stretch="UniformToFill" Stretch="UniformToFill"
CornerRadius="8" CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}" ContextFlyout="{TemplateBinding ContextFlyout}"

8
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -11,8 +11,10 @@ public class SelectableImageButton : Button
public static readonly StyledProperty<bool?> IsSelectedProperty = public static readonly StyledProperty<bool?> IsSelectedProperty =
ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>(); ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty = public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
AdvancedImage.SourceProperty.AddOwner<SelectableImageButton>(); SelectableImageButton,
Uri?
>("Source");
public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register< public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register<
SelectableImageButton, SelectableImageButton,
@ -48,7 +50,7 @@ public class SelectableImageButton : Button
set => SetValue(IsSelectedProperty, value); set => SetValue(IsSelectedProperty, value);
} }
public string? Source public Uri? Source
{ {
get => GetValue(SourceProperty); get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value); set => SetValue(SourceProperty, value);

28
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -100,6 +100,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
@ -314,7 +327,7 @@ public static class DesignData
); );
}*/ }*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel> CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{ {
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm => dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{ {
@ -406,7 +419,8 @@ public static class DesignData
new PackageInstallProgressItemViewModel( new PackageInstallProgressItemViewModel(
new PackageModificationRunner new PackageModificationRunner
{ {
CurrentProgress = new ProgressReport(0.5f, "Installing package...") CurrentProgress = new ProgressReport(0.5f, "Installing package..."),
ModificationCompleteMessage = "Package installed successfully"
} }
) )
} }
@ -509,6 +523,16 @@ public static class DesignData
} }
) )
}; };
vm.Categories = new ObservableCollectionExtended<PackageOutputCategory>
{
new()
{
Name = "Category 1",
Path = "path1",
SubDirectories = [new PackageOutputCategory { Name = "SubCategory 1", Path = "path3" }]
},
new() { 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;

93
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -20,17 +20,15 @@ public static class ComfyNodeBuilderExtensions
int? batchIndex = null int? batchIndex = null
) )
{ {
var emptyLatent = builder var emptyLatent = builder.Nodes.AddTypedNode(
.Nodes new ComfyNodeBuilder.EmptyLatentImage
.AddTypedNode( {
new ComfyNodeBuilder.EmptyLatentImage Name = "EmptyLatentImage",
{ BatchSize = batchSize,
Name = "EmptyLatentImage", Height = height,
BatchSize = batchSize, Width = width
Height = height, }
Width = width );
}
);
builder.Connections.Primary = emptyLatent.Output; builder.Connections.Primary = emptyLatent.Output;
builder.Connections.PrimarySize = new Size(width, height); builder.Connections.PrimarySize = new Size(width, height);
@ -39,15 +37,15 @@ public static class ComfyNodeBuilderExtensions
if (batchIndex is not null) if (batchIndex is not null)
{ {
builder.Connections.Primary = builder builder.Connections.Primary = builder
.Nodes .Nodes.AddTypedNode(
.AddNamedNode( new ComfyNodeBuilder.LatentFromBatch
ComfyNodeBuilder.LatentFromBatch( {
"LatentFromBatch", Name = "LatentFromBatch",
builder.GetPrimaryAsLatent(), Samples = builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based // remote expects a 0-based index, vm is 1-based
batchIndex.Value - 1, BatchIndex = batchIndex.Value - 1,
1 Length = 1
) }
) )
.Output; .Output;
} }
@ -67,9 +65,9 @@ public static class ComfyNodeBuilderExtensions
var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached()); var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached());
// Load source // Load source
var loadImage = builder var loadImage = builder.Nodes.AddTypedNode(
.Nodes new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }
.AddTypedNode(new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }); );
builder.Connections.Primary = loadImage.Output1; builder.Connections.Primary = loadImage.Output1;
builder.Connections.PrimarySize = imageSize; builder.Connections.PrimarySize = imageSize;
@ -78,15 +76,15 @@ public static class ComfyNodeBuilderExtensions
if (batchIndex is not null) if (batchIndex is not null)
{ {
builder.Connections.Primary = builder builder.Connections.Primary = builder
.Nodes .Nodes.AddTypedNode(
.AddNamedNode( new ComfyNodeBuilder.LatentFromBatch
ComfyNodeBuilder.LatentFromBatch( {
"LatentFromBatch", Name = "LatentFromBatch",
builder.GetPrimaryAsLatent(), Samples = builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based // remote expects a 0-based index, vm is 1-based
batchIndex.Value - 1, BatchIndex = batchIndex.Value - 1,
1 Length = 1
) }
) )
.Output; .Output;
} }
@ -97,25 +95,24 @@ public static class ComfyNodeBuilderExtensions
if (builder.Connections.Primary is null) if (builder.Connections.Primary is null)
throw new ArgumentException("No Primary"); throw new ArgumentException("No Primary");
var image = builder var image = builder.Connections.Primary.Match(
.Connections _ =>
.Primary builder.GetPrimaryAsImage(
.Match( builder.Connections.PrimaryVAE
_ => ?? builder.Connections.Refiner.VAE
builder.GetPrimaryAsImage( ?? builder.Connections.Base.VAE
builder.Connections.PrimaryVAE ?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
?? builder.Connections.Refiner.VAE ),
?? builder.Connections.Base.VAE image => image
?? throw new ArgumentException("No Primary, Refiner, or Base VAE") );
),
image => image
);
var previewImage = builder var previewImage = builder.Nodes.AddTypedNode(
.Nodes new ComfyNodeBuilder.PreviewImage
.AddTypedNode( {
new ComfyNodeBuilder.PreviewImage { Name = builder.Nodes.GetUniqueName("SaveImage"), Images = image } Name = builder.Nodes.GetUniqueName("SaveImage"),
); Images = image
}
);
builder.Connections.OutputNodes.Add(previewImage); builder.Connections.OutputNodes.Add(previewImage);

9
StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs

@ -103,4 +103,13 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
} }
} }
} }
public void ClearCache()
{
var cache =
this.GetPrivateField<ConcurrentDictionary<string, Task<Bitmap?>>>("_memoryCache")
?? throw new NullReferenceException("Memory cache not found");
cache.Clear();
}
} }

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

@ -401,6 +401,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>
@ -680,6 +689,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>
@ -725,6 +743,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Another instance of Stability Matrix is already running. Please close it before starting a new one..
/// </summary>
public static string Label_AnotherInstanceAlreadyRunning {
get {
return ResourceManager.GetString("Label_AnotherInstanceAlreadyRunning", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to API Key. /// Looks up a localized string similar to API Key.
/// </summary> /// </summary>
@ -752,6 +779,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>
@ -860,6 +896,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>
@ -869,6 +914,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>
@ -968,6 +1022,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>
@ -1013,6 +1085,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>
@ -1238,6 +1319,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>
@ -1751,6 +1841,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>
@ -1823,6 +1922,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>
@ -2210,6 +2318,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Stability Matrix is already running.
/// </summary>
public static string Label_StabilityMatrixAlreadyRunning {
get {
return ResourceManager.GetString("Label_StabilityMatrixAlreadyRunning", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Steps. /// Looks up a localized string similar to Steps.
/// </summary> /// </summary>
@ -2291,6 +2408,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>
@ -2435,6 +2561,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>
@ -2606,6 +2741,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>

4
StabilityMatrix.Avalonia/Languages/Resources.de.resx

@ -521,6 +521,7 @@
</data> </data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> <data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Wählen Sie Ihre bevorzugte Schnittstelle und klicken Sie auf Installieren, um loszulegen.</value> <value>Wählen Sie Ihre bevorzugte Schnittstelle und klicken Sie auf Installieren, um loszulegen.</value>
<comment>Fuzzy</comment>
</data> </data>
<data name="Label_Installing" xml:space="preserve"> <data name="Label_Installing" xml:space="preserve">
<value>Installiert</value> <value>Installiert</value>
@ -777,6 +778,9 @@
<data name="Label_ApiKey" xml:space="preserve"> <data name="Label_ApiKey" xml:space="preserve">
<value>API Schlüssel</value> <value>API Schlüssel</value>
</data> </data>
<data name="Label_Accounts" xml:space="preserve">
<value>Accounts</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve"> <data name="Label_Preprocessor" xml:space="preserve">
<value>Preprocessor</value> <value>Preprocessor</value>
</data> </data>

181
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -522,6 +522,7 @@
</data> </data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> <data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Elige tu interfaz preferida y pulsa Instalar para comenzar</value> <value>Elige tu interfaz preferida y pulsa Instalar para comenzar</value>
<comment>Fuzzy</comment>
</data> </data>
<data name="Label_Installing" xml:space="preserve"> <data name="Label_Installing" xml:space="preserve">
<value>Instalando</value> <value>Instalando</value>
@ -782,4 +783,184 @@
<data name="Label_Accounts" xml:space="preserve"> <data name="Label_Accounts" xml:space="preserve">
<value>Cuentas</value> <value>Cuentas</value>
</data> </data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Preprocesador</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Fortaleza</value>
</data>
<data name="Label_ControlWeight" xml:space="preserve">
<value>Control de Peso</value>
</data>
<data name="Label_ControlSteps" xml:space="preserve">
<value>Control de Pasos</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Debes iniciar sesión para descargar este checkpoint. Ingresa una clave API de CivitAI en la configuración.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>Descarga Fallida</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Actualizaciones automáticas</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Para los usuarios primerizos. Las versiones preliminares serán más fiables que las del canal de desarrollo y estarán disponibles antes de las versiones estables. Tus comentarios nos ayudarán enormemente a descubrir problemas y pulir elementos de diseño.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Para usuarios técnicos. Sé el primero en acceder a nuestras compilaciones de desarrollo desde las ramas de funciones tan pronto como estén disponibles. Es posible que haya algunas asperezas y errores a medida que experimentamos con nuevas funciones.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Actualizaciones</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Estás al día</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Última comprobación: {0}</value>
</data>
<data name="Action_CopyTriggerWords" xml:space="preserve">
<value>Copiar palabras desencadenantes</value>
</data>
<data name="Label_TriggerWords" xml:space="preserve">
<value>Palabras desencadenantes:</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Aquí se pueden habilitar carpetas adicionales como IPAdapters y TextualInversions (embeddings)</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Abrir en Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Actualizar metadatos existentes</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>General</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inferencia</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Prompt</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Archivos de imágenes resultantes</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Visor de Imágenes</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto Completado</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Reemplazar guiones bajos con espacios al insertar terminaciones</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Etiquetas de Prompt</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Importar Etiquetas de Prompt</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Archivo de etiquetas que se utilizarán para sugerir terminaciones (admite el formato .csv a1111-sd-webui-tagcomplete)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Información del Sistema</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Complementos</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Guardar Imagen Intermedia</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Ajustes</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Seleccionar Archivo</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>Reemplazar contenido</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>No disponible aún</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>La función estará disponible en una actualización futura</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Archivo de Imagen Faltante</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Modo Vacaciones</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>Saltar CLIP</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Imagen a Vídeo</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Frames por Segundo</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>CFG Mínimo</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Sin pérdidas</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Frames</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Nivel de aumento</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Método</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Calidad</value>
</data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Buscar en el navegador de modelos</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Instalado</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>No se encuentran extensiones.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Ocultar</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Copiar detalles</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>Descargar</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Verifica el progreso de instalación de tus paquetes y descargas de modelos aquí.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Modelos recomendados</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Mientras se instala tu paquete, aquí hay algunos modelos que recomendamos para ayudarte a comenzar.</value>
</data>
</root> </root>

38
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -319,7 +319,7 @@
<value>Recherche</value> <value>Recherche</value>
</data> </data>
<data name="Label_Sort" xml:space="preserve"> <data name="Label_Sort" xml:space="preserve">
<value>Trier</value> <value>Tri</value>
</data> </data>
<data name="Label_TimePeriod" xml:space="preserve"> <data name="Label_TimePeriod" xml:space="preserve">
<value>Période</value> <value>Période</value>
@ -810,6 +810,9 @@
<data name="TextTemplate_LastChecked" xml:space="preserve"> <data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Dernière vérification: {0}</value> <value>Dernière vérification: {0}</value>
</data> </data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Les dossiers additionnels comme IPAdapters ou TextualInversions (embeddings) peuvent être activés ici</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve"> <data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Ouvrir sur Hugging Face</value> <value>Ouvrir sur Hugging Face</value>
</data> </data>
@ -834,6 +837,9 @@
<data name="Label_AutoCompletion" xml:space="preserve"> <data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto-complétion</value> <value>Auto-complétion</value>
</data> </data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Remplacer les underscores par des espaces lors de l&apos;auto-complétion.</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve"> <data name="Label_SystemInformation" xml:space="preserve">
<value>Informations système</value> <value>Informations système</value>
</data> </data>
@ -888,10 +894,40 @@
<data name="Label_MotionBucketId" xml:space="preserve"> <data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value> <value>Motion Bucket ID</value>
</data> </data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Niveau d&apos;augmentation</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve"> <data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Méthode</value> <value>Méthode</value>
</data> </data>
<data name="Label_VideoQuality" xml:space="preserve"> <data name="Label_VideoQuality" xml:space="preserve">
<value>Qualité</value> <value>Qualité</value>
</data> </data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Trouver dans l&apos;explorateur de modèle</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Installé</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>Pas d&apos;extensions trouvés.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Masquer</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Copier les détails</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>Télécharger</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Vérifiez la progression de l&apos;installation de votre paquet ainsi que le téléchargement de votre modèle ici.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Modèles recommandés</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Pendant que votre paquet s&apos;installe, voici quelques modèles que nous recommandons pour vous aidez à démarrer.</value>
</data>
</root> </root>

31
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -530,6 +530,7 @@
</data> </data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> <data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>好きなUIをインストールして始めよう</value> <value>好きなUIをインストールして始めよう</value>
<comment>Fuzzy</comment>
</data> </data>
<data name="Label_Installing" xml:space="preserve"> <data name="Label_Installing" xml:space="preserve">
<value>インストール中</value> <value>インストール中</value>
@ -706,6 +707,24 @@
<data name="Action_Refresh" xml:space="preserve"> <data name="Action_Refresh" xml:space="preserve">
<value>更新</value> <value>更新</value>
</data> </data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>GitHubで開く</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>ユーザ名</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>パスワード</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>ログイン</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>サインアップ</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>パスワード再確認</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve"> <data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>ダウンロードにはCivitAIのログインが必要です。SettingからAPIキーを入力してください。</value> <value>ダウンロードにはCivitAIのログインが必要です。SettingからAPIキーを入力してください。</value>
</data> </data>
@ -715,6 +734,9 @@
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve"> <data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Devビルドはテクニカルなユーザ向けです。新機能をいち早く利用できます。荒削りな部分やバグがあるかもしれません。</value> <value>Devビルドはテクニカルなユーザ向けです。新機能をいち早く利用できます。荒削りな部分やバグがあるかもしれません。</value>
</data> </data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>IPAdaptersやTextualInversions(embeddings)などのフォルダは、ここで有効にできます。</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve"> <data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Hugging Faceで開く</value> <value>Hugging Faceで開く</value>
</data> </data>
@ -734,4 +756,13 @@
<data name="Label_Lossless" xml:space="preserve"> <data name="Label_Lossless" xml:space="preserve">
<value>非圧縮</value> <value>非圧縮</value>
</data> </data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Packageのインストール・Modelのダウンロードの進捗状況はこちらで確認できます。</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>推奨Model</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Packageのインストール中に、推奨Modelをいくつかご紹介しましょう。</value>
</data>
</root> </root>

24
StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx

@ -196,7 +196,7 @@
<value>Passos - Refinamento</value> <value>Passos - Refinamento</value>
</data> </data>
<data name="Label_CFGScale" xml:space="preserve"> <data name="Label_CFGScale" xml:space="preserve">
<value>Escale de CFG</value> <value>Escala de CFG</value>
</data> </data>
<data name="Label_DenoisingStrength" xml:space="preserve"> <data name="Label_DenoisingStrength" xml:space="preserve">
<value>Nível de Denoising</value> <value>Nível de Denoising</value>
@ -391,7 +391,7 @@
<value>Pasta de Modelos</value> <value>Pasta de Modelos</value>
</data> </data>
<data name="Label_Categories" xml:space="preserve"> <data name="Label_Categories" xml:space="preserve">
<value>Catagorias</value> <value>Categorias</value>
</data> </data>
<data name="Label_LetsGetStarted" xml:space="preserve"> <data name="Label_LetsGetStarted" xml:space="preserve">
<value>Vamos começar!</value> <value>Vamos começar!</value>
@ -409,7 +409,7 @@
<value>Exibir imagens dos Modelos</value> <value>Exibir imagens dos Modelos</value>
</data> </data>
<data name="Label_Appearance" xml:space="preserve"> <data name="Label_Appearance" xml:space="preserve">
<value>Aparencia</value> <value>Aparência</value>
</data> </data>
<data name="Label_Theme" xml:space="preserve"> <data name="Label_Theme" xml:space="preserve">
<value>Tema</value> <value>Tema</value>
@ -521,6 +521,7 @@
</data> </data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> <data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Escolha sua interface preferida e clique em Instalar para começar</value> <value>Escolha sua interface preferida e clique em Instalar para começar</value>
<comment>Fuzzy</comment>
</data> </data>
<data name="Label_Installing" xml:space="preserve"> <data name="Label_Installing" xml:space="preserve">
<value>Instalando</value> <value>Instalando</value>
@ -840,7 +841,7 @@
<comment>The Inference feature page</comment> <comment>The Inference feature page</comment>
</data> </data>
<data name="Label_Prompt" xml:space="preserve"> <data name="Label_Prompt" xml:space="preserve">
<value>Incitar</value> <value>Prompt</value>
<comment>A settings category for Inference generation prompts</comment> <comment>A settings category for Inference generation prompts</comment>
</data> </data>
<data name="Label_OutputImageFiles" xml:space="preserve"> <data name="Label_OutputImageFiles" xml:space="preserve">
@ -853,17 +854,17 @@
<value>Preenchimento automático</value> <value>Preenchimento automático</value>
</data> </data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve"> <data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Substitua sublinhados por espaços ao inserir conclusões</value> <value>Substitua caracteres sublinhados por espaços ao inserir conclusões</value>
</data> </data>
<data name="Label_PromptTags" xml:space="preserve"> <data name="Label_PromptTags" xml:space="preserve">
<value>Etiquetas de prompt</value> <value>Etiquetas de Prompt</value>
<comment>Tags for image generation prompts</comment> <comment>Tags for image generation prompts</comment>
</data> </data>
<data name="Label_PromptTagsImport" xml:space="preserve"> <data name="Label_PromptTagsImport" xml:space="preserve">
<value>Tags de prompt de importação</value> <value>Importar Tags de Prompt</value>
</data> </data>
<data name="Label_PromptTagsDescription" xml:space="preserve"> <data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Arquivo de tags a ser usado para sugerir conclusões (suporta o formato .csv a1111-sd-webui-tagcomplete)</value> <value>Arquivo de tags a ser usado para Sugestão de Completions (suporta o formato .csv a1111-sd-webui-tagcomplete)</value>
</data> </data>
<data name="Label_SystemInformation" xml:space="preserve"> <data name="Label_SystemInformation" xml:space="preserve">
<value>Informação do sistema</value> <value>Informação do sistema</value>
@ -872,7 +873,7 @@
<value>CivitAI</value> <value>CivitAI</value>
</data> </data>
<data name="Label_HuggingFace" xml:space="preserve"> <data name="Label_HuggingFace" xml:space="preserve">
<value>Abraçando o rosto</value> <value>Hugging Face</value>
</data> </data>
<data name="Label_Addons" xml:space="preserve"> <data name="Label_Addons" xml:space="preserve">
<value>Complementos</value> <value>Complementos</value>
@ -901,7 +902,7 @@
<value>Arquivo de imagem não encontrado</value> <value>Arquivo de imagem não encontrado</value>
</data> </data>
<data name="Label_HolidayMode" xml:space="preserve"> <data name="Label_HolidayMode" xml:space="preserve">
<value>Modo Férias</value> <value>Modo Feriado</value>
</data> </data>
<data name="Label_CLIPSkip" xml:space="preserve"> <data name="Label_CLIPSkip" xml:space="preserve">
<value>Pular CLIPE</value> <value>Pular CLIPE</value>
@ -930,4 +931,7 @@
<data name="Label_VideoOutputMethod" xml:space="preserve"> <data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Método</value> <value>Método</value>
</data> </data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Qualidade</value>
</data>
</root> </root>

48
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -996,7 +996,55 @@
<data name="Label_OpenArtBrowser" xml:space="preserve"> <data name="Label_OpenArtBrowser" xml:space="preserve">
<value>OpenArt Browser</value> <value>OpenArt Browser</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"> <data name="Label_ModelBrowser" xml:space="preserve">
<value>Model Browser</value> <value>Model Browser</value>
</data> </data>
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>The 'Open Web UI' button has moved to the command bar</value>
</data>
<data name="Label_AnotherInstanceAlreadyRunning" xml:space="preserve">
<value>Another instance of Stability Matrix is already running. Please close it before starting a new one.</value>
</data>
<data name="Label_StabilityMatrixAlreadyRunning" xml:space="preserve">
<value>Stability Matrix is already running</value>
</data>
</root> </root>

58
StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx

@ -133,7 +133,7 @@
<value>Dil</value> <value>Dil</value>
</data> </data>
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve"> <data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve">
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekiyor</value> <value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekir.</value>
</data> </data>
<data name="Action_Relaunch" xml:space="preserve"> <data name="Action_Relaunch" xml:space="preserve">
<value>Yeniden başlat</value> <value>Yeniden başlat</value>
@ -166,7 +166,7 @@
<value>Dallar</value> <value>Dallar</value>
</data> </data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve"> <data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın</value> <value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın.</value>
</data> </data>
<data name="Label_Emphasis" xml:space="preserve"> <data name="Label_Emphasis" xml:space="preserve">
<value>Vurgu</value> <value>Vurgu</value>
@ -235,7 +235,7 @@
<value>Sponsor Ol</value> <value>Sponsor Ol</value>
</data> </data>
<data name="Label_JoinDiscord" xml:space="preserve"> <data name="Label_JoinDiscord" xml:space="preserve">
<value>Discord Sunucusuna Katıl</value> <value>Discord Sunucumuza Katıl</value>
</data> </data>
<data name="Label_Downloads" xml:space="preserve"> <data name="Label_Downloads" xml:space="preserve">
<value>İndirmeler</value> <value>İndirmeler</value>
@ -313,7 +313,7 @@
<value>Tüm Sürümler</value> <value>Tüm Sürümler</value>
</data> </data>
<data name="Label_ModelSearchWatermark" xml:space="preserve"> <data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Modelleri, #etiketleri veya @kullanıcıları ara</value> <value>Modelleri,Tagları (#tag) veya Kullanıcıları (@user) Buradan Arayabilirsin</value>
</data> </data>
<data name="Action_Search" xml:space="preserve"> <data name="Action_Search" xml:space="preserve">
<value>Ara</value> <value>Ara</value>
@ -322,13 +322,13 @@
<value>Sırala</value> <value>Sırala</value>
</data> </data>
<data name="Label_TimePeriod" xml:space="preserve"> <data name="Label_TimePeriod" xml:space="preserve">
<value>Süre</value> <value>Sıralama Tarihi</value>
</data> </data>
<data name="Label_ModelType" xml:space="preserve"> <data name="Label_ModelType" xml:space="preserve">
<value>Model Türü</value> <value>Model Türü</value>
</data> </data>
<data name="Label_BaseModel" xml:space="preserve"> <data name="Label_BaseModel" xml:space="preserve">
<value>Temel Model</value> <value>Model Altyapısı</value>
</data> </data>
<data name="Label_ShowNsfwContent" xml:space="preserve"> <data name="Label_ShowNsfwContent" xml:space="preserve">
<value>NSFW İçerik Göster</value> <value>NSFW İçerik Göster</value>
@ -376,10 +376,10 @@
<value>Klasör</value> <value>Klasör</value>
</data> </data>
<data name="Label_DropFileToImport" xml:space="preserve"> <data name="Label_DropFileToImport" xml:space="preserve">
<value>İçe aktarma için dosyayı buraya bırakın</value> <value>Dosyayı içeri aktarmak için lütfen sürükleyip işaretlenen alana bırakın.</value>
</data> </data>
<data name="Label_ImportAsConnected" xml:space="preserve"> <data name="Label_ImportAsConnected" xml:space="preserve">
<value>Metadata ile içeri aktar</value> <value>Dosyaları metadata ile içeri aktar</value>
</data> </data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve"> <data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Yeni yerel içe aktarmalar için bağlı meta veri arayın</value> <value>Yeni yerel içe aktarmalar için bağlı meta veri arayın</value>
@ -448,7 +448,7 @@
<value>Entegrasyonlar</value> <value>Entegrasyonlar</value>
</data> </data>
<data name="Label_DiscordRichPresence" xml:space="preserve"> <data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Discord Zengin Varlık</value> <value>Discord Etkinliğini StabilityMatrix Olarak Göster.</value>
</data> </data>
<data name="Label_System" xml:space="preserve"> <data name="Label_System" xml:space="preserve">
<value>Sistem</value> <value>Sistem</value>
@ -469,10 +469,10 @@
<value>Tüm Kullanıcılar İçin Ekle</value> <value>Tüm Kullanıcılar İçin Ekle</value>
</data> </data>
<data name="Label_SelectNewDataDirectory" xml:space="preserve"> <data name="Label_SelectNewDataDirectory" xml:space="preserve">
<value>Yeni Veri Dizini Seç</value> <value>Yeni Dosya Dizinini Seç</value>
</data> </data>
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve"> <data name="Label_SelectNewDataDirectory_Details" xml:space="preserve">
<value>Mevcut veriyi taşımaz</value> <value>Bu işlem mevcut veriyi yeni dizine taşımaz.</value>
</data> </data>
<data name="Action_SelectDirectory" xml:space="preserve"> <data name="Action_SelectDirectory" xml:space="preserve">
<value>Dizin Seçin</value> <value>Dizin Seçin</value>
@ -490,7 +490,7 @@
<value>Başlamak için Başlat&apos;a tıklayın!</value> <value>Başlamak için Başlat&apos;a tıklayın!</value>
</data> </data>
<data name="Action_Stop" xml:space="preserve"> <data name="Action_Stop" xml:space="preserve">
<value>Dur</value> <value>Durdur</value>
</data> </data>
<data name="Action_SendInput" xml:space="preserve"> <data name="Action_SendInput" xml:space="preserve">
<value>Giriş Gönder</value> <value>Giriş Gönder</value>
@ -521,6 +521,7 @@
</data> </data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> <data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Tercih ettiğiniz arayüzü seçin ve başlamak için Yükle&apos;ye tıklayın</value> <value>Tercih ettiğiniz arayüzü seçin ve başlamak için Yükle&apos;ye tıklayın</value>
<comment>Fuzzy</comment>
</data> </data>
<data name="Label_Installing" xml:space="preserve"> <data name="Label_Installing" xml:space="preserve">
<value>Yükleniyor</value> <value>Yükleniyor</value>
@ -694,10 +695,10 @@
<value>{0} resim seçildi</value> <value>{0} resim seçildi</value>
</data> </data>
<data name="Label_OutputFolder" xml:space="preserve"> <data name="Label_OutputFolder" xml:space="preserve">
<value>Çıkış Klasörü</value> <value>Çıktı Klasörü</value>
</data> </data>
<data name="Label_OutputType" xml:space="preserve"> <data name="Label_OutputType" xml:space="preserve">
<value>Çıkış Türü</value> <value>Çıktı Türü</value>
</data> </data>
<data name="Action_ClearSelection" xml:space="preserve"> <data name="Action_ClearSelection" xml:space="preserve">
<value>Seçimi Temizle</value> <value>Seçimi Temizle</value>
@ -721,7 +722,7 @@
<value>Upscale</value> <value>Upscale</value>
</data> </data>
<data name="Label_OutputsPageTitle" xml:space="preserve"> <data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Çıkış Tarayıcısı</value> <value>Medya Kitaplığı</value>
</data> </data>
<data name="Label_OneImageSelected" xml:space="preserve"> <data name="Label_OneImageSelected" xml:space="preserve">
<value>1 resim seçildi</value> <value>1 resim seçildi</value>
@ -933,4 +934,31 @@
<data name="Label_VideoQuality" xml:space="preserve"> <data name="Label_VideoQuality" xml:space="preserve">
<value>Kalite</value> <value>Kalite</value>
</data> </data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Model Tarayıcıda Bul</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Kurulmuş</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>Uzantı bulunamadı.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Gizle</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Ayrıntıları Kopyala</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>İndir</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Paket kurulumlarınızın ve model indirmelerinizin ilerlemesini buradan kontrol edin.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Önerilen Modeller</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Paketiniz yüklenirken başlamanıza yardımcı olması için önerdiğimiz bazı modeller aşağıda verilmiştir.</value>
</data>
</root> </root>

41
StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs

@ -2,9 +2,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Hashing; using System.IO.Hashing;
using System.Linq;
using System.Text; using System.Text;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Avalonia.Models.Inference;
@ -17,7 +19,7 @@ public class ModuleApplyStepEventArgs : EventArgs
public NodeDictionary Nodes => Builder.Nodes; public NodeDictionary Nodes => Builder.Nodes;
public ModuleApplyStepTemporaryArgs Temp { get; } = new(); public ModuleApplyStepTemporaryArgs Temp { get; set; } = new();
/// <summary> /// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.) /// Generation overrides (like hires fix generate, current seed generate, etc.)
@ -26,6 +28,25 @@ public class ModuleApplyStepEventArgs : EventArgs
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = []; public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
/// <summary>
/// Creates a new <see cref="ModuleApplyStepEventArgs"/> with the given <see cref="ComfyNodeBuilder"/>.
/// </summary>
/// <returns></returns>
public ModuleApplyStepTemporaryArgs CreateTempFromBuilder()
{
return new ModuleApplyStepTemporaryArgs
{
Primary = Builder.Connections.Primary,
PrimaryVAE = Builder.Connections.PrimaryVAE,
Models = new Dictionary<string, ModelConnections>(
Builder.Connections.Models.ToDictionary(
pair => pair.Key,
pair => new ModelConnections(pair.Value)
)
)
};
}
public void AddFileTransfer(string sourcePath, string destinationRelativePath) public void AddFileTransfer(string sourcePath, string destinationRelativePath)
{ {
FilesToTransfer.Add((sourcePath, destinationRelativePath)); FilesToTransfer.Add((sourcePath, destinationRelativePath));
@ -54,22 +75,4 @@ public class ModuleApplyStepEventArgs : EventArgs
return destPath; return destPath;
} }
public class ModuleApplyStepTemporaryArgs
{
/// <summary>
/// Temporary conditioning apply step, used by samplers to apply control net.
/// </summary>
public ConditioningConnections? Conditioning { get; set; }
/// <summary>
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
/// </summary>
public ConditioningConnections? RefinerConditioning { get; set; }
/// <summary>
/// Temporary model apply step, used by samplers to apply control net.
/// </summary>
public ModelNodeConnection? Model { get; set; }
}
} }

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);

8
StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Services;
public interface IModelDownloadLinkHandler
{
Task StartListening();
}

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 />

248
StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs

@ -0,0 +1,248 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using MessagePipe;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))]
public class ModelDownloadLinkHandler(
IDistributedSubscriber<string, Uri> uriHandlerSubscriber,
ILogger<ModelDownloadLinkHandler> logger,
ICivitApi civitApi,
INotificationService notificationService,
ISettingsManager settingsManager,
IDownloadService downloadService,
ITrackedDownloadService trackedDownloadService
) : IAsyncDisposable, IModelDownloadLinkHandler
{
private IAsyncDisposable? uriHandlerSubscription;
private const string DownloadCivitModel = "downloadCivitModel";
public async Task StartListening()
{
uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync(
UriHandler.IpcKeySend,
UriReceivedHandler
);
}
public async ValueTask DisposeAsync()
{
if (uriHandlerSubscription is not null)
{
await uriHandlerSubscription.DisposeAsync();
uriHandlerSubscription = null;
}
}
private void UriReceivedHandler(Uri receivedUri)
{
logger.LogDebug("ModelDownloadLinkHandler Received URI: {Uri}", receivedUri.PathAndQuery);
if (!receivedUri.Host.Equals(DownloadCivitModel, StringComparison.OrdinalIgnoreCase))
return;
var queryDict = HttpUtility.ParseQueryString(receivedUri.Query);
var modelIdStr = queryDict["modelId"];
var modelVersionIdStr = queryDict["modelVersionId"];
var type = queryDict["type"];
var format = queryDict["format"];
var size = queryDict["size"];
var fp = queryDict["fp"];
if (
string.IsNullOrWhiteSpace(modelIdStr)
|| string.IsNullOrWhiteSpace(type)
|| string.IsNullOrWhiteSpace(format)
|| !int.TryParse(modelIdStr, out var modelId)
|| !Enum.TryParse<CivitFileType>(type, out var civitFileType)
|| !Enum.TryParse<CivitModelFormat>(format, out var civitFormat)
)
{
logger.LogError("ModelDownloadLinkHandler: Invalid query parameters");
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Invalid Download Link",
"The download link is invalid",
NotificationType.Error
)
)
);
return;
}
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
"Link Received",
"Successfully received download link",
NotificationType.Warning
)
);
var modelTask = civitApi.GetModelById(modelId);
modelTask.Wait();
var model = modelTask.Result;
var useModelVersion = !string.IsNullOrWhiteSpace(modelVersionIdStr);
var modelVersionId = useModelVersion ? int.Parse(modelVersionIdStr) : 0;
var modelVersion = useModelVersion
? model.ModelVersions?.FirstOrDefault(x => x.Id == modelVersionId)
: model.ModelVersions?.FirstOrDefault();
if (modelVersion is null)
{
logger.LogError("ModelDownloadLinkHandler: Model version not found");
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Model has no versions available",
"This model has no versions available for download",
NotificationType.Error
)
)
);
return;
}
var possibleFiles = modelVersion.Files?.Where(
x => x.Type == civitFileType && x.Metadata.Format == civitFormat
);
if (!string.IsNullOrWhiteSpace(fp) && Enum.TryParse<CivitModelFpType>(fp, out var fpType))
{
possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType);
}
if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse<CivitModelSize>(size, out var modelSize))
{
possibleFiles = possibleFiles?.Where(x => x.Metadata.Size == modelSize);
}
possibleFiles = possibleFiles?.ToList();
if (possibleFiles is null)
{
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Model has no files available",
"This model has no files available for download",
NotificationType.Error
)
)
);
logger.LogError("ModelDownloadLinkHandler: Model file not found");
return;
}
var selectedFile = possibleFiles.FirstOrDefault() ?? modelVersion.Files?.FirstOrDefault();
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(
selectedFile.Type == CivitFileType.VAE
? SharedFolderType.VAE.GetStringValue()
: model.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
downloadDirectory.Create();
var downloadPath = downloadDirectory.JoinFile(selectedFile.Name);
// Create tracked download
var download = trackedDownloadService.NewDownload(selectedFile.DownloadUrl, downloadPath);
// Download model info and preview first
var saveCmInfoTask = SaveCmInfo(model, modelVersion, selectedFile, downloadDirectory);
var savePreviewImageTask = SavePreviewImage(modelVersion, downloadPath);
Task.WaitAll([saveCmInfoTask, savePreviewImageTask]);
var cmInfoPath = saveCmInfoTask.Result;
var previewImagePath = savePreviewImageTask.Result;
// Add hash info
download.ExpectedHashSha256 = selectedFile.Hashes.SHA256;
// Add files to cleanup list
download.ExtraCleanupFileNames.Add(cmInfoPath);
if (previewImagePath is not null)
{
download.ExtraCleanupFileNames.Add(previewImagePath);
}
// Add hash context action
download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(selectedFile);
download.Start();
Dispatcher.UIThread.Post(
() => notificationService.Show("Download Started", $"Downloading {selectedFile.Name}")
);
}
private static async Task<FilePath> SaveCmInfo(
CivitModel model,
CivitModelVersion modelVersion,
CivitFile modelFile,
DirectoryPath downloadDirectory
)
{
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name);
var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow);
await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName);
var jsonName = $"{modelFileName}.cm-info.json";
return downloadDirectory.JoinFile(jsonName);
}
/// <summary>
/// Saves the preview image to the same directory as the model file
/// </summary>
/// <param name="modelVersion"></param>
/// <param name="modelFilePath"></param>
/// <returns>The file path of the saved preview image</returns>
private async Task<FilePath?> SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath)
{
// Skip if model has no images
if (modelVersion.Images == null || modelVersion.Images.Count == 0)
{
return null;
}
var image = modelVersion.Images[0];
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{
var imageDownloadPath = modelFilePath.Directory!.JoinFile(
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}"
);
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
await notificationService.TryAsync(imageTask, "Could not download preview image");
return imageDownloadPath;
}
return null;
}
}

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);
}
}

6
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -17,7 +17,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon> <ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.9.0-dev.999</Version> <Version>2.10.0-dev.999</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -66,8 +66,8 @@
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" /> <PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" /> <PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" /> <PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.230" /> <PackageReference Include="FluentIcons.Avalonia" Version="1.1.228" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.230" /> <PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.228" />
<PackageReference Include="FuzzySharp" Version="2.0.2" /> <PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" /> <PackageReference Include="Markdown.Avalonia" Version="11.0.2" />

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>

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

@ -16,10 +16,9 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ExifLibrary; using ExifLibrary;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
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 +58,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 +79,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 +261,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 +669,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 +685,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 +759,14 @@ public abstract partial class InferenceGenerationViewModelBase
var steps = new List<IPackageStep>(); var steps = new List<IPackageStep>();
foreach (var missingExtensionUrl in missingExtensions) // Add install for missing extensions
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;
} }
@ -706,6 +774,18 @@ public abstract partial class InferenceGenerationViewModelBase
steps.Add(new InstallExtensionStep(manager, localPackagePair.InstalledPackage, extension)); steps.Add(new InstallExtensionStep(manager, localPackagePair.InstalledPackage, extension));
} }
// Add update for out of date extensions
foreach (var (specifier, installed) in outOfDateExtensions)
{
if (!manifestExtensionsMap.TryGetValue(specifier.Name, out var extension))
{
Logger.Warn("Extension {MissingExtensionUrl} not found in manifests", specifier.Name);
continue;
}
steps.Add(new UpdateExtensionStep(manager, localPackagePair.InstalledPackage, installed));
}
var runner = new PackageModificationRunner var runner = new PackageModificationRunner
{ {
ShowDialogOnStart = true, ShowDialogOnStart = true,
@ -722,15 +802,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
{ {

178
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Collections; using Avalonia.Collections;
@ -15,12 +12,15 @@ using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Alias;
using DynamicData.Binding;
using LiteDB; using LiteDB;
using LiteDB.Async; using LiteDB.Async;
using NLog; using NLog;
using OneOf.Types;
using Refit; using Refit;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
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;
@ -41,24 +41,24 @@ namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(CivitAiBrowserPage))] [View(typeof(CivitAiBrowserPage))]
[Singleton] [Singleton]
public partial class CivitAiBrowserViewModel : TabViewModelBase public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScroll
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi; private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext; private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20; private const int MaxModelsPerPage = 20;
private LRUCache< private LRUCache<
int /* model id */ int /* model id */
, ,
CheckpointBrowserCardViewModel CheckpointBrowserCardViewModel
> cache = new(50); > cache = new(150);
[ObservableProperty] [ObservableProperty]
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards; private ObservableCollection<CheckpointBrowserCardViewModel> modelCards = new();
[ObservableProperty] [ObservableProperty]
private DataGridCollectionView? modelCardsView; private DataGridCollectionView? modelCardsView;
@ -81,27 +81,9 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
[ObservableProperty] [ObservableProperty]
private CivitModelType selectedModelType = CivitModelType.Checkpoint; private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty]
private int currentPageNumber;
[ObservableProperty]
private int totalPages;
[ObservableProperty] [ObservableProperty]
private bool hasSearched; private bool hasSearched;
[ObservableProperty]
private bool canGoToNextPage;
[ObservableProperty]
private bool canGoToPreviousPage;
[ObservableProperty]
private bool canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty] [ObservableProperty]
private bool isIndeterminate; private bool isIndeterminate;
@ -117,7 +99,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
[ObservableProperty] [ObservableProperty]
private bool showSantaHats = true; private bool showSantaHats = true;
private List<CheckpointBrowserCardViewModel> allModelCards = new(); [ObservableProperty]
private string? nextPageCursor;
public IEnumerable<CivitPeriod> AllCivitPeriods => public IEnumerable<CivitPeriod> AllCivitPeriods =>
Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>(); Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
@ -143,25 +126,11 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
) )
{ {
this.civitApi = civitApi; this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext; this.liteDbContext = liteDbContext;
this.notificationService = notificationService; this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested;
} }
@ -171,7 +140,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
return; return;
SearchQuery = $"$#{e}"; SearchQuery = $"$#{e}";
SearchModelsCommand.ExecuteAsync(null).SafeFireAndForget(); SearchModelsCommand.ExecuteAsync(false).SafeFireAndForget();
} }
public override void OnLoaded() public override void OnLoaded()
@ -223,7 +192,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
/// <summary> /// <summary>
/// Background update task /// Background update task
/// </summary> /// </summary>
private async Task CivitModelQuery(CivitModelsRequest request) private async Task CivitModelQuery(CivitModelsRequest request, bool isInfiniteScroll = false)
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
var queryText = request.Query; var queryText = request.Query;
@ -276,15 +245,9 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
} }
); );
if (cacheNew) UpdateModelCards(models, isInfiniteScroll);
{
Logger.Debug("New cache entry, updating model cards"); NextPageCursor = modelsResponse.Metadata?.NextCursor;
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -327,7 +290,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
/// <summary> /// <summary>
/// Updates model cards using api response object. /// Updates model cards using api response object.
/// </summary> /// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata) private void UpdateModelCards(List<CivitModel>? models, bool addCards = false)
{ {
if (models is null) if (models is null)
{ {
@ -335,7 +298,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
} }
else else
{ {
var updateCards = models var modelsToAdd = models
.Select(model => .Select(model =>
{ {
var cachedViewModel = cache.Get(model.Id); var cachedViewModel = cache.Get(model.Id);
@ -364,23 +327,34 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
return newCard; return newCard;
}) })
.ToList(); .Where(FilterModelCardsPredicate);
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed) if (SortMode == CivitSortMode.Installed)
{ {
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available"); modelsToAdd = modelsToAdd.OrderByDescending(x => x.UpdateCardText == "Update Available");
}
if (!addCards)
{
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(modelsToAdd);
} }
else
{
foreach (var model in modelsToAdd)
{
if (
ModelCards.Contains(
model,
new PropertyComparer<CheckpointBrowserCardViewModel>(x => x.CivitModel.Id)
)
)
continue;
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards); ModelCards.Add(model);
}
}
} }
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update // Status update
ShowMainLoadingSpinner = false; ShowMainLoadingSpinner = false;
IsIndeterminate = false; IsIndeterminate = false;
@ -390,27 +364,30 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
private string previousSearchQuery = string.Empty; private string previousSearchQuery = string.Empty;
[RelayCommand] [RelayCommand]
private async Task SearchModels() private async Task SearchModels(bool isInfiniteScroll = false)
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery) if (SearchQuery != previousSearchQuery || !isInfiniteScroll)
{ {
// Reset page number // Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery; previousSearchQuery = SearchQuery;
NextPageCursor = null;
} }
// Build request // Build request
var modelRequest = new CivitModelsRequest var modelRequest = new CivitModelsRequest
{ {
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter Nsfw = "true", // Handled by local view filter
Sort = SortMode, Sort = SortMode,
Period = SelectedPeriod, Period = SelectedPeriod
Page = CurrentPageNumber
}; };
if (NextPageCursor != null)
{
modelRequest.Cursor = NextPageCursor;
}
if (SelectedModelType != CivitModelType.All) if (SelectedModelType != CivitModelType.All)
{ {
modelRequest.Types = [SelectedModelType]; modelRequest.Types = [SelectedModelType];
@ -516,14 +493,15 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
modelRequest.GetHashCode(), modelRequest.GetHashCode(),
elapsed.TotalSeconds elapsed.TotalSeconds
); );
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata); NextPageCursor = cachedQuery.Metadata?.NextCursor;
UpdateModelCards(cachedQuery.Items, isInfiniteScroll);
// Start remote query (background mode) // Start remote query (background mode)
// Skip when last query was less than 2 min ago // Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt; var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2) if (timeSinceCache?.TotalMinutes >= 2)
{ {
CivitModelQuery(modelRequest).SafeFireAndForget(); CivitModelQuery(modelRequest, isInfiniteScroll).SafeFireAndForget();
Logger.Debug( Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query", "Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds timeSinceCache.Value.TotalSeconds
@ -534,54 +512,23 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
{ {
// Not cached, wait for remote query // Not cached, wait for remote query
ShowMainLoadingSpinner = true; ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest); await CivitModelQuery(modelRequest, isInfiniteScroll);
} }
UpdateResultsText(); UpdateResultsText();
} }
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery() public void ClearSearchQuery()
{ {
SearchQuery = string.Empty; SearchQuery = string.Empty;
} }
partial void OnShowNsfwChanged(bool value) public async Task LoadNextPageAsync()
{ {
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value); if (NextPageCursor != null)
// ModelCardsView?.Refresh(); {
var updateCards = allModelCards.Where(FilterModelCardsPredicate); await SearchModelsCommand.ExecuteAsync(true);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards); }
if (!HasSearched)
return;
UpdateResultsText();
} }
partial void OnSelectedPeriodChanged(CivitPeriod value) partial void OnSelectedPeriodChanged(CivitPeriod value)
@ -596,6 +543,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType SelectedBaseModelType
) )
); );
NextPageCursor = null;
} }
partial void OnSortModeChanged(CivitSortMode value) partial void OnSortModeChanged(CivitSortMode value)
@ -610,6 +558,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType SelectedBaseModelType
) )
); );
NextPageCursor = null;
} }
partial void OnSelectedModelTypeChanged(CivitModelType value) partial void OnSelectedModelTypeChanged(CivitModelType value)
@ -624,6 +573,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType SelectedBaseModelType
) )
); );
NextPageCursor = null;
} }
partial void OnSelectedBaseModelTypeChanged(string value) partial void OnSelectedBaseModelTypeChanged(string value)
@ -638,6 +588,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
value value
) )
); );
NextPageCursor = null;
} }
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
@ -648,18 +599,17 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
if (shouldUpdatePageNumber) if (shouldUpdatePageNumber)
{ {
CurrentPageNumber = 1; NextPageCursor = null;
} }
// execute command instead of calling method directly so that the IsRunning property gets updated // execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null); await SearchModelsCommand.ExecuteAsync(false);
} }
private void UpdateResultsText() private void UpdateResultsText()
{ {
NoResultsFound = ModelCards?.Count <= 0; NoResultsFound = ModelCards?.Count <= 0;
NoResultsText = NoResultsText = "No results found";
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found";
} }
public override string Header => Resources.Label_CivitAi; public override string Header => Resources.Label_CivitAi;

8
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -8,6 +8,7 @@ using Avalonia.Data;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
@ -120,6 +121,7 @@ public partial class CheckpointFile : ViewModelBase
if (File.Exists(FilePath)) if (File.Exists(FilePath))
{ {
IsLoading = true; IsLoading = true;
Progress = new ProgressReport(0f, "Deleting...");
try try
{ {
await using var delay = new MinimumDelay(200, 500); await using var delay = new MinimumDelay(200, 500);
@ -135,6 +137,12 @@ public partial class CheckpointFile : ViewModelBase
{ {
await Task.Run(() => File.Delete(cmInfoPath)); await Task.Run(() => File.Delete(cmInfoPath));
} }
var settingsManager = App.Services.GetRequiredService<ISettingsManager>();
settingsManager.Transaction(s =>
{
s.InstalledModelHashes?.Remove(ConnectedModel.Hashes.BLAKE3);
});
} }
} }
catch (IOException ex) catch (IOException ex)

12
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -111,6 +111,10 @@ public partial class CheckpointFolder : ViewModelBase
public IObservableCollection<string> BaseModelOptions { get; } = public IObservableCollection<string> BaseModelOptions { get; } =
new ObservableCollectionExtended<string>(); new ObservableCollectionExtended<string>();
private IEnumerable<string> allModelOptions = Enum.GetValues<CivitBaseModelType>()
.Where(x => x != CivitBaseModelType.All)
.Select(x => x.GetStringValue());
public CheckpointFolder( public CheckpointFolder(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDownloadService downloadService, IDownloadService downloadService,
@ -175,11 +179,7 @@ public partial class CheckpointFolder : ViewModelBase
SubFoldersCache.Refresh(); SubFoldersCache.Refresh();
}); });
BaseModelOptionsCache.AddOrUpdate( BaseModelOptionsCache.AddOrUpdate(allModelOptions);
Enum.GetValues<CivitBaseModelType>()
.Where(x => x != CivitBaseModelType.All)
.Select(x => x.GetStringValue())
);
CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged; CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged;
// DisplayedCheckpointFiles = CheckpointFiles; // DisplayedCheckpointFiles = CheckpointFiles;
@ -187,7 +187,7 @@ public partial class CheckpointFolder : ViewModelBase
private bool BaseModelFilter(CheckpointFile file) private bool BaseModelFilter(CheckpointFile file)
{ {
return file.IsConnectedModel return file.IsConnectedModel && allModelOptions.Contains(file.ConnectedModel!.BaseModel)
? BaseModelOptions.Contains(file.ConnectedModel!.BaseModel) ? BaseModelOptions.Contains(file.ConnectedModel!.BaseModel)
: BaseModelOptions.Contains("Other"); : BaseModelOptions.Contains("Other");
} }

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 };

21
StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs

@ -0,0 +1,21 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(ConfirmPackageDeleteDialog))]
[ManagedService]
[Transient]
public partial class ConfirmPackageDeleteDialogViewModel : ContentDialogViewModelBase
{
public required string ExpectedPackageName { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsValid))]
private string packageName = string.Empty;
public bool IsValid => ExpectedPackageName.Equals(PackageName, StringComparison.Ordinal);
}

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);
});
} }
} }

7
StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs

@ -75,7 +75,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
.Connect() .Connect()
.DeferUntilLoaded() .DeferUntilLoaded()
.Filter(incompatiblePredicate) .Filter(incompatiblePredicate)
.Filter(p => p.OfferInOneClickInstaller || ShowIncompatiblePackages) .Filter(p => p.OfferInOneClickInstaller)
.Sort( .Sort(
SortExpressionComparer<BasePackage> SortExpressionComparer<BasePackage>
.Ascending(p => p.InstallerSortOrder) .Ascending(p => p.InstallerSortOrder)
@ -85,6 +85,10 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
.Subscribe(); .Subscribe();
AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages()); AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages());
if (ShownPackages.Count > 0)
return;
ShowIncompatiblePackages = true;
} }
[RelayCommand] [RelayCommand]
@ -182,6 +186,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
{ {
ShowDialogOnStart = false, ShowDialogOnStart = false,
HideCloseButton = false, HideCloseButton = false,
ModificationCompleteMessage = $"{installedPackage.DisplayName} Install Complete",
}; };
runner runner

7
StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs

@ -192,7 +192,12 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage); var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage);
steps.Add(addInstalledPackageStep); steps.Add(addInstalledPackageStep);
var runner = new PackageModificationRunner { ShowDialogOnStart = true, HideCloseButton = true, }; var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
HideCloseButton = true,
ModificationCompleteMessage = "Install complete"
};
EventManager.Instance.OnPackageInstallProgressAdded(runner); EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps); await runner.ExecuteSteps(steps);

6
StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs

@ -97,9 +97,11 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
new RecommendedModelItemViewModel new RecommendedModelItemViewModel
{ {
ModelVersion = model.ModelVersions.First( ModelVersion = model.ModelVersions.First(
x => !x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase) x =>
!x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase)
&& !x.BaseModel.Contains("Lightning", StringComparison.OrdinalIgnoreCase)
), ),
Author = $"by {model.Creator.Username}", Author = $"by {model.Creator?.Username}",
CivitModel = model CivitModel = model
} }
) )

19
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs

@ -91,7 +91,6 @@ public partial class SelectModelVersionViewModel(
{ {
SelectedVersionViewModel = Versions[0]; SelectedVersionViewModel = Versions[0];
CanGoToNextImage = true; CanGoToNextImage = true;
LoadInstallLocations();
} }
partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value) partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value)
@ -130,6 +129,8 @@ public partial class SelectModelVersionViewModel(
var canImport = true; var canImport = true;
if (settingsManager.IsLibraryDirSet) if (settingsManager.IsLibraryDirSet)
{ {
LoadInstallLocations();
var fileSizeBytes = value?.CivitFile.SizeKb * 1024; var fileSizeBytes = value?.CivitFile.SizeKb * 1024;
var freeSizeBytes = var freeSizeBytes =
SystemInfo.GetDiskFreeSpaceBytes(settingsManager.ModelsDirectory) ?? long.MaxValue; SystemInfo.GetDiskFreeSpaceBytes(settingsManager.ModelsDirectory) ?? long.MaxValue;
@ -151,9 +152,9 @@ public partial class SelectModelVersionViewModel(
IsImportEnabled = value?.CivitFile != null && canImport && !ShowEmptyPathWarning; IsImportEnabled = value?.CivitFile != null && canImport && !ShowEmptyPathWarning;
} }
partial void OnSelectedInstallLocationChanged(string value) partial void OnSelectedInstallLocationChanged(string? value)
{ {
if (value.Equals("Custom...", StringComparison.OrdinalIgnoreCase)) if (value?.Equals("Custom...", StringComparison.OrdinalIgnoreCase) is true)
{ {
Dispatcher.UIThread.InvokeAsync(SelectCustomFolder); Dispatcher.UIThread.InvokeAsync(SelectCustomFolder);
} }
@ -304,14 +305,16 @@ public partial class SelectModelVersionViewModel(
private void LoadInstallLocations() private void LoadInstallLocations()
{ {
var installLocations = new ObservableCollection<string>();
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir( var downloadDirectory = rootModelsDirectory.JoinDir(
CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue() SelectedFile?.CivitFile.Type == CivitFileType.VAE
? SharedFolderType.VAE.GetStringValue()
: CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
); );
var installLocations = new ObservableCollection<string>
{ installLocations.Add(downloadDirectory.ToString().Replace(rootModelsDirectory, "Models"));
downloadDirectory.ToString().Replace(rootModelsDirectory, "Models")
};
foreach (var directory in downloadDirectory.EnumerateDirectories()) foreach (var directory in downloadDirectory.EnumerateDirectories())
{ {

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;

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

@ -6,6 +6,9 @@ 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.Extensions;
using StabilityMatrix.Core.Helper;
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 +42,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 +50,101 @@ 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;
}
// If ReferenceOnly is selected, use special node
if (card.SelectedModel == RemoteModels.ControlNetReferenceOnlyModel)
{
// We need to rescale image to be the current primary size if it's not already
var originalPrimary = e.Temp.Primary!.Unwrap();
var originalPrimarySize = e.Builder.Connections.PrimarySize;
if (card.SelectImageCardViewModel.CurrentBitmapSize != originalPrimarySize)
{
var scaled = e.Builder.Group_Upscale(
e.Nodes.GetUniqueName("ControlNet_Rescale"),
image,
e.Temp.GetDefaultVAE(),
ComfyUpscaler.NearestExact,
originalPrimarySize.Width,
originalPrimarySize.Height
);
e.Temp.Primary = scaled;
}
else
{
e.Temp.Primary = image;
}
// Set image as new latent source, add reference only node
var model = e.Temp.GetRefinerOrBaseModel();
var controlNetReferenceOnly = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ReferenceOnlySimple
{
Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly"),
Reference = e.Builder.GetPrimaryAsLatent(
e.Temp.Primary,
e.Builder.Connections.GetDefaultVAE()
),
Model = model
}
);
var referenceOnlyModel = controlNetReferenceOnly.Output1;
// If ControlNet strength is not 1, add Model Merge
if (Math.Abs(card.Strength - 1) > 0.01)
{
var modelBlend = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ModelMergeSimple
{
Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly_ModelMerge"),
Model1 = referenceOnlyModel,
Model2 = e.Temp.GetRefinerOrBaseModel(),
// Where 0 is full reference only, 1 is full original
Ratio = 1 - card.Strength
}
);
referenceOnlyModel = modelBlend.Output;
}
// Set output as new primary and model source
if (model == e.Temp.Refiner.Model)
{
e.Temp.Refiner.Model = referenceOnlyModel;
}
else
{
e.Temp.Base.Model = referenceOnlyModel;
}
e.Temp.Primary = controlNetReferenceOnly.Output2;
// Indicate that the Primary latent has been temp batched
// https://github.com/comfyanonymous/ComfyUI_experiments/issues/11
e.Temp.IsPrimaryTempBatched = true;
// Index 0 is the original image, index 1 is the reference only latent
e.Temp.PrimaryTempBatchPickIndex = 1;
return;
}
var controlNetLoader = e.Nodes.AddTypedNode( var controlNetLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetLoader new ComfyNodeBuilder.ControlNetLoader
@ -62,36 +159,36 @@ 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.Base.Conditioning!.Unwrap().Positive,
Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"), Negative = e.Temp.Base.Conditioning.Negative,
Strength = card.Strength, Strength = card.Strength,
StartPercent = card.StartPercent, StartPercent = card.StartPercent,
EndPercent = card.EndPercent, EndPercent = card.EndPercent,
} }
); );
e.Temp.Conditioning = (controlNetApply.Output1, controlNetApply.Output2); e.Temp.Base.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
// Refiner if available // Refiner if available
if (e.Temp.RefinerConditioning is not null) if (e.Temp.Refiner.Conditioning is not null)
{ {
var controlNetRefinerApply = e.Nodes.AddTypedNode( var controlNetRefinerApply = e.Nodes.AddTypedNode(
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.Refiner.Conditioning!.Unwrap().Positive,
Negative = e.Temp.RefinerConditioning.Negative, Negative = e.Temp.Refiner.Conditioning.Negative,
Strength = card.Strength, Strength = card.Strength,
StartPercent = card.StartPercent, StartPercent = card.StartPercent,
EndPercent = card.EndPercent, EndPercent = card.EndPercent,
} }
); );
e.Temp.RefinerConditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2); e.Temp.Refiner.Conditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2);
} }
} }
} }

57
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs

@ -78,30 +78,39 @@ public partial class HiresFixModule : ModuleBase
); );
} }
var hiresSampler = builder // If we need to inherit primary sampler addons, use their temp args
.Nodes if (samplerCard.InheritPrimarySamplerAddons)
.AddTypedNode( {
new ComfyNodeBuilder.KSampler e.Temp = e.Builder.Connections.BaseSamplerTemporaryArgs ?? e.CreateTempFromBuilder();
{ }
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"), else
Model = builder.Connections.GetRefinerOrBaseModel(), {
Seed = builder.Connections.Seed, // otherwise just use new ones
Steps = samplerCard.Steps, e.Temp = e.CreateTempFromBuilder();
Cfg = samplerCard.CfgScale, }
SamplerName =
samplerCard.SelectedSampler?.Name var hiresSampler = builder.Nodes.AddTypedNode(
?? e.Builder.Connections.PrimarySampler?.Name new ComfyNodeBuilder.KSampler
?? throw new ArgumentException("No PrimarySampler"), {
Scheduler = Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
samplerCard.SelectedScheduler?.Name Model = builder.Connections.GetRefinerOrBaseModel(),
?? e.Builder.Connections.PrimaryScheduler?.Name Seed = builder.Connections.Seed,
?? throw new ArgumentException("No PrimaryScheduler"), Steps = samplerCard.Steps,
Positive = builder.Connections.GetRefinerOrBaseConditioning().Positive, Cfg = samplerCard.CfgScale,
Negative = builder.Connections.GetRefinerOrBaseConditioning().Negative, SamplerName =
LatentImage = builder.GetPrimaryAsLatent(), samplerCard.SelectedSampler?.Name
Denoise = samplerCard.DenoiseStrength ?? e.Builder.Connections.PrimarySampler?.Name
} ?? throw new ArgumentException("No PrimarySampler"),
); Scheduler =
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = e.Temp.GetRefinerOrBaseConditioning().Positive,
Negative = e.Temp.GetRefinerOrBaseConditioning().Negative,
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
// Set as primary // Set as primary
builder.Connections.Primary = hiresSampler.Output; builder.Connections.Primary = hiresSampler.Output;

68
StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs

@ -87,6 +87,11 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
[Required] [Required]
private ComfyScheduler? selectedScheduler = ComfyScheduler.Normal; private ComfyScheduler? selectedScheduler = ComfyScheduler.Normal;
[ObservableProperty]
[property: Category("Settings")]
[property: DisplayName("Inherit Primary Sampler Addons")]
private bool inheritPrimarySamplerAddons = true;
[JsonPropertyName("Modules")] [JsonPropertyName("Modules")]
public StackEditableCardViewModel ModulesCardViewModel { get; } public StackEditableCardViewModel ModulesCardViewModel { get; }
@ -130,8 +135,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
} }
// Provide temp values // Provide temp values
e.Temp.Conditioning = e.Builder.Connections.Base.Conditioning; e.Temp = e.CreateTempFromBuilder();
e.Temp.RefinerConditioning = e.Builder.Connections.Refiner.Conditioning;
// Apply steps from our addons // Apply steps from our addons
ApplyAddonSteps(e); ApplyAddonSteps(e);
@ -142,17 +146,26 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
if (!e.Nodes.ContainsKey("Sampler")) if (!e.Nodes.ContainsKey("Sampler"))
{ {
ApplyStepsInitialSampler(e); ApplyStepsInitialSampler(e);
// Save temp
e.Builder.Connections.BaseSamplerTemporaryArgs = e.Temp;
} }
else else
{ {
ApplyStepsAdditionalSampler(e); // Hires does its own sampling so just throw I guess
throw new InvalidOperationException(
"Sampler ApplyStep was called when Sampler node already exists"
);
} }
} }
private void ApplyStepsInitialSampler(ModuleApplyStepEventArgs e) private void ApplyStepsInitialSampler(ModuleApplyStepEventArgs e)
{ {
// Get primary as latent using vae // Get primary as latent using vae
var primaryLatent = e.Builder.GetPrimaryAsLatent(); var primaryLatent = e.Builder.GetPrimaryAsLatent(
e.Temp.Primary!.Unwrap(),
e.Builder.Connections.GetDefaultVAE()
);
// Set primary sampler and scheduler // Set primary sampler and scheduler
var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected"); var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
@ -162,8 +175,8 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
e.Builder.Connections.PrimaryScheduler = primaryScheduler; e.Builder.Connections.PrimaryScheduler = primaryScheduler;
// Use Temp Conditioning that may be modified by addons // Use Temp Conditioning that may be modified by addons
var conditioning = e.Temp.Conditioning.Unwrap(); var conditioning = e.Temp.Base.Conditioning.Unwrap();
var refinerConditioning = e.Temp.RefinerConditioning; var refinerConditioning = e.Temp.Refiner.Conditioning;
// Use custom sampler if SDTurbo scheduler is selected // Use custom sampler if SDTurbo scheduler is selected
if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo) if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo)
@ -209,21 +222,16 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
); );
e.Builder.Connections.Primary = sampler.Output1; e.Builder.Connections.Primary = sampler.Output1;
return;
} }
// Use KSampler if no refiner, otherwise need KSamplerAdvanced // Use KSampler if no refiner, otherwise need KSamplerAdvanced
if (e.Builder.Connections.Refiner.Model is null) else if (e.Builder.Connections.Refiner.Model is null)
{ {
var baseConditioning = e.Builder.Connections.Base.Conditioning.Unwrap();
// No refiner // No refiner
var sampler = e.Nodes.AddTypedNode( var sampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler new ComfyNodeBuilder.KSampler
{ {
Name = "Sampler", Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(), Model = e.Temp.Base.Model!.Unwrap(),
Seed = e.Builder.Connections.Seed, Seed = e.Builder.Connections.Seed,
SamplerName = primarySampler.Name, SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name, Scheduler = primaryScheduler.Name,
@ -245,7 +253,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
new ComfyNodeBuilder.KSamplerAdvanced new ComfyNodeBuilder.KSamplerAdvanced
{ {
Name = "Sampler", Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(), Model = e.Temp.Base.Model!.Unwrap(),
AddNoise = true, AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed, NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps, Steps = TotalSteps,
@ -261,8 +269,30 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
} }
); );
e.Builder.Connections.Primary = sampler.Output;
}
// If temp batched, add a LatentFromBatch to pick the temp batch right after first sampler
if (e.Temp.IsPrimaryTempBatched)
{
e.Builder.Connections.Primary = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = e.Nodes.GetUniqueName("ControlNet_LatentFromBatch"),
Samples = e.Builder.GetPrimaryAsLatent(),
BatchIndex = e.Temp.PrimaryTempBatchPickIndex,
// Use max length here as recommended
// https://github.com/comfyanonymous/ComfyUI_experiments/issues/11
Length = 64
}
).Output;
}
// Refiner
if (e.Builder.Connections.Refiner.Model is not null)
{
// Add refiner sampler // Add refiner sampler
var refinerSampler = e.Nodes.AddTypedNode( e.Builder.Connections.Primary = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSamplerAdvanced new ComfyNodeBuilder.KSamplerAdvanced
{ {
Name = "Sampler_Refiner", Name = "Sampler_Refiner",
@ -276,19 +306,15 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
Positive = refinerConditioning!.Positive, Positive = refinerConditioning!.Positive,
Negative = refinerConditioning.Negative, Negative = refinerConditioning.Negative,
// Connect to previous sampler // Connect to previous sampler
LatentImage = sampler.Output, LatentImage = e.Builder.GetPrimaryAsLatent(),
StartAtStep = Steps, StartAtStep = Steps,
EndAtStep = TotalSteps, EndAtStep = TotalSteps,
ReturnWithLeftoverNoise = false ReturnWithLeftoverNoise = false
} }
); ).Output;
e.Builder.Connections.Primary = refinerSampler.Output;
} }
} }
private void ApplyStepsAdditionalSampler(ModuleApplyStepEventArgs e) { }
/// <summary> /// <summary>
/// Applies each step of our addons /// Applies each step of our addons
/// </summary> /// </summary>

13
StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs

@ -90,9 +90,18 @@ public partial class SelectImageCardViewModel(INotificationService notificationS
partial void OnImageSourceChanged(ImageSource? value) partial void OnImageSourceChanged(ImageSource? value)
{ {
// Cache the hash for later upload use // Cache the hash for later upload use
if (value?.LocalFile is { Exists: true }) if (value?.LocalFile is { Exists: true } localFile)
{ {
value.GetBlake3HashAsync().SafeFireAndForget(); value
.GetBlake3HashAsync()
.SafeFireAndForget(ex =>
{
Logger.Warn(ex, "Error getting hash for image {Path}", localFile.Name);
notificationService.ShowPersistent(
$"Error getting hash for image {localFile.Name}",
$"{ex.GetType().Name}: {ex.Message}"
);
});
} }
} }

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>

33
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
@ -36,6 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase
private readonly ITrackedDownloadService trackedDownloadService; private readonly ITrackedDownloadService trackedDownloadService;
private readonly IDiscordRichPresenceService discordRichPresenceService; private readonly IDiscordRichPresenceService discordRichPresenceService;
private readonly IModelIndexService modelIndexService; private readonly IModelIndexService modelIndexService;
private readonly Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler;
public string Greeting => "Welcome to Avalonia!"; public string Greeting => "Welcome to Avalonia!";
[ObservableProperty] [ObservableProperty]
@ -71,7 +73,8 @@ public partial class MainWindowViewModel : ViewModelBase
IDiscordRichPresenceService discordRichPresenceService, IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory, ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService, ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService IModelIndexService modelIndexService,
Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler
) )
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -79,6 +82,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.discordRichPresenceService = discordRichPresenceService; this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService; this.trackedDownloadService = trackedDownloadService;
this.modelIndexService = modelIndexService; this.modelIndexService = modelIndexService;
this.modelDownloadLinkHandler = modelDownloadLinkHandler;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>(); ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>(); UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
} }
@ -107,6 +111,25 @@ public partial class MainWindowViewModel : ViewModelBase
return; return;
} }
try
{
await modelDownloadLinkHandler.Value.StartListening();
}
catch (IOException)
{
var dialog = new BetterContentDialog
{
Title = Resources.Label_StabilityMatrixAlreadyRunning,
Content = Resources.Label_AnotherInstanceAlreadyRunning,
IsPrimaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Close,
DefaultButton = ContentDialogButton.Primary
};
await dialog.ShowAsync();
App.Shutdown();
return;
}
// Initialize Discord Rich Presence (this needs LibraryDir so is set here) // Initialize Discord Rich Presence (this needs LibraryDir so is set here)
discordRichPresenceService.UpdateState(); discordRichPresenceService.UpdateState();
@ -139,14 +162,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();

176
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using AsyncImageLoader; using AsyncImageLoader;
@ -28,8 +29,6 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage; using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -52,19 +51,21 @@ public partial class OutputsPageViewModel : PageViewModelBase
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly INavigationService<MainWindowViewModel> navigationService; private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ILogger<OutputsPageViewModel> logger; private readonly ILogger<OutputsPageViewModel> logger;
private readonly List<CancellationTokenSource> cancellationTokenSources = [];
public override string Title => Resources.Label_OutputsPageTitle; public override string Title => Resources.Label_OutputsPageTitle;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true }; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
public SourceCache<LocalImageFile, string> OutputsCache { get; } = new(file => file.AbsolutePath); public SourceCache<LocalImageFile, string> OutputsCache { get; } = new(file => file.AbsolutePath);
private SourceCache<PackageOutputCategory, string> categoriesCache = new(category => category.Path);
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } = public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>(); new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>(); public IObservableCollection<PackageOutputCategory> Categories { get; set; } =
new ObservableCollectionExtended<PackageOutputCategory>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))] [NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
@ -86,6 +87,15 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private bool isConsolidating; private bool isConsolidating;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private bool showFolders;
[ObservableProperty]
private bool isChangingCategory;
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 =>
@ -95,6 +105,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
private string[] allowedExtensions = [".png", ".webp"]; private string[] allowedExtensions = [".png", ".webp"];
private PackageOutputCategory? lastOutputCategory;
public OutputsPageViewModel( public OutputsPageViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IPackageFactory packageFactory, IPackageFactory packageFactory,
@ -113,7 +125,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
// Observable predicate from SearchQuery changes // Observable predicate from SearchQuery changes
var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))! .Throttle(TimeSpan.FromMilliseconds(100))!
.Select(property => searcher.GetPredicate(property.Value)) .Select(property => searcher.GetPredicate(property.Value))
.AsObservable(); .AsObservable();
@ -122,28 +134,37 @@ public partial class OutputsPageViewModel : PageViewModelBase
.DeferUntilLoaded() .DeferUntilLoaded()
.Filter(searchPredicate) .Filter(searchPredicate)
.Transform(file => new OutputImageViewModel(file)) .Transform(file => new OutputImageViewModel(file))
.SortBy(vm => vm.ImageFile.CreatedAt, SortDirection.Descending) .Sort(
SortExpressionComparer<OutputImageViewModel>
.Descending(vm => vm.ImageFile.CreatedAt)
.ThenByDescending(vm => vm.ImageFile.FileName)
)
.Bind(Outputs) .Bind(Outputs)
.WhenPropertyChanged(p => p.IsSelected) .WhenPropertyChanged(p => p.IsSelected)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(_ => .Subscribe(_ =>
{ {
NumItemsSelected = Outputs.Count(o => o.IsSelected); NumItemsSelected = Outputs.Count(o => o.IsSelected);
}); });
categoriesCache.Connect().DeferUntilLoaded().Bind(Categories).Subscribe();
settingsManager.RelayPropertyFor( settingsManager.RelayPropertyFor(
this, this,
vm => vm.ImageSize, vm => vm.ImageSize,
settings => settings.OutputsImageSize, settings => settings.OutputsImageSize,
delay: TimeSpan.FromMilliseconds(250) delay: TimeSpan.FromMilliseconds(250)
); );
}
protected override void OnInitialLoaded() settingsManager.RelayPropertyFor(
{ this,
RefreshCategories(); vm => vm.ShowFolders,
settings => settings.IsOutputsTreeViewEnabled,
true
);
} }
public override void OnLoaded() protected override void OnInitialLoaded()
{ {
if (Design.IsDesignMode) if (Design.IsDesignMode)
return; return;
@ -151,14 +172,17 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (!settingsManager.IsLibraryDirSet) if (!settingsManager.IsLibraryDirSet)
return; return;
base.OnLoaded();
Directory.CreateDirectory(settingsManager.ImagesDirectory); Directory.CreateDirectory(settingsManager.ImagesDirectory);
RefreshCategories();
SelectedCategory ??= Categories.First(); SelectedCategory ??= Categories.First();
SelectedOutputType ??= SharedOutputType.All; SelectedOutputType ??= SharedOutputType.All;
SearchQuery = string.Empty; SearchQuery = string.Empty;
ImageSize = settingsManager.Settings.OutputsImageSize; ImageSize = settingsManager.Settings.OutputsImageSize;
lastOutputCategory = SelectedCategory;
IsChangingCategory = true;
var path = var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
@ -169,7 +193,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
partial void OnSelectedCategoryChanged(PackageOutputCategory? oldValue, PackageOutputCategory? newValue) partial void OnSelectedCategoryChanged(PackageOutputCategory? oldValue, PackageOutputCategory? newValue)
{ {
if (oldValue == newValue || newValue == null) if (oldValue == newValue || oldValue == null || newValue == null)
return; return;
var path = var path =
@ -177,11 +201,12 @@ public partial class OutputsPageViewModel : PageViewModelBase
? Path.Combine(newValue.Path, SelectedOutputType.ToString()) ? Path.Combine(newValue.Path, SelectedOutputType.ToString())
: SelectedCategory.Path; : SelectedCategory.Path;
GetOutputs(path); GetOutputs(path);
lastOutputCategory = newValue;
} }
partial void OnSelectedOutputTypeChanged(SharedOutputType? oldValue, SharedOutputType? newValue) partial void OnSelectedOutputTypeChanged(SharedOutputType? oldValue, SharedOutputType? newValue)
{ {
if (oldValue == newValue || newValue == null) if (oldValue == newValue || oldValue == null || newValue == null)
return; return;
var path = var path =
@ -262,8 +287,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
public void Refresh() public void Refresh()
{ {
Dispatcher.UIThread.Post(() => RefreshCategories()); Dispatcher.UIThread.Post(RefreshCategories);
Dispatcher.UIThread.Post(OnLoaded);
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
} }
public async Task DeleteImage(OutputImageViewModel? item) public async Task DeleteImage(OutputImageViewModel? item)
@ -273,8 +303,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 +382,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,
@ -491,7 +521,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
} }
} }
OnLoaded(); Refresh();
IsConsolidating = false; IsConsolidating = false;
} }
@ -517,20 +547,58 @@ public partial class OutputsPageViewModel : PageViewModelBase
return; return;
} }
var files = Directory if (lastOutputCategory?.Path.Equals(directory) is not true)
.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(path => allowedExtensions.Contains(new FilePath(path).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
if (files.Count == 0)
{ {
OutputsCache.Clear(); OutputsCache.Clear();
IsChangingCategory = true;
} }
else
IsLoading = true;
cancellationTokenSources.ForEach(cts => cts.Cancel());
Task.Run(() =>
{ {
OutputsCache.EditDiff(files); var getOutputsTokenSource = new CancellationTokenSource();
} cancellationTokenSources.Add(getOutputsTokenSource);
if (getOutputsTokenSource.IsCancellationRequested)
{
cancellationTokenSources.Remove(getOutputsTokenSource);
return;
}
var files = Directory
.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(file => allowedExtensions.Contains(new FilePath(file).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
if (getOutputsTokenSource.IsCancellationRequested)
{
cancellationTokenSources.Remove(getOutputsTokenSource);
return;
}
Dispatcher.UIThread.Post(() =>
{
if (files.Count == 0 && OutputsCache.Count == 0)
{
IsLoading = false;
IsChangingCategory = false;
return;
}
OutputsCache.EditDiff(
files,
(oldItem, newItem) => oldItem.AbsolutePath == newItem.AbsolutePath
);
IsLoading = false;
IsChangingCategory = false;
});
cancellationTokenSources.Remove(getOutputsTokenSource);
});
} }
private void RefreshCategories() private void RefreshCategories()
@ -559,7 +627,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 +640,37 @@ 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( categoriesCache.EditDiff(packageCategories, (a, b) => a.Path == b.Path);
1,
new PackageOutputCategory { Path = settingsManager.ImagesInferenceDirectory, Name = "Inference" } SelectedCategory = previouslySelectedCategory ?? Categories.First();
); }
private ObservableCollection<PackageOutputCategory> GetSubfolders(string strPath)
{
var subfolders = new ObservableCollection<PackageOutputCategory>();
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories); 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);
}
SelectedCategory = return subfolders;
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
} }
} }

287
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 void Launch() public override void OnUnloaded()
{
EventManager.Instance.PackageRelaunchRequested -= InstanceOnPackageRelaunchRequested;
runningPackageService.RunningPackages.CollectionChanged -= RunningPackagesOnCollectionChanged;
}
public async Task Launch()
{ {
if (Package == null) if (Package == null)
return; return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id); 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 NavToConsole()
{
if (Package == null)
return;
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);
}
}
}
// Detach handlers
if (sender is BasePackage basePackage)
{
basePackage.Exited -= BasePackageOnExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete;
}
if (Package?.Id != null)
{
runningPackageService.RunningPackages.Remove(Package.Id);
}
navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition()); IsRunning = false;
EventManager.Instance.OnPackageLaunchRequested(Package.Id); ShowWebUiButton = false;
})
.SafeFireAndForget();
} }
public async Task Uninstall() public async Task Uninstall()
@ -181,13 +321,17 @@ public partial class PackageCardViewModel : ProgressViewModel
return; return;
} }
var dialog = new ContentDialog var dialogViewModel = vmFactory.Get<ConfirmPackageDeleteDialogViewModel>(vm =>
{ {
Title = Resources.Label_ConfirmDelete, vm.ExpectedPackageName = Package?.DisplayName;
Content = Resources.Text_PackageUninstall_Details, });
PrimaryButtonText = Resources.Action_OK,
CloseButtonText = Resources.Action_Cancel, var dialog = new BetterContentDialog
DefaultButton = ContentDialogButton.Primary {
Content = dialogViewModel,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
@ -282,6 +426,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 +594,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 +782,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;
} }

17
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));
@ -119,7 +120,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (Design.IsDesignMode) if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet)
return; return;
installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer); installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer);

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();
}
}

60
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -14,6 +14,7 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -784,7 +785,10 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ [
new CommandItem(DebugFindLocalModelFromIndexCommand), new CommandItem(DebugFindLocalModelFromIndexCommand),
new CommandItem(DebugExtractDmgCommand), new CommandItem(DebugExtractDmgCommand),
new CommandItem(DebugShowNativeNotificationCommand) new CommandItem(DebugShowNativeNotificationCommand),
new CommandItem(DebugClearImageCacheCommand),
new CommandItem(DebugGCCollectCommand),
new CommandItem(DebugExtractImagePromptsToTxtCommand)
]; ];
[RelayCommand] [RelayCommand]
@ -899,6 +903,60 @@ public partial class MainSettingsViewModel : PageViewModelBase
); );
} }
[RelayCommand]
private void DebugClearImageCache()
{
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.ClearCache();
}
}
[RelayCommand]
private void DebugGCCollect()
{
GC.Collect();
}
[RelayCommand]
private async Task DebugExtractImagePromptsToTxt()
{
// Choose images
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = true });
if (files.Count == 0)
return;
var images = await Task.Run(
() => files.Select(f => LocalImageFile.FromPath(f.TryGetLocalPath()!)).ToList()
);
var successfulFiles = new List<LocalImageFile>();
foreach (var localImage in images)
{
var imageFile = new FilePath(localImage.AbsolutePath);
// Write a txt with the same name as the image
var txtFile = imageFile.WithName(imageFile.NameWithoutExtension + ".txt");
// Read metadata
if (localImage.GenerationParameters?.PositivePrompt is { } positivePrompt)
{
await File.WriteAllTextAsync(txtFile, positivePrompt);
successfulFiles.Add(localImage);
}
}
notificationService.Show(
"Extracted prompts",
$"Extracted prompts from {successfulFiles.Count}/{images.Count} images.",
NotificationType.Success
);
}
#endregion #endregion
#region Info Section #region Info Section

86
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml

@ -15,6 +15,8 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}" d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="800" d:DesignWidth="800"
@ -70,6 +72,7 @@
</UserControl.Styles> </UserControl.Styles>
<UserControl.Resources> <UserControl.Resources>
<system:Boolean x:Key="False">False</system:Boolean>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/> <converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}"> <DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border <Border
@ -100,28 +103,27 @@
CommandParameter="{Binding CivitModel}" CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}"> IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto"> <Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage Grid.Row="0" <controls1:AsyncImage Grid.Row="0"
CornerRadius="8" CornerRadius="8"
VerticalAlignment="Top" VerticalAlignment="Top"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Height="75" Height="75"
ZIndex="10" ZIndex="10"
IsVisible="{Binding ShowSantaHats}" IsVisible="{Binding ShowSantaHats, FallbackValue=False}"
Margin="0,8,0,0" Margin="0,8,0,0"
Source="avares://StabilityMatrix.Avalonia/Assets/santahat.png"> Source="avares://StabilityMatrix.Avalonia/Assets/santahat.png">
<!-- <controls:BetterAdvancedImage.RenderTransform> --> <!-- <controls:BetterAdvancedImage.RenderTransform> -->
<!-- <RotateTransform Angle="315"></RotateTransform> --> <!-- <RotateTransform Angle="315"></RotateTransform> -->
<!-- </controls:BetterAdvancedImage.RenderTransform> --> <!-- </controls:BetterAdvancedImage.RenderTransform> -->
</controls:BetterAdvancedImage> </controls1:AsyncImage>
<controls:BetterAdvancedImage <controls1:AsyncImage
Grid.Row="0" Grid.Row="0"
Grid.RowSpan="2" Grid.RowSpan="2"
CornerRadius="8" CornerRadius="8"
Width="330" Width="330"
Height="400" Height="400"
Source="{Binding CardImage}" Source="{Binding CardImage}"
Stretch="UniformToFill" Stretch="UniformToFill"/>
StretchDirection="Both"/>
<StackPanel <StackPanel
Grid.Row="0" Grid.Row="0"
@ -180,7 +182,7 @@
Classes="transparent" Classes="transparent"
Padding="10,4"> Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage <controls1:AsyncImage
Width="22" Width="22"
Height="22" Height="22"
Effect="{StaticResource ImageDropShadowEffect}" Effect="{StaticResource ImageDropShadowEffect}"
@ -319,11 +321,11 @@
Background="#66000000" Background="#66000000"
FontSize="16" FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}" Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" /> Value="{Binding CivitModel.ModelVersionStats.Rating}" />
<TextBlock <TextBlock
Margin="4,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}" Text="{Binding CivitModel.ModelVersionStats.RatingCount}"
TextAlignment="Center" /> TextAlignment="Center" />
</StackPanel> </StackPanel>
@ -336,7 +338,7 @@
<TextBlock <TextBlock
Margin="4,0" Margin="4,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" /> Text="{Binding CivitModel.Stats.ThumbsUpCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" /> <avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock <TextBlock
@ -402,6 +404,7 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes="accent" Classes="accent"
Command="{Binding SearchModelsCommand}" Command="{Binding SearchModelsCommand}"
CommandParameter="{StaticResource False}"
IsDefault="True"> IsDefault="True">
<Grid> <Grid>
<controls:ProgressRing <controls:ProgressRing
@ -472,57 +475,18 @@
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </ScrollViewer>
<TextBlock <TextBlock Grid.Row="2" Text="End of results"
Grid.Row="2" TextAlignment="Center"
Margin="8,8" Margin="0,0,0,8">
VerticalAlignment="Center" <TextBlock.IsVisible>
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" /> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="HasSearched"/>
<StackPanel Grid.Row="2" <Binding Path="NextPageCursor"
HorizontalAlignment="Center" Converter="{x:Static StringConverters.IsNullOrEmpty}"/>
IsVisible="{Binding HasSearched}" </MultiBinding>
Margin="0,8,0,8" </TextBlock.IsVisible>
Orientation="Horizontal"> </TextBlock>
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"

16
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs

@ -1,8 +1,11 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
@ -26,8 +29,15 @@ public partial class CivitAiBrowserPage : UserControlBase
if (sender is not ScrollViewer scrollViewer) if (sender is not ScrollViewer scrollViewer)
return; return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum; if (scrollViewer.Offset.Y == 0)
Debug.WriteLine($"IsAtEnd: {isAtEnd}"); return;
var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f;
if (isAtEnd && DataContext is IInfinitelyScroll scroll)
{
scroll.LoadNextPageAsync().SafeFireAndForget();
}
} }
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)

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="{Binding ShowWebUiTeachingTip}"/>
</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);
});
}
}

55
StabilityMatrix.Avalonia/Views/Dialogs/ConfirmPackageDeleteDialog.axaml

@ -0,0 +1,55 @@
<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:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="dialogs:ConfirmPackageDeleteDialogViewModel"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.ConfirmPackageDeleteDialog">
<Grid RowDefinitions="Auto, Auto, Auto, Auto, *"
Margin="8">
<TextBlock Text="{x:Static lang:Resources.Text_PackageUninstall_Details}"
FontWeight="Bold"
FontSize="20"
TextAlignment="Center"
TextWrapping="Wrap"/>
<TextBlock Grid.Row="1"
Margin="0,32,0,8"
TextAlignment="Center">
<Run Text="Please type"/>
<Run FontWeight="Bold" Text="{Binding ExpectedPackageName}"/>
<Run Text="to confirm the deletion of the package:"/>
</TextBlock>
<TextBox Grid.Row="2"
Text="{Binding PackageName, Mode=TwoWay}"
Margin="0,16,0,0"/>
<controls1:InfoBar Grid.Row="3"
Margin="0,16,0,0"
IsClosable="False"
IsOpen="True"
Title="{x:Static lang:Resources.Label_ActionCannotBeUndone}"
Severity="Warning"/>
<UniformGrid Grid.Row="4" HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Margin="0,32,0,0">
<Button Content="{x:Static lang:Resources.Action_Delete}"
Classes="danger"
IsEnabled="{Binding IsValid}"
HorizontalAlignment="Stretch"
Margin="0,0,4,0"
Command="{Binding OnPrimaryButtonClick}"
FontSize="16"/>
<Button Content="{x:Static lang:Resources.Action_Cancel}"
Command="{Binding OnCloseButtonClick}"
Margin="4,0,0,0"
HorizontalAlignment="Stretch"
FontSize="16"/>
</UniformGrid>
</Grid>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/Dialogs/ConfirmPackageDeleteDialog.axaml.cs

@ -0,0 +1,13 @@
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
[Transient]
public partial class ConfirmPackageDeleteDialog : UserControlBase
{
public ConfirmPackageDeleteDialog()
{
InitializeComponent();
}
}

39
StabilityMatrix.Avalonia/Views/Dialogs/NewOneClickInstallDialog.axaml

@ -62,6 +62,27 @@
VerticalAlignment="Top"> VerticalAlignment="Top">
<controls:Card.Styles> <controls:Card.Styles>
<Style Selector="controls|Card[Tag=ReallyRecommended]">
<Setter Property="Background"
Value="{DynamicResource ThemeGreenColorTransparent}" />
<Setter Property="BorderBrush"
Value="{DynamicResource ThemeGreenColorTransparent}" />
<Style
Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground"
Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="Recommended"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Recommended]"> <Style Selector="controls|Card[Tag=Recommended]">
<Setter Property="Background" <Setter Property="Background"
Value="{DynamicResource ThemeGreenColorTransparent}" /> Value="{DynamicResource ThemeGreenColorTransparent}" />
@ -117,6 +138,24 @@
</Template> </Template>
</Setter> </Setter>
</Style> </Style>
<Style Selector="controls|Card[Tag=InferenceCompatible]">
<Setter Property="Background"
Value="{DynamicResource ThemePurpleColor}" />
<Setter Property="BorderBrush"
Value="{DynamicResource ThemePurpleColor}" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="Inference Compatible"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card"> <Style Selector="controls|Card">
<Setter Property="Background" <Setter Property="Background"
Value="{DynamicResource ThemeDarkRedColor}" /> Value="{DynamicResource ThemeDarkRedColor}" />

4
StabilityMatrix.Avalonia/Views/Dialogs/RecommendedModelsDialog.axaml

@ -110,11 +110,11 @@
Background="#66000000" Background="#66000000"
FontSize="16" FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}" Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" /> Value="{Binding ModelVersion.Stats.Rating}" />
<TextBlock <TextBlock
Margin="4,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}" Text="{Binding ModelVersion.Stats.RatingCount}"
TextAlignment="Center" /> TextAlignment="Center" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

4
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -14,8 +14,8 @@
d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}" d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Icon="/Assets/Icon.ico" Icon="/Assets/Icon.ico"
Width="1100" Width="1300"
Height="750" Height="950"
BackRequested="TopLevel_OnBackRequested" BackRequested="TopLevel_OnBackRequested"
Title="Stability Matrix" Title="Stability Matrix"
DockProperties.IsDragEnabled="True" DockProperties.IsDragEnabled="True"

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)
{ {

165
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -2,15 +2,12 @@
x:Class="StabilityMatrix.Avalonia.Views.OutputsPage" x:Class="StabilityMatrix.Avalonia.Views.OutputsPage"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent" xmlns:fluentAvalonia="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage" xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll" xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard" xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
@ -23,13 +20,33 @@
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}"
MaxWidth="250"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{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 +56,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 +71,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"
@ -115,13 +82,6 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
<TextBlock
Grid.Row="0"
Grid.Column="3"
Margin="4"
IsVisible="{Binding !!NumItemsSelected}"
Text="{Binding NumImagesSelected}" />
<ui:CommandBar <ui:CommandBar
Grid.Row="1" Grid.Row="1"
Grid.Column="3" Grid.Column="3"
@ -129,27 +89,56 @@
VerticalAlignment="Center" VerticalAlignment="Center"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
DefaultLabelPosition="Right"> DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands> <ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton <ui:CommandBarElementContainer>
IsEnabled="{Binding !!NumItemsSelected}" <TextBlock Text="{Binding NumImagesSelected}"
IsVisible="{Binding NumItemsSelected}"
VerticalAlignment="Center"/>
</ui:CommandBarElementContainer>
<ui:CommandBarButton
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" IsEnabled="{Binding !IsLoading}"
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"
Classes.loading="{Binding IsLoading}"
Command="{Binding Refresh}" Command="{Binding Refresh}"
Label="{x:Static lang:Resources.Action_Refresh}" /> IsEnabled="{Binding !IsLoading}"
Label="{x:Static lang:Resources.Action_Refresh}">
<ui:CommandBarButton.Styles>
<Style Selector="ui|CommandBarButton:disabled.loading">
<Style Selector="^ ui|SymbolIcon">
<Style.Animations>
<Animation Duration="0:0:2" IterationCount="INFINITE">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle" Value="360"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Style>
</ui:CommandBarButton.Styles>
</ui:CommandBarButton>
<ui:CommandBarToggleButton
IconSource="List"
IsChecked="{Binding ShowFolders}"
Label="Show Folders"/>
</ui:CommandBar.PrimaryCommands> </ui:CommandBar.PrimaryCommands>
<ui:CommandBar.SecondaryCommands> <ui:CommandBar.SecondaryCommands>
@ -160,11 +149,22 @@
</ui:CommandBar.SecondaryCommands> </ui:CommandBar.SecondaryCommands>
</ui:CommandBar> </ui:CommandBar>
</Grid> </Grid>
<controls:ProgressRing Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="200"
Height="200"
IsIndeterminate="True"
IsVisible="{Binding IsChangingCategory}"/>
<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"
HorizontalAlignment="Center"
ItemsSource="{Binding Outputs}"> ItemsSource="{Binding Outputs}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="8" MinRowSpacing="8" /> <UniformGridLayout MinColumnSpacing="8" MinRowSpacing="8" />
@ -206,7 +206,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>();
}
} }

15
StabilityMatrix.Core/Extensions/NullableExtensions.cs

@ -1,7 +1,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Extensions;
@ -16,7 +16,12 @@ public static class NullableExtensions
[DebuggerStepThrough] [DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Unwrap<T>([NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null) [ContractAnnotation("obj:null => halt; obj:notnull => notnull")]
[return: System.Diagnostics.CodeAnalysis.NotNull]
public static T Unwrap<T>(
[System.Diagnostics.CodeAnalysis.NotNull] this T? obj,
[CallerArgumentExpression("obj")] string? paramName = null
)
where T : class where T : class
{ {
if (obj is null) if (obj is null)
@ -35,7 +40,11 @@ public static class NullableExtensions
[DebuggerStepThrough] [DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Unwrap<T>([NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null) [ContractAnnotation("obj:null => halt")]
public static T Unwrap<T>(
[System.Diagnostics.CodeAnalysis.NotNull] this T? obj,
[CallerArgumentExpression("obj")] string? paramName = null
)
where T : struct where T : struct
{ {
if (obj is null) if (obj is null)

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);

8
StabilityMatrix.Core/Helper/RemoteModels.cs

@ -167,8 +167,14 @@ public static class RemoteModels
) )
}; };
public static HybridModelFile ControlNetReferenceOnlyModel { get; } =
HybridModelFile.FromRemote("@ReferenceOnly");
public static IReadOnlyList<HybridModelFile> ControlNetModels { get; } = public static IReadOnlyList<HybridModelFile> ControlNetModels { get; } =
ControlNets.Select(HybridModelFile.FromDownloadable).ToImmutableArray(); ControlNets
.Select(HybridModelFile.FromDownloadable)
.Concat([ControlNetReferenceOnlyModel])
.ToImmutableArray();
private static IEnumerable<RemoteResource> PromptExpansions => private static IEnumerable<RemoteResource> PromptExpansions =>
[ [

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)

16
StabilityMatrix.Core/Models/Api/CivitBaseModelType.cs

@ -8,6 +8,9 @@ public enum CivitBaseModelType
{ {
All, All,
[StringValue("Pony")]
Pony,
[StringValue("SD 1.5")] [StringValue("SD 1.5")]
Sd15, Sd15,
@ -26,7 +29,20 @@ public enum CivitBaseModelType
[StringValue("SDXL 1.0 LCM")] [StringValue("SDXL 1.0 LCM")]
Sdxl10Lcm, Sdxl10Lcm,
[StringValue("SDXL Distilled")]
SdxlDistilled,
[StringValue("SDXL Lightning")]
SdxlLightning,
[StringValue("SDXL Turbo")] [StringValue("SDXL Turbo")]
SdxlTurbo, SdxlTurbo,
[StringValue("SVD")]
SVD,
[StringValue("Stable Cascade")]
StableCascade,
Other, Other,
} }

14
StabilityMatrix.Core/Models/Api/CivitMetadata.cs

@ -2,24 +2,26 @@
namespace StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Api;
public class CivitMetadata public class CivitMetadata
{ {
[JsonPropertyName("totalItems")] [JsonPropertyName("totalItems")]
public int TotalItems { get; set; } public int TotalItems { get; set; }
[JsonPropertyName("currentPage")] [JsonPropertyName("currentPage")]
public int CurrentPage { get; set; } public int CurrentPage { get; set; }
[JsonPropertyName("pageSize")] [JsonPropertyName("pageSize")]
public int PageSize { get; set; } public int PageSize { get; set; }
[JsonPropertyName("totalPages")] [JsonPropertyName("totalPages")]
public int TotalPages { get; set; } public int TotalPages { get; set; }
[JsonPropertyName("nextPage")] [JsonPropertyName("nextPage")]
public string? NextPage { get; set; } public string? NextPage { get; set; }
[JsonPropertyName("prevPage")] [JsonPropertyName("prevPage")]
public string? PrevPage { get; set; } public string? PrevPage { get; set; }
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; set; }
} }

7
StabilityMatrix.Core/Models/Api/CivitModel.cs

@ -48,9 +48,7 @@ public class CivitModel
var latestVersion = ModelVersions?.FirstOrDefault(); var latestVersion = ModelVersions?.FirstOrDefault();
if (latestVersion?.Files != null && latestVersion.Files.Any()) if (latestVersion?.Files != null && latestVersion.Files.Any())
{ {
var latestModelFile = latestVersion.Files.FirstOrDefault( var latestModelFile = latestVersion.Files.FirstOrDefault(x => x.Type == CivitFileType.Model);
x => x.Type == CivitFileType.Model
);
kbs = latestModelFile?.SizeKb ?? 0; kbs = latestModelFile?.SizeKb ?? 0;
} }
fullFilesSize = new FileSizeType(kbs); fullFilesSize = new FileSizeType(kbs);
@ -65,4 +63,7 @@ public class CivitModel
ModelVersions != null && ModelVersions.Any() ModelVersions != null && ModelVersions.Any()
? ModelVersions[0].BaseModel?.Replace("SD", "").Trim() ? ModelVersions[0].BaseModel?.Replace("SD", "").Trim()
: string.Empty; : string.Empty;
public CivitModelStats ModelVersionStats =>
ModelVersions != null && ModelVersions.Any() ? ModelVersions[0].Stats : new CivitModelStats();
} }

5
StabilityMatrix.Core/Models/Api/CivitModelStats.cs

@ -6,7 +6,10 @@ public class CivitModelStats : CivitStats
{ {
[JsonPropertyName("favoriteCount")] [JsonPropertyName("favoriteCount")]
public int FavoriteCount { get; set; } public int FavoriteCount { get; set; }
[JsonPropertyName("commentCount")] [JsonPropertyName("commentCount")]
public int CommentCount { get; set; } public int CommentCount { get; set; }
[JsonPropertyName("thumbsUpCount")]
public int ThumbsUpCount { get; set; }
} }

21
StabilityMatrix.Core/Models/Api/CivitModelVersion.cs

@ -6,31 +6,34 @@ public class CivitModelVersion
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public int Id { get; set; } public int Id { get; set; }
[JsonPropertyName("name")] [JsonPropertyName("name")]
public string Name { get; set; } public string Name { get; set; }
[JsonPropertyName("description")] [JsonPropertyName("description")]
public string Description { get; set; } public string Description { get; set; }
[JsonPropertyName("createdAt")] [JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
[JsonPropertyName("downloadUrl")] [JsonPropertyName("downloadUrl")]
public string DownloadUrl { get; set; } public string DownloadUrl { get; set; }
[JsonPropertyName("trainedWords")] [JsonPropertyName("trainedWords")]
public string[] TrainedWords { get; set; } public string[] TrainedWords { get; set; }
[JsonPropertyName("baseModel")] [JsonPropertyName("baseModel")]
public string? BaseModel { get; set; } public string? BaseModel { get; set; }
[JsonPropertyName("files")] [JsonPropertyName("files")]
public List<CivitFile>? Files { get; set; } public List<CivitFile>? Files { get; set; }
[JsonPropertyName("images")] [JsonPropertyName("images")]
public List<CivitImage>? Images { get; set; } public List<CivitImage>? Images { get; set; }
[JsonPropertyName("stats")] [JsonPropertyName("stats")]
public CivitModelStats Stats { get; set; } public CivitModelStats Stats { get; set; }
[JsonPropertyName("publishedAt")]
public DateTimeOffset? PublishedAt { get; set; }
} }

60
StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs

@ -2,7 +2,6 @@
namespace StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Api;
public class CivitModelsRequest public class CivitModelsRequest
{ {
/// <summary> /// <summary>
@ -10,99 +9,99 @@ public class CivitModelsRequest
/// </summary> /// </summary>
[AliasAs("limit")] [AliasAs("limit")]
public int? Limit { get; set; } public int? Limit { get; set; }
/// <summary> /// <summary>
/// The page from which to start fetching models /// The page from which to start fetching models
/// </summary> /// </summary>
[AliasAs("page")] [AliasAs("page")]
public int? Page { get; set; } public int? Page { get; set; }
/// <summary> /// <summary>
/// Search query to filter models by name /// Search query to filter models by name
/// </summary> /// </summary>
[AliasAs("query")] [AliasAs("query")]
public string? Query { get; set; } public string? Query { get; set; }
/// <summary> /// <summary>
/// Search query to filter models by tag /// Search query to filter models by tag
/// </summary> /// </summary>
[AliasAs("tag")] [AliasAs("tag")]
public string? Tag { get; set; } public string? Tag { get; set; }
/// <summary> /// <summary>
/// Search query to filter models by user /// Search query to filter models by user
/// </summary> /// </summary>
[AliasAs("username")] [AliasAs("username")]
public string? Username { get; set; } public string? Username { get; set; }
/// <summary> /// <summary>
/// The type of model you want to filter with. If none is specified, it will return all types /// The type of model you want to filter with. If none is specified, it will return all types
/// </summary> /// </summary>
[AliasAs("types")] [AliasAs("types")]
public CivitModelType[]? Types { get; set; } public CivitModelType[]? Types { get; set; }
/// <summary> /// <summary>
/// The order in which you wish to sort the results /// The order in which you wish to sort the results
/// </summary> /// </summary>
[AliasAs("sort")] [AliasAs("sort")]
public CivitSortMode? Sort { get; set; } public CivitSortMode? Sort { get; set; }
/// <summary> /// <summary>
/// The time frame in which the models will be sorted /// The time frame in which the models will be sorted
/// </summary> /// </summary>
[AliasAs("period")] [AliasAs("period")]
public CivitPeriod? Period { get; set; } public CivitPeriod? Period { get; set; }
/// <summary> /// <summary>
/// The rating you wish to filter the models with. If none is specified, it will return models with any rating /// The rating you wish to filter the models with. If none is specified, it will return models with any rating
/// </summary> /// </summary>
[AliasAs("rating")] [AliasAs("rating")]
public int? Rating { get; set; } public int? Rating { get; set; }
/// <summary> /// <summary>
/// Filter to models that require or don't require crediting the creator /// Filter to models that require or don't require crediting the creator
/// <remarks>Requires Authentication</remarks> /// <remarks>Requires Authentication</remarks>
/// </summary> /// </summary>
[AliasAs("favorites")] [AliasAs("favorites")]
public bool? Favorites { get; set; } public bool? Favorites { get; set; }
/// <summary> /// <summary>
/// Filter to hidden models of the authenticated user /// Filter to hidden models of the authenticated user
/// <remarks>Requires Authentication</remarks> /// <remarks>Requires Authentication</remarks>
/// </summary> /// </summary>
[AliasAs("hidden")] [AliasAs("hidden")]
public bool? Hidden { get; set; } public bool? Hidden { get; set; }
/// <summary> /// <summary>
/// Only include the primary file for each model (This will use your preferred format options if you use an API token or session cookie) /// Only include the primary file for each model (This will use your preferred format options if you use an API token or session cookie)
/// </summary> /// </summary>
[AliasAs("primaryFileOnly")] [AliasAs("primaryFileOnly")]
public bool? PrimaryFileOnly { get; set; } public bool? PrimaryFileOnly { get; set; }
/// <summary> /// <summary>
/// Filter to models that allow or don't allow creating derivatives /// Filter to models that allow or don't allow creating derivatives
/// </summary> /// </summary>
[AliasAs("allowDerivatives")] [AliasAs("allowDerivatives")]
public bool? AllowDerivatives { get; set; } public bool? AllowDerivatives { get; set; }
/// <summary> /// <summary>
/// Filter to models that allow or don't allow derivatives to have a different license /// Filter to models that allow or don't allow derivatives to have a different license
/// </summary> /// </summary>
[AliasAs("allowDifferentLicenses")] [AliasAs("allowDifferentLicenses")]
public bool? AllowDifferentLicenses { get; set; } public bool? AllowDifferentLicenses { get; set; }
/// <summary> /// <summary>
/// Filter to models based on their commercial permissions /// Filter to models based on their commercial permissions
/// </summary> /// </summary>
[AliasAs("allowCommercialUse")] [AliasAs("allowCommercialUse")]
public CivitCommercialUse? AllowCommercialUse { get; set; } public CivitCommercialUse? AllowCommercialUse { get; set; }
/// <summary> /// <summary>
/// If false, will return safer images and hide models that don't have safe images /// If false, will return safer images and hide models that don't have safe images
/// </summary> /// </summary>
[AliasAs("nsfw")] [AliasAs("nsfw")]
public string? Nsfw { get; set; } public string? Nsfw { get; set; }
/// <summary> /// <summary>
/// options: <br/> /// options: <br/>
/// SD 1.4 <br/> /// SD 1.4 <br/>
@ -117,22 +116,25 @@ public class CivitModelsRequest
/// </summary> /// </summary>
[AliasAs("baseModels")] [AliasAs("baseModels")]
public string? BaseModel { get; set; } public string? BaseModel { get; set; }
[AliasAs("ids")] [AliasAs("ids")]
public string CommaSeparatedModelIds { get; set; } public string CommaSeparatedModelIds { get; set; }
[AliasAs("cursor")]
public string? Cursor { get; set; }
public override string ToString() public override string ToString()
{ {
return $"Page: {Page}, " + return $"Page: {Page}, "
$"Query: {Query}, " + + $"Query: {Query}, "
$"Tag: {Tag}, " + + $"Tag: {Tag}, "
$"Username: {Username}, " + + $"Username: {Username}, "
$"Types: {Types}, " + + $"Types: {Types}, "
$"Sort: {Sort}, " + + $"Sort: {Sort}, "
$"Period: {Period}, " + + $"Period: {Period}, "
$"Rating: {Rating}, " + + $"Rating: {Rating}, "
$"Nsfw: {Nsfw}, " + + $"Nsfw: {Nsfw}, "
$"BaseModel: {BaseModel}, " + + $"BaseModel: {BaseModel}, "
$"CommaSeparatedModelIds: {CommaSeparatedModelIds}"; + $"CommaSeparatedModelIds: {CommaSeparatedModelIds}";
} }
} }

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;
}
} }

9
StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs

@ -5,6 +5,15 @@
/// </summary> /// </summary>
public record ModelConnections(string Name) public record ModelConnections(string Name)
{ {
public ModelConnections(ModelConnections other)
{
Name = other.Name;
Model = other.Model;
VAE = other.VAE;
Clip = other.Clip;
Conditioning = other.Conditioning;
}
public ModelNodeConnection? Model { get; set; } public ModelNodeConnection? Model { get; set; }
public VAENodeConnection? VAE { get; set; } public VAENodeConnection? VAE { get; set; }

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

@ -7,6 +7,7 @@ using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes;
@ -131,23 +132,35 @@ public class ComfyNodeBuilder
public int StopAtClipLayer { get; init; } = -1; public int StopAtClipLayer { get; init; } = -1;
} }
public static NamedComfyNode<LatentNodeConnection> LatentFromBatch( public record LatentFromBatch : ComfyTypedNodeBase<LatentNodeConnection>
string name,
LatentNodeConnection samples,
int batchIndex,
int length
)
{ {
return new NamedComfyNode<LatentNodeConnection>(name) public required LatentNodeConnection Samples { get; init; }
{
ClassType = "LatentFromBatch", [Range(0, 63)]
Inputs = new Dictionary<string, object?> public int BatchIndex { get; init; } = 0;
{
["samples"] = samples.Data, [Range(1, 64)]
["batch_index"] = batchIndex, public int Length { get; init; } = 1;
["length"] = length, }
}
}; public record LatentBlend : ComfyTypedNodeBase<LatentNodeConnection>
{
public required LatentNodeConnection Samples1 { get; init; }
public required LatentNodeConnection Samples2 { get; init; }
[Range(0d, 1d)]
public double BlendFactor { get; init; } = 0.5;
}
public record ModelMergeSimple : ComfyTypedNodeBase<ModelNodeConnection>
{
public required ModelNodeConnection Model1 { get; init; }
public required ModelNodeConnection Model2 { get; init; }
[Range(0d, 1d)]
public double Ratio { get; init; } = 1;
} }
public static NamedComfyNode<ImageNodeConnection> ImageUpscaleWithModel( public static NamedComfyNode<ImageNodeConnection> ImageUpscaleWithModel(
@ -332,7 +345,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 +355,34 @@ 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;
}
[TypedNodeOptions(
Name = "Inference_Core_ReferenceOnlySimple",
RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.3.0"]
)]
public record ReferenceOnlySimple : ComfyTypedNodeBase<ModelNodeConnection, LatentNodeConnection>
{
public required ModelNodeConnection Model { get; init; }
public required LatentNodeConnection Reference { get; init; }
[Range(1, 64)]
public int BatchSize { get; init; } = 1;
}
public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae) public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae)
{ {
var name = GetUniqueName("VAEDecode"); var name = GetUniqueName("VAEDecode");
@ -818,6 +859,14 @@ public class ComfyNodeBuilder
public ModelConnections Base => Models["Base"]; public ModelConnections Base => Models["Base"];
public ModelConnections Refiner => Models["Refiner"]; public ModelConnections Refiner => Models["Refiner"];
public Dictionary<string, ModuleApplyStepTemporaryArgs?> SamplerTemporaryArgs { get; } = new();
public ModuleApplyStepTemporaryArgs? BaseSamplerTemporaryArgs
{
get => SamplerTemporaryArgs.GetValueOrDefault("Base");
set => SamplerTemporaryArgs["Base"] = value;
}
public PrimaryNodeConnection? Primary { get; set; } public PrimaryNodeConnection? Primary { get; set; }
public VAENodeConnection? PrimaryVAE { get; set; } public VAENodeConnection? PrimaryVAE { get; set; }
public Size PrimarySize { get; set; } public Size PrimarySize { get; set; }

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]}";
} }

8
StabilityMatrix.Core/Models/HybridModelFile.cs

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Models;
@ -20,7 +21,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; }
@ -67,6 +68,11 @@ public record HybridModelFile
return "Default"; return "Default";
} }
if (ReferenceEquals(this, RemoteModels.ControlNetReferenceOnlyModel))
{
return "Reference Only";
}
var fileName = Path.GetFileNameWithoutExtension(RelativePath); var fileName = Path.GetFileNameWithoutExtension(RelativePath);
if ( if (

46
StabilityMatrix.Core/Models/Inference/ModuleApplyStepTemporaryArgs.cs

@ -0,0 +1,46 @@
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Core.Models.Inference;
public class ModuleApplyStepTemporaryArgs
{
/// <summary>
/// Temporary Primary apply step, used by ControlNet ReferenceOnly which changes the latent.
/// </summary>
public PrimaryNodeConnection? Primary { get; set; }
public VAENodeConnection? PrimaryVAE { get; set; }
/// <summary>
/// Used by Reference-Only ControlNet to indicate that <see cref="Primary"/> has been batched.
/// </summary>
public bool IsPrimaryTempBatched { get; set; }
/// <summary>
/// When <see cref="IsPrimaryTempBatched"/> is true, this is the index of the temp batch to pick after sampling.
/// </summary>
public int PrimaryTempBatchPickIndex { get; set; }
public Dictionary<string, ModelConnections> Models { get; set; } =
new() { ["Base"] = new ModelConnections("Base"), ["Refiner"] = new ModelConnections("Refiner") };
public ModelConnections Base => Models["Base"];
public ModelConnections Refiner => Models["Refiner"];
public ConditioningConnections GetRefinerOrBaseConditioning()
{
return Refiner.Conditioning
?? Base.Conditioning
?? throw new NullReferenceException("No Refiner or Base Conditioning");
}
public ModelNodeConnection GetRefinerOrBaseModel()
{
return Refiner.Model ?? Base.Model ?? throw new NullReferenceException("No Refiner or Base Model");
}
public VAENodeConnection GetDefaultVAE()
{
return PrimaryVAE ?? Refiner.VAE ?? Base.VAE ?? throw new NullReferenceException("No VAE");
}
}

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; } = [];
}

12
StabilityMatrix.Core/Models/PackageDifficulty.cs

@ -2,11 +2,13 @@
public enum PackageDifficulty public enum PackageDifficulty
{ {
ReallyRecommended = -1,
Recommended = 0, Recommended = 0,
Simple = 1, InferenceCompatible = 1,
Advanced = 2, Simple = 2,
Expert = 3, Advanced = 3,
Nightmare = 4, Expert = 4,
UltraNightmare = 5, Nightmare = 5,
UltraNightmare = 6,
Impossible = 999 Impossible = 999
} }

2
StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs

@ -32,7 +32,7 @@ public interface IPackageModificationRunner
string? ModificationCompleteTitle { get; init; } string? ModificationCompleteTitle { get; init; }
string? ModificationCompleteMessage { get; init; } string ModificationCompleteMessage { get; init; }
string? ModificationFailedTitle { get; init; } string? ModificationFailedTitle { get; init; }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save