Browse Source

Merge pull request #594 from ionite34/v2.10

v2.10.0
pull/629/head
JT 7 months ago committed by GitHub
parent
commit
56acf60ead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.Gif/Avalonia.Gif.csproj
  2. 104
      CHANGELOG.md
  3. 47
      README.md
  4. 6
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  5. 7
      StabilityMatrix.Avalonia/App.axaml
  6. 25
      StabilityMatrix.Avalonia/App.axaml.cs
  7. 14
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  8. 208
      StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs
  9. 80
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  10. 7
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  11. 9
      StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml
  12. 48
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml
  13. 7
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs
  14. 30
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml
  15. 2
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs
  16. 6
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  17. 8
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  18. 20
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs
  19. 42
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs
  20. 135
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs
  21. 248
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs
  22. 565
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs
  23. 18
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs
  24. 36
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs
  25. 76
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs
  26. 156
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs
  27. 49
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs
  28. 21
      StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE
  29. 48
      StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml
  30. 116
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  31. 3
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  32. 93
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  33. 9
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  34. 2
      StabilityMatrix.Avalonia/Helpers/UriHandler.cs
  35. 153
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  36. 62
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  37. 50
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  38. 51
      StabilityMatrix.Avalonia/Languages/Resources.resx
  39. 60
      StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx
  40. 3
      StabilityMatrix.Avalonia/Models/ImageSource.cs
  41. 51
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  42. 10
      StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs
  43. 22
      StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs
  44. 5
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  45. 21
      StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs
  46. 1
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  47. 8
      StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs
  48. 2
      StabilityMatrix.Avalonia/Services/INavigationService.cs
  49. 20
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  50. 251
      StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs
  51. 2
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  52. 153
      StabilityMatrix.Avalonia/Services/RunningPackageService.cs
  53. 40
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  54. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings
  55. 73
      StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml
  56. 403
      StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml
  57. 30
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  58. 1
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  59. 119
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  60. 16
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  61. 3
      StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
  62. 32
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  63. 42
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  64. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  65. 1
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  66. 2
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  67. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  68. 15
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
  69. 21
      StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs
  70. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  71. 185
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs
  72. 4
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelItemViewModel.cs
  73. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs
  74. 6
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  75. 9
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  76. 84
      StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs
  77. 36
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs
  78. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  79. 15
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
  80. 19
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  81. 100
      StabilityMatrix.Avalonia/ViewModels/Inference/LayerDiffuseCardViewModel.cs
  82. 119
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  83. 57
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
  84. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LayerDiffuseModule.cs
  85. 30
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
  86. 75
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  87. 144
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  88. 173
      StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs
  89. 33
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  90. 3
      StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
  91. 7
      StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs
  92. 348
      StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
  93. 188
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  94. 277
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  95. 20
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs
  96. 17
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  97. 15
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  98. 169
      StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs
  99. 74
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  100. 39
      StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
Avalonia.Gif/Avalonia.Gif.csproj

@ -10,7 +10,7 @@
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>

104
CHANGELOG.md

@ -5,6 +5,106 @@ 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/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.10.0
### 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.
- 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.
- Added Layer Diffuse sampler addon to Inference, allows generating foreground with transparency with SD1.5 and SDXL.
- Added support for deep links from the new Stability Matrix Chrome extension
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added more metadata to the image dialog info flyout
- Added Output Sharing toggle in Advanced Options during install flow
### 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
- Model download location selector now searches all subfolders
- 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
- Updated translations for French, Spanish, and Turkish
- Changed to a new image control for pages with many images
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Fixed [#559](https://github.com/LykosAI/StabilityMatrix/issues/559) - "Unable to load bitmap from provided data" error in Checkpoints page
- Fixed [#522](https://github.com/LykosAI/StabilityMatrix/issues/522) - Incorrect output directory path for latest Auto1111
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed Civitai model browser error when sorting by Installed with more than 100 installed models
- Fixed CLIP Install errors due to setuptools distutils conflict, added default environment variable setting `SETUPTOOLS_USE_DISTUTILS=stdlib`
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- 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
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.10.0-pre.2
### Added
- Added more metadata to the image dialog info flyout
- Added Restart button to console page
### Changed
- Model download location selector now searches all subfolders
### Fixed
- Fixed Civitai model browser not showing images when "Show NSFW" is disabled
- Fixed crash when Installed Workflows page is opened with no Workflows folder
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
## v2.10.0-pre.1
### Added
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added Output Sharing toggle in Advanced Options during install flow
### Changed
- Changed to a new image control for pages with many images
- Removed Symlink option for InvokeAI due to changes with InvokeAI v4.0+
- Output sharing is now enabled by default for new installations
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed "Could not find entry point for InvokeAI" error on InvokeAI v4.0+
## 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.3
### Changed
- Removed Symlink option for InvokeAI to prevent InvokeAI from moving models into its own directories (will be replaced with a Config option in a future update)
@ -22,9 +122,6 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- 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 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.9.1
### Added
@ -82,6 +179,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed output sharing for Stable Diffusion WebUI Forge
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed default command line args for SDWebUI Forge on macOS
- Fixed output paths and output sharing for SDWebUI Forge

47
README.md

@ -2,6 +2,7 @@
[![Build](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml/badge.svg)](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml)
[![Discord Server](https://img.shields.io/discord/1115555685476868168?logo=discord&logoColor=white&label=Discord%20Server)](https://discord.com/invite/TUrgfECxHz)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[release]: https://github.com/LykosAI/StabilityMatrix/releases/latest
[download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip
@ -9,7 +10,7 @@
[download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg
[auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui
[auto1111-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml
[sdwebui-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml
[webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux
[comfy]: https://github.com/comfyanonymous/ComfyUI
[sdnext]: https://github.com/vladmandic/automatic
@ -27,9 +28,13 @@
[civitai]: https://civitai.com/
[huggingface]: https://huggingface.co/
Multi-Platform Package Manager and Inference UI for Stable Diffusion
![Header image for Stability Matrix, Multi-Platform Package Manager and Inference UI for Stable Diffusion](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
### ✨ New in 2.5 - [Inference](#inference-A-reimagined-built-in-Stable-Diffusion-experience), a built-in interface for Stable Diffusion powered by ComfyUI
[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
Multi-Platform Package Manager and Inference UI for Stable Diffusion
### 🖱 One click install and update for Stable Diffusion Web UI Packages
- Supports:
@ -41,31 +46,12 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion
- [InvokeAI][invokeai]
- [Kohya's GUI][kohya-ss]
- [OneTrainer][onetrainer]
- Manage plugins / extensions for supported packages ([Automatic 1111][auto1111], [Comfy UI][comfy])
- Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext])
- Easily install or update Python dependencies for each package
- Embedded Git and Python dependencies, with no need for either to be globally installed
- Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
![header](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
### Inference - A reimagined built-in Stable Diffusion experience
### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix
- Powerful auto-completion and syntax highlighting using a formal language grammar
- Workspaces open in tabs that save and load from `.smproj` project files
@ -79,13 +65,20 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/4341cc34-a584-4e9c-bb3b-276009bdae80" alt=""/>
</p>
### Searchable launch options
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
<p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/75456866-9d95-47c6-8c0a-fdc19443ee02" alt=""/>
</p>
### Model browser powered by [Civit AI][civitai]
- Downloads new models, automatically uses the appropriate shared model directory
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
- Pause and resume downloads, even after closing the app
<p align="center">

6
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,12 +19,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="NLog" Version="5.2.8" />
</ItemGroup>

7
StabilityMatrix.Avalonia/App.axaml

@ -4,7 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
xmlns:controls="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
Name="Stability Matrix"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -29,6 +29,7 @@
<ResourceInclude Source="Styles/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/FAComboBoxStyles.axaml"/>
<ResourceInclude Source="Controls/Inference/ImageFolderCard.axaml"/>
<ResourceInclude Source="Controls/VendorLabs/Themes/BetterAsyncImage.axaml"/>
</ResourceDictionary.MergedDictionaries>
<idcr:ControlRecycling x:Key="ControlRecyclingKey" />
@ -55,6 +56,7 @@
<StyleInclude Source="Styles/DockStyles.axaml"/>
<StyleInclude Source="Styles/BorderStyles.axaml"/>
<StyleInclude Source="Styles/TextBoxStyles.axaml"/>
<StyleInclude Source="Styles/CommandBarButtonStyles.axaml"/>
<StyleInclude Source="Controls/AdvancedImageBox.axaml"/>
<StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
@ -80,7 +82,8 @@
<StyleInclude Source="Controls/Inference/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/>
<StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/>
<controls:ControlThemes/>
<StyleInclude Source="Controls/Inference/LayerDiffuseCard.axaml"/>
<labs:ControlThemes/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

25
StabilityMatrix.Avalonia/App.axaml.cs

@ -40,7 +40,6 @@ using Polly.Extensions.Http;
using Polly.Timeout;
using Refit;
using Sentry;
using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@ -54,14 +53,12 @@ using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
@ -323,17 +320,18 @@ public sealed class App : Application
provider.GetRequiredService<IDiscordRichPresenceService>(),
provider.GetRequiredService<ServiceManager<ViewModelBase>>(),
provider.GetRequiredService<ITrackedDownloadService>(),
provider.GetRequiredService<IModelIndexService>()
provider.GetRequiredService<IModelIndexService>(),
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>()
)
{
Pages =
{
provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<WorkflowsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -556,7 +554,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15);
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
@ -565,7 +563,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15);
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
@ -583,6 +581,15 @@ public sealed class App : Application
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
);
services
.AddRefitClient<IOpenArtApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://openart.ai/api/public/workflows");
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
// Add Refit client managers
services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));

14
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -68,6 +68,20 @@ def _patch_rich_console():
pass
except Exception as e:
print("[sitecustomize error]:", e)
try:
from pip._vendor.rich import console
class _Console(console.Console):
@property
def is_terminal(self) -> bool:
return True
console.Console = _Console
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_rich_console()

208
StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs

@ -0,0 +1,208 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactions.DragAndDrop;
using Avalonia.Xaml.Interactivity;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterContextDragBehavior : Behavior<Control>
{
private Point _dragStartPoint;
private PointerEventArgs? _triggerEvent;
private bool _lock;
private bool _captured;
public static readonly StyledProperty<object?> ContextProperty = AvaloniaProperty.Register<
ContextDragBehavior,
object?
>(nameof(Context));
public static readonly StyledProperty<IDragHandler?> HandlerProperty = AvaloniaProperty.Register<
ContextDragBehavior,
IDragHandler?
>(nameof(Handler));
public static readonly StyledProperty<double> HorizontalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(HorizontalDragThreshold), 3);
public static readonly StyledProperty<double> VerticalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(VerticalDragThreshold), 3);
public static readonly StyledProperty<string> DataFormatProperty = AvaloniaProperty.Register<
BetterContextDragBehavior,
string
>("DataFormat");
public string DataFormat
{
get => GetValue(DataFormatProperty);
set => SetValue(DataFormatProperty, value);
}
public object? Context
{
get => GetValue(ContextProperty);
set => SetValue(ContextProperty, value);
}
public IDragHandler? Handler
{
get => GetValue(HandlerProperty);
set => SetValue(HandlerProperty, value);
}
public double HorizontalDragThreshold
{
get => GetValue(HorizontalDragThresholdProperty);
set => SetValue(HorizontalDragThresholdProperty, value);
}
public double VerticalDragThreshold
{
get => GetValue(VerticalDragThresholdProperty);
set => SetValue(VerticalDragThresholdProperty, value);
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
AssociatedObject?.AddHandler(
InputElement.PointerPressedEvent,
AssociatedObject_PointerPressed,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerReleasedEvent,
AssociatedObject_PointerReleased,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerMovedEvent,
AssociatedObject_PointerMoved,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerCaptureLostEvent,
AssociatedObject_CaptureLost,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed);
AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased);
AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved);
AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost);
}
private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value)
{
var data = new DataObject();
data.Set(DataFormat, value!);
var effect = DragDropEffects.None;
if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt))
{
effect |= DragDropEffects.Link;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
effect |= DragDropEffects.Move;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control))
{
effect |= DragDropEffects.Copy;
}
else
{
effect |= DragDropEffects.Move;
}
await DragDrop.DoDragDrop(triggerEvent, data, effect);
}
private void Released()
{
_triggerEvent = null;
_lock = false;
}
private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (properties.IsLeftButtonPressed)
{
if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext)
{
_dragStartPoint = e.GetPosition(null);
_triggerEvent = e;
_lock = true;
_captured = true;
}
}
}
private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_captured)
{
if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { })
{
Released();
}
_captured = false;
}
}
private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { })
{
var point = e.GetPosition(null);
var diff = _dragStartPoint - point;
var horizontalDragThreshold = HorizontalDragThreshold;
var verticalDragThreshold = VerticalDragThreshold;
if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold)
{
if (_lock)
{
_lock = false;
}
else
{
return;
}
var context = Context ?? AssociatedObject?.DataContext;
Handler?.BeforeDragDrop(sender, _triggerEvent, context);
await DoDragDrop(_triggerEvent, context);
Handler?.AfterDragDrop(sender, _triggerEvent, context);
_triggerEvent = null;
}
}
}
private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
Released();
_captured = false;
}
}

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

@ -36,29 +36,46 @@
<sg:SpacedGrid
ColumnDefinitions="0.34*,0.65*"
ColumnSpacing="8"
RowDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto"
RowSpacing="4">
<!-- Image Select -->
<controls:SelectImageCard
Grid.RowSpan="2"
Grid.RowSpan="3"
Padding="6"
VerticalAlignment="Stretch"
DataContext="{Binding SelectImageCardViewModel}"
FontSize="13" />
<!-- TODO: Preprocessor Model -->
<ui:FAComboBox
<!-- Preprocessor Model -->
<sg:SpacedGrid
Grid.Row="0"
Grid.Column="1"
Margin="0,0,0,4"
HorizontalAlignment="Stretch"
markupExtensions:ShowDisabledTooltipExtension.ShowOnDisabled="True"
Header="{x:Static lang:Resources.Label_Preprocessor}"
IsEnabled="False"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
ToolTip.ShowDelay="1200"
ToolTip.Tip="{StaticResource WipFeatureToolTip}" />
ColumnDefinitions="*,Auto">
<ui:FAComboBox
Margin="0,0,0,4"
SelectedItem="{Binding SelectedPreprocessor}"
ItemsSource="{Binding ClientManager.Preprocessors}"
DisplayMemberBinding="{Binding DisplayName}"
HorizontalAlignment="Stretch"
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 -->
<ui:FAComboBox
@ -101,6 +118,45 @@
</ui:FAComboBox.DataTemplates>
</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

7
StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml

@ -9,6 +9,8 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith>
@ -195,12 +197,11 @@
<Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding AbsolutePath}"
Stretch="UniformToFill"
StretchDirection="Both" />
Stretch="UniformToFill" />
<Border
Grid.Row="1"

9
StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml

@ -5,6 +5,8 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
x:DataType="vmInference:ImageGalleryCardViewModel">
<Design.PreviewWith>
@ -115,12 +117,11 @@
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ImageSource}">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
MaxHeight="65"
CornerRadius="4"
Source="{Binding}"
Stretch="Uniform"
StretchDirection="Both" />
Source="{Binding Uri}"
Stretch="Uniform" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

48
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml

@ -0,0 +1,48 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
x:DataType="vmInference:LayerDiffuseCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:LayerDiffuseCard DataContext="{x:Static mocks:DesignData.LayerDiffuseCardViewModel}"/>
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|LayerDiffuseCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Mode Selection -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Mode"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding Converter={x:Static converters:EnumAttributeConverters.DisplayName}}"
ItemsSource="{Binding AvailableModes}"
SelectedItem="{Binding SelectedMode}" />
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs

@ -0,0 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class LayerDiffuseCard : TemplatedControl;

30
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml

@ -8,6 +8,8 @@
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
x:DataType="vmInference:PromptCardViewModel">
<Design.PreviewWith>
<Grid Width="460" Height="600">
@ -56,21 +58,29 @@
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
VerticalContentAlignment="Top"
icons:Attached.Icon="fa-solid fa-question"
Classes="transparent-full"
Command="{Binding ShowHelpDialogCommand}" />
<Panel>
<Button
x:Name="PART_HelpButton"
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
VerticalContentAlignment="Top"
icons:Attached.Icon="fa-solid fa-question"
Classes="transparent-full"
Command="{Binding ShowHelpDialogCommand}" />
<ui:TeachingTip
Target="{Binding #PART_HelpButton}"
Title="{x:Static lang:Resources.TeachingTip_InferencePromptHelpButton}"
IsOpen="{Binding IsHelpButtonTeachingTipOpen}"/>
</Panel>
<Button
Padding="8,4"
Command="{Binding DebugShowTokensCommand}"
Content="Show Tokens"
IsVisible="{Binding SharedState.IsDebugMode}" />
</StackPanel>
<Border

2
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs

@ -12,7 +12,7 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptCard : TemplatedControl
public class PromptCard : TemplatedControlBase
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

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

@ -1,7 +1,9 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
@ -38,7 +40,7 @@
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"

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

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

20
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
public class AsyncImageFailedEventArgs : RoutedEventArgs
{
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "")
: base(FailedEvent)
{
ErrorException = errorException;
ErrorMessage = errorMessage;
}
public Exception? ErrorException { get; private set; }
public string ErrorMessage { get; private set; }
}
}

42
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs

@ -0,0 +1,42 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Deines the <see cref="Opened"/> event
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register<
BetterAsyncImage,
RoutedEventArgs
>(nameof(Opened), RoutingStrategies.Bubble);
/// <summary>
/// Deines the <see cref="Failed"/> event
/// </summary>
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent =
RoutedEvent.Register<
BetterAsyncImage,
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs
>(nameof(Failed), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the image is successfully loaded.
/// </summary>
public event EventHandler<RoutedEventArgs>? Opened
{
add => AddHandler(OpenedEvent, value);
remove => RemoveHandler(OpenedEvent, value);
}
/// <summary>
/// Occurs when the image fails to load the uri provided.
/// </summary>
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed
{
add => AddHandler(FailedEvent, value);
remove => RemoveHandler(FailedEvent, value);
}
}

135
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs

@ -0,0 +1,135 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Labs.Controls;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Defines the <see cref="PlaceholderSource"/> property.
/// </summary>
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
IImage?
>(nameof(PlaceholderSource));
/// <summary>
/// Defines the <see cref="Source"/> property.
/// </summary>
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Uri?
>(nameof(Source));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(Stretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="PlaceholderStretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(PlaceholderStretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="State"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>(
nameof(State),
o => o.State,
(o, v) => o.State = v
);
/// <summary>
/// Defines the <see cref="ImageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty =
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>(
nameof(ImageTransition),
new CrossFade(TimeSpan.FromSeconds(0.25))
);
/// <summary>
/// Defines the <see cref="IsCacheEnabled"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>(
nameof(IsCacheEnabled),
o => o.IsCacheEnabled,
(o, v) => o.IsCacheEnabled = v
);
private bool _isCacheEnabled;
/// <summary>
/// Gets or sets the placeholder image.
/// </summary>
public IImage? PlaceholderSource
{
get => GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
/// <summary>
/// Gets or sets the uri pointing to the image resource
/// </summary>
public Uri? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets a value controlling how the image will be stretched.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets or sets a value controlling how the placeholder will be stretched.
/// </summary>
public Stretch PlaceholderStretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets the current loading state of the image.
/// </summary>
public AsyncImageState State
{
get => _state;
private set => SetAndRaise(StateProperty, ref _state, value);
}
/// <summary>
/// Gets or sets the transition to run when the image is loaded.
/// </summary>
public IPageTransition? ImageTransition
{
get => GetValue(ImageTransitionProperty);
set => SetValue(ImageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether to use cache for retrieved images
/// </summary>
public bool IsCacheEnabled
{
get => _isCacheEnabled;
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value);
}
}

248
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs

@ -0,0 +1,248 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Labs.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
/// <summary>
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>.
/// </summary>
[TemplatePart("PART_Image", typeof(Image))]
[TemplatePart("PART_PlaceholderImage", typeof(Image))]
public partial class BetterAsyncImage : TemplatedControl
{
protected Image? ImagePart { get; private set; }
protected Image? PlaceholderPart { get; private set; }
private bool _isInitialized;
private CancellationTokenSource? _tokenSource;
private AsyncImageState _state;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ImagePart = e.NameScope.Get<Image>("PART_Image");
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage");
_tokenSource = new CancellationTokenSource();
_isInitialized = true;
if (Source != null)
{
SetSource(Source);
}
}
private async void SetSource(object? source)
{
if (!_isInitialized)
{
return;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
AttachSource(null);
if (source == null)
{
return;
}
State = AsyncImageState.Loading;
if (Source is IImage image)
{
AttachSource(image);
return;
}
if (Source == null)
{
return;
}
var uri = Source;
if (uri != null && uri.IsAbsoluteUri)
{
if (uri.Scheme == "http" || uri.Scheme == "https")
{
Bitmap? bitmap = null;
// Android doesn't allow network requests on the main thread, even though we are using async apis.
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroid())
{
await Task.Run(async () =>
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
});
}
else
#endif
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
}
AttachSource(bitmap);
}
else if (uri.Scheme == "avares")
{
try
{
AttachSource(new Bitmap(AssetLoader.Open(uri)));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath))
{
// Added error handling here for local files
try
{
AttachSource(new Bitmap(uri.LocalPath));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Uri has unsupported scheme. Uri:{source}")
)
);
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Relative paths aren't supported. Uri:{source}")
)
);
}
}
private void AttachSource(IImage? image)
{
if (ImagePart != null)
{
ImagePart.Source = image;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
if (image == null)
{
State = AsyncImageState.Unloaded;
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token);
}
else if (image.Size != default)
{
State = AsyncImageState.Loaded;
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token);
RaiseEvent(new RoutedEventArgs(OpenedEvent));
}
}
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token)
{
if (await ProvideCachedResourceAsync(url, token) is { } bitmap)
{
return bitmap;
}
#if NET6_0_OR_GREATER
using var client = new HttpClient();
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false);
await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false);
#elif NETSTANDARD2_0
using var client = new HttpClient();
var response = await client.GetAsync(url, token).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
#endif
memoryStream.Position = 0;
return new Bitmap(memoryStream);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SourceProperty)
{
SetSource(Source);
}
}
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token)
{
if (IsCacheEnabled && imageUri != null)
{
return await ImageCache
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token)
.ConfigureAwait(false);
}
return null;
}
}

565
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs

@ -0,0 +1,565 @@
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
internal abstract class CacheBase<T>
{
private class ConcurrentRequest
{
public Task<T?>? Task { get; set; }
public bool EnsureCachedCopy { get; set; }
}
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1);
private string? _baseFolder = null;
private string? _cacheFolderName = null;
private string? _cacheFolder = null;
private InMemoryStorage<T>? _inMemoryFileStorage = null;
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks =
new ConcurrentDictionary<string, ConcurrentRequest>();
private HttpClient? _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
protected CacheBase()
{
var options = CacheOptions.Default;
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1);
_baseFolder = options?.BaseCachePath ?? null;
_inMemoryFileStorage = new InMemoryStorage<T>();
RetryCount = 1;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
/// <summary>
/// Gets or sets max in-memory item storage count
/// </summary>
public int MaxMemoryCacheCount
{
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; }
set
{
if (_inMemoryFileStorage != null)
_inMemoryFileStorage.MaxItemCount = value;
}
}
/// <summary>
/// Gets instance of <see cref="HttpClient"/>
/// </summary>
protected HttpClient HttpClient
{
get
{
if (_httpClient == null)
{
var messageHandler = new HttpClientHandler();
_httpClient = new HttpClient(messageHandler);
}
return _httpClient;
}
}
/// <summary>
/// Initializes FileCache and provides root folder and cache folder name
/// </summary>
/// <param name="folder">Folder that is used as root for cache</param>
/// <param name="folderName">Cache folder name</param>
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param>
/// <returns>awaitable task</returns>
public virtual async Task InitializeAsync(
string? folder = null,
string? folderName = null,
HttpMessageHandler? httpMessageHandler = null
)
{
_baseFolder = folder;
_cacheFolderName = folderName;
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false);
if (httpMessageHandler != null)
{
_httpClient = new HttpClient(httpMessageHandler);
}
}
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false);
_inMemoryFileStorage?.Clear();
}
/// <summary>
/// Clears file if it has expired
/// </summary>
/// <param name="duration">timespan to compute whether file has expired or not</param>
/// <returns>awaitable task</returns>
public Task ClearAsync(TimeSpan duration)
{
return RemoveExpiredAsync(duration);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
foreach (var file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Clear(expiryDuration);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
var keys = new List<string>();
Dictionary<string, string> hashDictionary = new Dictionary<string, string>();
foreach (var file in files)
{
hashDictionary.Add(Path.GetFileName(file), file);
}
foreach (var uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (hashDictionary.TryGetValue(fileName, out var file))
{
filesToDelete.Add(file);
keys.Add(fileName);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Remove(keys);
}
/// <summary>
/// Assures that item represented by Uri is cached.
/// </summary>
/// <param name="uri">Uri of the item</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param>
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>Awaitable Task</returns>
public Task PreCacheAsync(
Uri uri,
bool throwOnError = false,
bool storeToMemoryCache = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>an instance of Generic type</returns>
public Task<T?> GetFromCacheAsync(
Uri uri,
bool throwOnError = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, false, cancellationToken);
}
/// <summary>
/// Gets the string containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a string</returns>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
return Path.Combine(folder!, GetCacheFileName(uri));
}
/// <summary>
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>an instance of Generic type</returns>
public T? GetFromMemoryCache(Uri uri)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
return instance;
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(Stream stream);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(string baseFile);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var info = new FileInfo(file);
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
private async Task<T?> GetItemAsync(
Uri uri,
bool throwOnError,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
_concurrentTasks.TryGetValue(fileName, out var request);
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
{
if (request.Task != null)
await request.Task.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = new ConcurrentRequest()
{
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken),
EnsureCachedCopy = preCacheOnly
};
_concurrentTasks[fileName] = request;
}
try
{
if (request.Task != null)
instance = await request.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
if (throwOnError)
{
throw;
}
}
finally
{
_concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T?> GetFromCacheOrDownloadAsync(
Uri uri,
string fileName,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
if (instance != null)
{
return instance;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var baseFile = Path.Combine(folder!, fileName);
bool downloadDataFile =
!File.Exists(baseFile)
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
if (!File.Exists(baseFile))
{
File.Create(baseFile).Dispose();
}
if (downloadDataFile)
{
uint retries = 0;
try
{
while (retries < RetryCount)
{
try
{
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken)
.ConfigureAwait(false);
if (instance != null)
{
break;
}
}
catch (FileNotFoundException) { }
retries++;
}
}
catch (Exception ex)
{
File.Delete(baseFile);
throw; // re-throwing the exception changes the stack trace. just throw
}
}
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly)
{
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var properties = new FileInfo(baseFile);
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance);
_inMemoryFileStorage?.SetItem(msi);
}
}
return instance;
}
private async Task<T?> DownloadFileAsync(
Uri uri,
string baseFile,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
using (MemoryStream ms = new MemoryStream())
{
using (var stream = await HttpClient.GetStreamAsync(uri))
{
stream.CopyTo(ms);
ms.Flush();
ms.Position = 0;
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write))
{
ms.CopyTo(fs);
fs.Flush();
ms.Position = 0;
}
}
// if its pre-cache we aren't looking to load items in memory
if (!preCacheOnly)
{
instance = await ConvertFromAsync(ms).ConfigureAwait(false);
}
}
return instance;
}
private async Task InternalClearAsync(IEnumerable<string?> files)
{
foreach (var file in files)
{
try
{
File.Delete(file!);
}
catch
{
// Just ignore errors for now}
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task ForceInitialiseAsync()
{
if (_cacheFolder != null)
{
return;
}
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false);
_inMemoryFileStorage = new InMemoryStorage<T>();
if (_baseFolder == null)
{
_baseFolder = Path.GetTempPath();
}
if (string.IsNullOrWhiteSpace(_cacheFolderName))
{
_cacheFolderName = GetType().Name;
}
try
{
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName);
Directory.CreateDirectory(_cacheFolder);
}
finally
{
_cacheFolderSemaphore.Release();
}
}
private async Task<string?> GetCacheFolderAsync()
{
if (_cacheFolder == null)
{
await ForceInitialiseAsync().ConfigureAwait(false);
}
return _cacheFolder;
}
}

18
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs

@ -0,0 +1,18 @@
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
public class CacheOptions
{
private static CacheOptions? _cacheOptions;
public static CacheOptions Default => _cacheOptions ??= new CacheOptions();
public static void SetDefault(CacheOptions defaultCacheOptions)
{
_cacheOptions = defaultCacheOptions;
}
public string? BaseCachePath { get; set; }
public TimeSpan? CacheDuration { get; set; }
}

36
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs

@ -0,0 +1,36 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// </summary>
internal class FileCache : CacheBase<string>
{
/// <summary>
/// Private singleton field.
/// </summary>
private static FileCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static FileCache Instance => _instance ?? (_instance = new FileCache());
protected override Task<string> ConvertFromAsync(Stream stream)
{
// nothing to do in this instance;
return Task.FromResult<string>("");
}
/// <summary>
/// Returns a cached path
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected override Task<string> ConvertFromAsync(string baseFile)
{
return Task.FromResult(baseFile);
}
}

76
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs

@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache images in a folder
/// </summary>
internal class ImageCache : CacheBase<Bitmap>
{
/// <summary>
/// Private singleton field.
/// </summary>
[ThreadStatic]
private static ImageCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static ImageCache Instance => _instance ?? (_instance = new ImageCache());
/// <summary>
/// Creates a bitmap from a stream
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(Stream stream)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return new Bitmap(stream);
}
/// <summary>
/// Creates a bitmap from a cached file
/// </summary>
/// <param name="baseFile">file</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(string baseFile)
{
using (var stream = File.OpenRead(baseFile))
{
return await ConvertFromAsync(stream).ConfigureAwait(false);
}
}
/// <summary>
/// Checks whether file is valid or not.
/// </summary>
/// <param name="file">file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var fileInfo = new FileInfo(file);
return fileInfo.Length == 0
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration;
}
}

156
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs

@ -0,0 +1,156 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Labs.Controls.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic in-memory storage of items
/// </summary>
/// <typeparam name="T">T defines the type of item stored</typeparam>
public class InMemoryStorage<T>
{
private int _maxItemCount;
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage =
new ConcurrentDictionary<string, InMemoryStorageItem<T>>();
private object _settingMaxItemCountLocker = new object();
/// <summary>
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance.
/// </summary>
public int MaxItemCount
{
get { return _maxItemCount; }
set
{
if (_maxItemCount == value)
{
return;
}
_maxItemCount = value;
lock (_settingMaxItemCountLocker)
{
EnsureStorageBounds(value);
}
}
}
/// <summary>
/// Clears all items stored in memory
/// </summary>
public void Clear()
{
_inMemoryStorage.Clear();
}
/// <summary>
/// Clears items stored in memory based on duration passed
/// </summary>
/// <param name="duration">TimeSpan to identify expired items</param>
public void Clear(TimeSpan duration)
{
var expirationDate = DateTime.Now.Subtract(duration);
var itemsToRemove = _inMemoryStorage
.Where(kvp => kvp.Value.LastUpdated <= expirationDate)
.Select(kvp => kvp.Key);
if (itemsToRemove.Any())
{
Remove(itemsToRemove);
}
}
/// <summary>
/// Remove items based on provided keys
/// </summary>
/// <param name="keys">identified of the in-memory storage item</param>
public void Remove(IEnumerable<string> keys)
{
foreach (var key in keys)
{
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
_inMemoryStorage.TryRemove(key, out _);
}
}
/// <summary>
/// Add new item to in-memory storage
/// </summary>
/// <param name="item">item to be stored</param>
public void SetItem(InMemoryStorageItem<T> item)
{
if (MaxItemCount == 0)
{
return;
}
_inMemoryStorage[item.Id] = item;
// ensure max limit is maintained. trim older entries first
if (_inMemoryStorage.Count > MaxItemCount)
{
var itemsToRemove = _inMemoryStorage
.OrderBy(kvp => kvp.Value.Created)
.Take(_inMemoryStorage.Count - MaxItemCount)
.Select(kvp => kvp.Key);
Remove(itemsToRemove);
}
}
/// <summary>
/// Get item from in-memory storage as long as it has not ex
/// </summary>
/// <param name="id">id of the in-memory storage item</param>
/// <param name="duration">timespan denoting expiration</param>
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns>
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration)
{
if (!_inMemoryStorage.TryGetValue(id, out var tempItem))
{
return null;
}
var expirationDate = DateTime.Now.Subtract(duration);
if (tempItem.LastUpdated > expirationDate)
{
return tempItem;
}
_inMemoryStorage.TryRemove(id, out _);
return null;
}
private void EnsureStorageBounds(int maxCount)
{
if (_inMemoryStorage.Count == 0)
{
return;
}
if (maxCount == 0)
{
_inMemoryStorage.Clear();
return;
}
if (_inMemoryStorage.Count > maxCount)
{
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount));
}
}
}

49
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs

@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic InMemoryStorageItem holds items for InMemoryStorage.
/// </summary>
/// <typeparam name="T">Type is set by consuming cache</typeparam>
public class InMemoryStorageItem<T>
{
/// <summary>
/// Gets the item identifier
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets the item created timestamp.
/// </summary>
public DateTime Created { get; private set; }
/// <summary>
/// Gets the item last updated timestamp.
/// </summary>
public DateTime LastUpdated { get; private set; }
/// <summary>
/// Gets the item being stored.
/// </summary>
public T Item { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class.
/// Constructor for InMemoryStorageItem
/// </summary>
/// <param name="id">uniquely identifies the item</param>
/// <param name="lastUpdated">last updated timestamp</param>
/// <param name="item">the item being stored</param>
public InMemoryStorageItem(string id, DateTime lastUpdated, T item)
{
Id = id;
LastUpdated = lastUpdated;
Item = item;
Created = DateTime.Now;
}
}

21
StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 AvaloniaUI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml

@ -0,0 +1,48 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Border Width="200"
Height="200">
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type vendorLabs:BetterAsyncImage}"
TargetType="vendorLabs:BetterAsyncImage">
<Setter Property="Background" Value="Transparent" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Margin="0"
Padding="0"
ClipToBounds="True"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Name="PART_PlaceholderImage"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Source="{TemplateBinding PlaceholderSource}"
Stretch="{TemplateBinding PlaceholderStretch}"/>
<Image Name="PART_Image"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Stretch="{TemplateBinding Stretch}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[State=Failed] /template/ Image#PART_Image">
<Setter Property="Opacity"
Value="0.0" />
</Style>
<Style Selector="^[State=Failed] /template/ Image#PART_PlaceholderImage">
<Setter Property="Opacity"
Value="1.0" />
</Style>
</ControlTheme>
</ResourceDictionary>

116
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -36,6 +36,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
@ -51,6 +52,7 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData;
[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
@ -98,6 +100,19 @@ public static class DesignData
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
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
@ -312,7 +327,7 @@ public static class DesignData
);
}*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -405,7 +420,7 @@ public static class DesignData
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package..."),
ModificationCompleteMessage = "Done"
ModificationCompleteMessage = "Package installed successfully"
}
)
}
@ -508,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;
}
}
@ -898,6 +923,39 @@ The gallery images are often inpainted, but you will get something very similar
vm.IsBatchIndexEnabled = true;
});
public static LayerDiffuseCardViewModel LayerDiffuseCardViewModel =>
DialogFactory.Get<LayerDiffuseCardViewModel>();
public static InstalledWorkflowsViewModel InstalledWorkflowsViewModel
{
get
{
var vm = Services.GetRequiredService<InstalledWorkflowsViewModel>();
vm.DisplayedWorkflows = new ObservableCollectionExtended<OpenArtMetadata>
{
new()
{
Workflow = new()
{
Name = "Test Workflow",
Creator = new OpenArtCreator { Name = "Test Creator" },
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512"
)
}
]
}
}
};
return vm;
}
}
public static IList<ICompletionData> SampleCompletionData =>
new List<ICompletionData>
{
@ -1009,6 +1067,60 @@ The gallery images are often inpainted, but you will get something very similar
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<ControlNetCardViewModel>();
public static OpenArtWorkflowViewModel OpenArtWorkflowViewModel =>
new(Services.GetRequiredService<ISettingsManager>(), Services.GetRequiredService<IPackageFactory>())
{
Workflow = new OpenArtSearchResult
{
Name = "Test Workflow",
Creator = new OpenArtCreator
{
Name = "Test Creator Name",
Username = "Test Creator Username"
},
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
}
],
NodesIndex =
[
"Anything Everywhere",
"Reroute",
"Note",
".",
"ComfyUI's ControlNet Auxiliary Preprocessors",
"DWPreprocessor",
"PixelPerfectResolution",
"AIO_Preprocessor",
",",
"ComfyUI",
"PreviewImage",
"CLIPTextEncode",
"EmptyLatentImage",
"SplitImageWithAlpha",
"ControlNetApplyAdvanced",
"JoinImageWithAlpha",
"LatentUpscaleBy",
"VAEEncode",
"LoadImage",
"ControlNetLoader",
"CLIPVisionLoader",
"SaveImage",
",",
"ComfyUI Impact Pack",
"SAMLoader",
"UltralyticsDetectorProvider",
"FaceDetailer",
","
]
}
};
public static string CurrentDirectory => Directory.GetCurrentDirectory();
public static Indexer Types { get; } = new();

3
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

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

93
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

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

2
StabilityMatrix.Avalonia/Helpers/UriHandler.cs

@ -83,7 +83,7 @@ public class UriHandler
[SupportedOSPlatform("windows")]
private void RegisterUriSchemeWin()
{
using var key = Registry.CurrentUser.CreateSubKey(@$"SOFTWARE\Classes\{Scheme}");
using var key = Registry.CurrentUser.CreateSubKey(@$"SOFTWARE\Classes\{Scheme}", true);
key.SetValue("", "URL:" + Description);
key.SetValue(null, Description);

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

@ -374,6 +374,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open on OpenArt.
/// </summary>
public static string Action_OpenOnOpenArt {
get {
return ResourceManager.GetString("Action_OpenOnOpenArt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Project....
/// </summary>
@ -734,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>
/// Looks up a localized string similar to API Key.
/// </summary>
@ -1301,6 +1319,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Error retrieving workflows.
/// </summary>
public static string Label_ErrorRetrievingWorkflows {
get {
return ResourceManager.GetString("Label_ErrorRetrievingWorkflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Everything looks good!.
/// </summary>
@ -1337,6 +1364,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Finished importing workflow and custom nodes.
/// </summary>
public static string Label_FinishedImportingWorkflow {
get {
return ResourceManager.GetString("Label_FinishedImportingWorkflow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to First Page.
/// </summary>
@ -1481,6 +1517,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Infinite Scrolling.
/// </summary>
public static string Label_InfiniteScrolling {
get {
return ResourceManager.GetString("Label_InfiniteScrolling", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inner exception.
/// </summary>
@ -1769,6 +1814,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Node Details.
/// </summary>
public static string Label_NodeDetails {
get {
return ResourceManager.GetString("Label_NodeDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No extensions found..
/// </summary>
@ -1832,6 +1886,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to OpenArt Browser.
/// </summary>
public static string Label_OpenArtBrowser {
get {
return ResourceManager.GetString("Label_OpenArtBrowser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Folder.
/// </summary>
@ -2273,6 +2336,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>
/// Looks up a localized string similar to Steps.
/// </summary>
@ -2543,6 +2615,69 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Browser.
/// </summary>
public static string Label_WorkflowBrowser {
get {
return ResourceManager.GetString("Label_WorkflowBrowser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Deleted.
/// </summary>
public static string Label_WorkflowDeleted {
get {
return ResourceManager.GetString("Label_WorkflowDeleted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} deleted successfully.
/// </summary>
public static string Label_WorkflowDeletedSuccessfully {
get {
return ResourceManager.GetString("Label_WorkflowDeletedSuccessfully", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Description.
/// </summary>
public static string Label_WorkflowDescription {
get {
return ResourceManager.GetString("Label_WorkflowDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The workflow and custom nodes have been imported..
/// </summary>
public static string Label_WorkflowImportComplete {
get {
return ResourceManager.GetString("Label_WorkflowImportComplete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Imported.
/// </summary>
public static string Label_WorkflowImported {
get {
return ResourceManager.GetString("Label_WorkflowImported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflows.
/// </summary>
public static string Label_Workflows {
get {
return ResourceManager.GetString("Label_Workflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You&apos;re up to date.
/// </summary>
@ -2624,6 +2759,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Installed Workflows.
/// </summary>
public static string TabLabel_InstalledWorkflows {
get {
return ResourceManager.GetString("TabLabel_InstalledWorkflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add a package to get started!.
/// </summary>
@ -2651,6 +2795,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Click here to review prompt syntax and how to include Lora / Embeddings..
/// </summary>
public static string TeachingTip_InferencePromptHelpButton {
get {
return ResourceManager.GetString("TeachingTip_InferencePromptHelpButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here.
/// </summary>

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

@ -963,4 +963,66 @@
<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>
<data name="Label_Notifications" xml:space="preserve">
<value>Notificaciones</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Ninguno</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>Se Requiere ComfyUI</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>Se requiere ComfyUI para instalar este paquete. ¿Quieres instalarlo ahora?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Por favor, seleccione una ubicación de descarga.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Seleccione la Ubicación de Descarga:</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Configuración</value>
<comment>This is used in inference when models have a yaml config file</comment>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Desplazamiento Automático hasta el Final</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Confirmar Salida</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>¿Seguro que quieres salir? Esto también cerrará cualquier paquete que se esté ejecutando actualmente.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Consola</value>
</data>
<data name="Label_WebUi" xml:space="preserve">
<value>Interfaz Web</value>
<comment>This will be used on a button to launch the web ui</comment>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paquetes</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Esta acción no se puede deshacer.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>¿Estás seguro de que deseas eliminar {0} imágenes?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Estamos verificando algunas especificaciones de hardware para determinar la compatibilidad.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>¡Todo parece estar bien!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>Recomendamos una GPU con soporte CUDA para obtener la mejor experiencia. Puedes continuar sin una, pero es posible que algunos paquetes no funcionen y la inferencia pueda ser más lenta.</value>
</data>
<data name="Label_Checkpoints" xml:space="preserve">
<value>Checkpoints</value>
</data>
<data name="Label_ModelBrowser" xml:space="preserve">
<value>Navegador de Modelos</value>
</data>
</root>

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

@ -598,7 +598,7 @@
<value>Informations sur la version de Python</value>
</data>
<data name="Action_Restart" xml:space="preserve">
<value>Redémarrage</value>
<value>Redémarrer</value>
</data>
<data name="Label_ConfirmDelete" xml:space="preserve">
<value>Confirmer la suppression</value>
@ -930,4 +930,52 @@
<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>
<data name="Label_Notifications" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Aucun</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>ComfyUI est requis</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>ComfyUI est requis pour installer ce paquet. Voulez vous l&apos;installer maintenant ?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Merci de sélectionner un dossier de téléchargement.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Sélectionner un dossier de téléchargement:</value>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Défiler automatiquement à la fin</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Confirmer la sortie</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>Êtes vous certain de vouloir quitter ? Cela va fermer toutes les instances actuellement lancées.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Console</value>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paquets</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Cette action ne peut être annulée.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>Êtes vous certain de vouloir supprimer les {0} images?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Nous vérifions quelques spécifications matérielles pour vérifier la compatibilité.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>Tout à l&apos;air bon!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>Nous recommandons un GPU avec prise en charge CUDA pour une meilleure expérience. Vous pouvez continuer sans en avoir un, mais certains packages peuvent ne pas fonctionner et l&apos;inférence peut être plus lente.</value>
</data>
</root>

51
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -972,9 +972,30 @@
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Select Download Location:</value>
</data>
<data name="Label_Workflows" xml:space="preserve">
<value>Workflows</value>
</data>
<data name="Label_InfiniteScrolling" xml:space="preserve">
<value>Infinite Scrolling</value>
</data>
<data name="Label_WorkflowBrowser" xml:space="preserve">
<value>Workflow Browser</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Config</value>
</data>
<data name="Action_OpenOnOpenArt" xml:space="preserve">
<value>Open on OpenArt</value>
</data>
<data name="Label_NodeDetails" xml:space="preserve">
<value>Node Details</value>
</data>
<data name="Label_WorkflowDescription" xml:space="preserve">
<value>Workflow Description</value>
</data>
<data name="Label_OpenArtBrowser" xml:space="preserve">
<value>OpenArt Browser</value>
</data>
<data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Preview Preprocessor</value>
</data>
@ -1020,4 +1041,34 @@
<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>
<data name="Label_WorkflowDeletedSuccessfully" xml:space="preserve">
<value>{0} deleted successfully</value>
</data>
<data name="Label_WorkflowDeleted" xml:space="preserve">
<value>Workflow Deleted</value>
</data>
<data name="Label_ErrorRetrievingWorkflows" xml:space="preserve">
<value>Error retrieving workflows</value>
</data>
<data name="TabLabel_InstalledWorkflows" xml:space="preserve">
<value>Installed Workflows</value>
</data>
<data name="Label_WorkflowImported" xml:space="preserve">
<value>Workflow Imported</value>
</data>
<data name="Label_FinishedImportingWorkflow" xml:space="preserve">
<value>Finished importing workflow and custom nodes</value>
</data>
<data name="Label_WorkflowImportComplete" xml:space="preserve">
<value>The workflow and custom nodes have been imported.</value>
</data>
<data name="TeachingTip_InferencePromptHelpButton" xml:space="preserve">
<value>Click here to review prompt syntax and how to include Lora / Embeddings.</value>
</data>
</root>

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

@ -961,4 +961,64 @@
<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>
<data name="Label_Notifications" xml:space="preserve">
<value>Bildirimler</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Hiçbiri</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>ComfyUI Gerekli</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>Bu paketi kurmak için ComfyUI gereklidir. Şimdi yüklemek ister misiniz?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Lütfen bir indirme konumu seçin.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>İndirme Konumunu Seçin:</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Yapılandırma</value>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Otomatik Sona Kaydır</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Çıkışı Onayla</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>Çıkmak istediğine emin misin? Bu aynı zamanda şu anda çalışan tüm paketleri de kapatacaktır.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Konsol</value>
</data>
<data name="Label_WebUi" xml:space="preserve">
<value>Web arayüzü</value>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paketler</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Bu eylem geri alınamaz.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>{0} görseli silmek istediğinizden emin misiniz?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Uyumluluğu belirlemek için bazı donanım özelliklerini kontrol ediyoruz.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>Her şey iyi gözüküyor!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>En iyi deneyim için CUDA destekli bir GPU öneriyoruz. Olmadan da devam edebilirsiniz ancak bazı paketler çalışmayabilir ve çıkarımlar daha yavaş olabilir.</value>
</data>
<data name="Label_Checkpoints" xml:space="preserve">
<value>Kontrol noktaları</value>
</data>
<data name="Label_ModelBrowser" xml:space="preserve">
<value>Model Tarayıcı</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Models/ImageSource.cs

@ -58,6 +58,9 @@ public record ImageSource : IDisposable, ITemplateKey<ImageSourceTemplateType>
Bitmap = bitmap;
}
[JsonIgnore]
public Uri? Uri => LocalFile?.FullPath != null ? new Uri(LocalFile.FullPath) : RemoteUrl;
/// <inheritdoc />
public ImageSourceTemplateType TemplateKey { get; private set; }

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

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Hashing;
using System.Linq;
using System.Text;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.Models.Inference;
@ -17,7 +19,7 @@ public class ModuleApplyStepEventArgs : EventArgs
public NodeDictionary Nodes => Builder.Nodes;
public ModuleApplyStepTemporaryArgs Temp { get; } = new();
public ModuleApplyStepTemporaryArgs Temp { get; set; } = new();
/// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.)
@ -26,6 +28,35 @@ public class ModuleApplyStepEventArgs : EventArgs
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
public List<Action<ModuleApplyStepEventArgs>> PreOutputActions { get; init; } = [];
public void InvokeAllPreOutputActions()
{
foreach (var action in PreOutputActions)
{
action(this);
}
}
/// <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)
{
FilesToTransfer.Add((sourcePath, destinationRelativePath));
@ -54,22 +85,4 @@ public class ModuleApplyStepEventArgs : EventArgs
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; }
}
}

10
StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace StabilityMatrix.Avalonia.Models;
public class OpenArtCustomNode
{
public required string Title { get; set; }
public List<string> Children { get; set; } = [];
public bool IsInstalled { get; set; }
}

22
StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Avalonia.Platform.Storage;
using StabilityMatrix.Core.Models.Api.OpenArt;
namespace StabilityMatrix.Avalonia.Models;
public class OpenArtMetadata
{
[JsonPropertyName("sm_workflow_data")]
public OpenArtSearchResult? Workflow { get; set; }
[JsonIgnore]
public string? FirstThumbnail => Workflow?.Thumbnails?.Select(x => x.Url).FirstOrDefault()?.ToString();
[JsonIgnore]
public List<IStorageFile>? FilePath { get; set; }
[JsonIgnore]
public bool HasMetadata => Workflow?.Creator != null;
}

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 ObservableCollection<PackageOutputCategory> SubDirectories { get; set; } = new();
public required string Name { get; set; }
public required string Path { get; set; }
}

21
StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs

@ -0,0 +1,21 @@
using System;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
namespace StabilityMatrix.Avalonia.Models.PackageSteps;
public class UnpackSiteCustomizeStep(DirectoryPath venvPath) : IPackageStep
{
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath);
var file = sitePackages.JoinFile("sitecustomize.py");
file.Directory?.Create();
await Assets.PyScriptSiteCustomize.ExtractTo(file);
}
public string ProgressTitle => "Unpacking prerequisites...";
}

1
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

@ -44,6 +44,7 @@ public interface IInferenceClientManager : IDisposable, INotifyPropertyChanged,
IObservableCollection<ComfySampler> Samplers { get; }
IObservableCollection<ComfyUpscaler> Upscalers { get; }
IObservableCollection<ComfyScheduler> Schedulers { get; }
IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; }
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();
}

2
StabilityMatrix.Avalonia/Services/INavigationService.cs

@ -37,4 +37,6 @@ public interface INavigationService<[SuppressMessage("ReSharper", "UnusedTypePar
void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null);
bool GoBack();
bool CanGoBack { get; }
}

20
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -14,6 +14,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Inference;
using StabilityMatrix.Core.Models;
@ -101,6 +102,11 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>();
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>();
private readonly SourceCache<ComfyAuxPreprocessor, string> preprocessorsSource = new(p => p.Value);
public InferenceClientManager(
ILogger<InferenceClientManager> logger,
IApiFactory apiFactory,
@ -166,6 +172,8 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
schedulersSource.Connect().DeferUntilLoaded().Bind(Schedulers).Subscribe();
preprocessorsSource.Connect().DeferUntilLoaded().Bind(Preprocessors).Subscribe();
settingsManager.RegisterOnLibraryDirSet(_ =>
{
Dispatcher.UIThread.Post(ResetSharedProperties, DispatcherPriority.Background);
@ -270,6 +278,15 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
});
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>
@ -342,6 +359,9 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
u => !modelUpscalersSource.Lookup(u.Name).HasValue
);
downloadableUpscalersSource.EditDiff(remoteUpscalers, ComfyUpscaler.Comparer);
// Default Preprocessors
preprocessorsSource.EditDiff(ComfyAuxPreprocessor.Defaults);
}
/// <inheritdoc />

251
StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs

@ -0,0 +1,251 @@
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.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
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;
}
}

2
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -188,4 +188,6 @@ public class NavigationService<T> : INavigationService<T>
_frame.GoBack();
return true;
}
public bool CanGoBack => _frame?.CanGoBack ?? false;
}

153
StabilityMatrix.Avalonia/Services/RunningPackageService.cs

@ -0,0 +1,153 @@
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
);
if (installedPackage.UseSharedOutputFolder)
{
await basePackage.SetupOutputFolderLinks(installedPackage.FullPath!);
}
// 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);
}
}

40
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -39,16 +39,17 @@
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.3" />
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.9" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.10.1" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.10.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.0.6.3" />
<PackageReference Include="bodong.PropertyModels" Version="11.0.6.2" />
@ -57,10 +58,10 @@
<PackageReference Include="DesktopNotifications" Version="1.3.1" />
<PackageReference Include="DesktopNotifications.Avalonia" Version="1.3.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.5" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.5" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.5" />
<PackageReference Include="DynamicData" Version="8.3.27" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.7" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.7" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.7" />
<PackageReference Include="DynamicData" Version="8.4.1" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
@ -77,25 +78,25 @@
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="5.2.8" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="OneOf" Version="3.0.263" />
<PackageReference Include="Polly" Version="8.3.0" />
<PackageReference Include="Polly" Version="8.3.1" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.1" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.2" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="3.0.0" />
<PackageReference Include="Sentry" Version="4.1.2" />
<PackageReference Include="Sentry.NLog" Version="4.1.2" />
<PackageReference Include="Sentry" Version="4.2.1" />
<PackageReference Include="Sentry.NLog" Version="4.2.1" />
<PackageReference Include="SpacedGrid-Avalonia" Version="11.0.0" />
<PackageReference Include="Sylvan.Common" Version="0.4.3" />
<PackageReference Include="Sylvan.Data" Version="0.2.13" />
<PackageReference Include="Sylvan.Data.Csv" Version="1.3.7" />
<PackageReference Include="System.Drawing.Common" Version="8.0.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
<PackageReference Include="URISchemeTools" Version="1.0.2" />
@ -197,6 +198,9 @@
<DependentUpon>NewInstallerDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Models\OpenArtMetadata.cs">
<Generator>MSBuild:GenerateCodeFromAttributes</Generator>
</Compile>
</ItemGroup>
<!-- set HUSKY to 0 to disable, or opt-in during CI by setting HUSKY to 1 -->

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings

@ -2,4 +2,5 @@
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String>
<s:String x:Key="/Default/CodeInspection/Daemon/ConfigureAwaitAnalysisMode/@EntryValue">UI</s:String>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cvendorlabs_005Casyncimage/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

73
StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml

@ -99,6 +99,43 @@
</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 -->
<Style Selector="Button.info">
<Style Selector="^ /template/ ui|FABorder#Root">
@ -140,6 +177,42 @@
</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-->
<Style Selector="Button.accent">
<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>

30
StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml

@ -6,7 +6,9 @@
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Panel Width="450" Height="600">
@ -42,15 +44,14 @@
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
Stretch="UniformToFill"/>
<StackPanel
Grid.Column="1"
MaxWidth="300"
@ -125,18 +126,14 @@
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Grid.RowSpan="2"
Width="42"
Height="42"
RenderOptions.BitmapInterpolationMode="HighQuality"
CornerRadius="40"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,42,42" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
Stretch="UniformToFill"/>
<!-- Text -->
<sg:SpacedGrid
@ -217,18 +214,15 @@
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Grid.RowSpan="2"
Width="36"
Height="36"
CornerRadius="60"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,36,36" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
Stretch="UniformToFill">
</vendorLabs:BetterAsyncImage>
<!-- Text -->
<sg:SpacedGrid

1
StabilityMatrix.Avalonia/Styles/ThemeColors.axaml

@ -21,6 +21,7 @@
<Color x:Key="ThemeCyanColor">#00BCD4</Color>
<Color x:Key="ThemeTealColor">#009688</Color>
<Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color>
<SolidColorBrush x:Key="ThemeDarkDarkGreenColorBrush">#2C582C</SolidColorBrush>
<Color x:Key="ThemeDarkGreenColor">#3A783C</Color>
<Color x:Key="ThemeGreenColor">#4BA04F</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 ExifLibrary;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using Nito.Disposables.Internals;
using NLog;
using Refit;
using Semver;
using SkiaSharp;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
@ -59,6 +58,7 @@ public abstract partial class InferenceGenerationViewModelBase
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly RunningPackageService runningPackageService;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
@ -79,12 +79,14 @@ public abstract partial class InferenceGenerationViewModelBase
ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager,
INotificationService notificationService,
ISettingsManager settingsManager
ISettingsManager settingsManager,
RunningPackageService runningPackageService
)
: base(notificationService)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.runningPackageService = runningPackageService;
this.vmFactory = vmFactory;
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>
/// Runs a generation task
/// </summary>
@ -643,12 +669,10 @@ public abstract partial class InferenceGenerationViewModelBase
{
// Get prompt required extensions
// Just static for now but could do manifest lookup when we support custom workflows
var requiredExtensions = nodeDictionary
.ClassTypeRequiredExtensions.Values.SelectMany(x => x)
.ToHashSet();
var requiredExtensionSpecifiers = nodeDictionary.RequiredExtensions.ToList();
// Skip if no extensions required
if (requiredExtensions.Count == 0)
if (requiredExtensionSpecifiers.Count == 0)
{
return true;
}
@ -661,20 +685,63 @@ public abstract partial class InferenceGenerationViewModelBase
await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync(
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
.Except(localExtensions.Select(ext => ext.GitRepositoryUrl).WhereNotNull())
.ToImmutableArray();
var missingExtensions = new List<ExtensionSpecifier>();
var outOfDateExtensions =
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;
}
var dialog = DialogHelper.CreateMarkdownDialog(
$"#### 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?"
);
@ -692,13 +759,14 @@ public abstract partial class InferenceGenerationViewModelBase
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(
"Extension {MissingExtensionUrl} not found in manifests",
missingExtensionUrl
missingExtension.Name
);
continue;
}
@ -706,6 +774,18 @@ public abstract partial class InferenceGenerationViewModelBase
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
{
ShowDialogOnStart = true,
@ -722,15 +802,12 @@ public abstract partial class InferenceGenerationViewModelBase
return;
// Restart Package
// TODO: This should be handled by some DI package manager service
var launchPage = App.Services.GetRequiredService<LaunchPageViewModel>();
try
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await launchPage.Stop();
await launchPage.LaunchAsync();
await runningPackageService.StopPackage(localPackagePair.InstalledPackage.Id);
await runningPackageService.StartPackage(localPackagePair.InstalledPackage);
});
}
catch (Exception e)

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

@ -98,11 +98,7 @@ public abstract partial class InferenceTabViewModelBase
protected async Task<ViewState> SaveViewState()
{
var eventArgs = new SaveViewStateEventArgs();
saveViewStateRequestedEventManager?.RaiseEvent(
this,
eventArgs,
nameof(SaveViewStateRequested)
);
saveViewStateRequestedEventManager?.RaiseEvent(this, eventArgs, nameof(SaveViewStateRequested));
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
var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>();
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo());
navService.NavigateTo<NewPackageManagerViewModel>(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition());
}
@ -157,9 +153,7 @@ public abstract partial class InferenceTabViewModelBase
if (result == ContentDialogResult.Primary && textFields[0].Text is { } json)
{
LoadViewState(
new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } }
);
LoadViewState(new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } });
}
}
@ -226,9 +220,7 @@ public abstract partial class InferenceTabViewModelBase
if (this is IParametersLoadableState paramsLoadableVm)
{
Dispatcher.UIThread.Invoke(
() => paramsLoadableVm.LoadStateFromParameters(parameters)
);
Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters));
}
else
{

3
StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

@ -11,6 +11,7 @@ using NLog;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -20,12 +21,14 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
[JsonDerivedType(typeof(UpscalerCardViewModel), UpscalerCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(ControlNetCardViewModel), ControlNetCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(PromptExpansionCardViewModel), PromptExpansionCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(LayerDiffuseCardViewModel), LayerDiffuseCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(FreeUModule))]
[JsonDerivedType(typeof(HiresFixModule))]
[JsonDerivedType(typeof(UpscalerModule))]
[JsonDerivedType(typeof(ControlNetModule))]
[JsonDerivedType(typeof(SaveImageModule))]
[JsonDerivedType(typeof(PromptExpansionModule))]
[JsonDerivedType(typeof(LayerDiffuseModule))]
public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

32
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@ -19,6 +20,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database;
@ -157,7 +159,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
// Try to find a valid image
var image = images
?.Where(img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains))
?.Where(
img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains) && img.Type == "image"
)
.FirstOrDefault(image => nsfwEnabled || image.NsfwLevel <= 1);
if (image != null)
{
@ -216,7 +220,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
MaxDialogHeight = 950,
};
var prunedDescription = PruneDescription(model);
var prunedDescription = Utilities.RemoveHtml(model.Description);
var viewModel = dialogFactory.Get<SelectModelVersionViewModel>();
viewModel.Dialog = dialog;
@ -255,7 +259,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{
var subFolder =
viewModel?.SelectedInstallLocation
?? Path.Combine("Models", model.Type.ConvertTo<SharedFolderType>().GetStringValue());
?? Path.Combine(@"Models", model.Type.ConvertTo<SharedFolderType>().GetStringValue());
downloadPath = Path.Combine(settingsManager.LibraryDir, subFolder);
}
@ -263,23 +267,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
await DoImport(model, downloadPath, selectedVersion, selectedFile);
}
private static string PruneDescription(CivitModel model)
{
var prunedDescription =
model
.Description?.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h2>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h3>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h4>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h5>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h6>", $"{Environment.NewLine}{Environment.NewLine}") ?? string.Empty;
prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
return prunedDescription;
}
private static async Task<FilePath> SaveCmInfo(
CivitModel model,
CivitModelVersion modelVersion,
@ -310,7 +297,10 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
return null;
}
var image = modelVersion.Images[0];
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{

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

@ -196,10 +196,40 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScro
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
var models = new List<CivitModel>();
CivitModelsResponse? modelsResponse = null;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (!string.IsNullOrWhiteSpace(request.CommaSeparatedModelIds))
{
// count IDs
var ids = request.CommaSeparatedModelIds.Split(',');
if (ids.Length > 100)
{
var idChunks = ids.Chunk(100);
foreach (var chunk in idChunks)
{
request.CommaSeparatedModelIds = string.Join(",", chunk);
var chunkModelsResponse = await civitApi.GetModels(request);
if (chunkModelsResponse.Items != null)
{
models.AddRange(chunkModelsResponse.Items);
}
}
}
else
{
modelsResponse = await civitApi.GetModels(request);
models = modelsResponse.Items;
}
}
else
{
modelsResponse = await civitApi.GetModels(request);
models = modelsResponse.Items;
}
if (models is null)
{
Logger.Debug(
@ -241,13 +271,13 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScro
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
Metadata = modelsResponse?.Metadata
}
);
UpdateModelCards(models, isInfiniteScroll);
NextPageCursor = modelsResponse.Metadata?.NextCursor;
NextPageCursor = modelsResponse?.Metadata?.NextCursor;
}
catch (OperationCanceledException)
{
@ -428,12 +458,14 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScro
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
.Where(c => c.IsConnectedModel)
.ToList();
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}

3
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

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

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

@ -137,6 +137,7 @@ public partial class CheckpointFile : ViewModelBase
{
await Task.Run(() => File.Delete(cmInfoPath));
}
var settingsManager = App.Services.GetRequiredService<ISettingsManager>();
settingsManager.Transaction(s =>
{

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

@ -500,7 +500,7 @@ public partial class CheckpointFolder : ViewModelBase
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
var image = version.Images?.FirstOrDefault();
var image = version.Images?.FirstOrDefault(x => x.Type == "image");
if (image != null)
{
var imageExt = Path.GetExtension(image.Url).TrimStart('.');

3
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

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

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

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

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

@ -12,6 +12,7 @@ using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models.PackageSteps;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -75,7 +76,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
.Connect()
.DeferUntilLoaded()
.Filter(incompatiblePredicate)
.Filter(p => p.OfferInOneClickInstaller || ShowIncompatiblePackages)
.Filter(p => p.OfferInOneClickInstaller)
.Sort(
SortExpressionComparer<BasePackage>
.Ascending(p => p.InstallerSortOrder)
@ -107,18 +108,19 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
{
Task.Run(async () =>
{
var installLocation = Path.Combine(
settingsManager.LibraryDir,
"Packages",
selectedPackage.Name
);
var steps = new List<IPackageStep>
{
new SetPackageInstallingStep(settingsManager, selectedPackage.Name),
new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage)
new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage),
};
// get latest version & download & install
var installLocation = Path.Combine(
settingsManager.LibraryDir,
"Packages",
selectedPackage.Name
);
if (Directory.Exists(installLocation))
{
var installPath = new DirectoryPath(installLocation);
@ -148,6 +150,11 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
);
steps.Add(downloadStep);
var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(
Path.Combine(installLocation, "venv")
);
steps.Add(unpackSiteCustomizeStep);
var installStep = new InstallPackageStep(
selectedPackage,
torchVersion,

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

@ -196,7 +196,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
{
ShowDialogOnStart = true,
HideCloseButton = true,
ModificationCompleteMessage = $"{SelectedPackage.DisplayName} installed successfully",
ModificationCompleteMessage = $"{SelectedPackage.DisplayName} installed successfully"
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);

185
StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs

@ -0,0 +1,185 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(OpenArtWorkflowDialog))]
[ManagedService]
[Transient]
public partial class OpenArtWorkflowViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory
) : ContentDialogViewModelBase
{
public required OpenArtSearchResult Workflow { get; init; }
[ObservableProperty]
private ObservableCollection<OpenArtCustomNode> customNodes = [];
[ObservableProperty]
private string prunedDescription = string.Empty;
[ObservableProperty]
private bool installRequiredNodes = true;
[ObservableProperty]
private InstalledPackage? selectedPackage;
public PackagePair? SelectedPackagePair =>
SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null;
public List<InstalledPackage> AvailablePackages =>
settingsManager
.Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI")
.ToList();
public List<PackageExtension> MissingNodes { get; } = [];
public override async Task OnLoadedAsync()
{
if (Design.IsDesignMode)
return;
if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage)
{
SelectedPackage = preferredPackage;
}
else
{
SelectedPackage = AvailablePackages.FirstOrDefault();
}
if (SelectedPackage == null)
{
InstallRequiredNodes = false;
}
CustomNodes = new ObservableCollection<OpenArtCustomNode>(
await ParseNodes(Workflow.NodesIndex.ToList())
);
PrunedDescription = Utilities.RemoveHtml(Workflow.Description);
}
partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue)
{
if (oldValue is null)
return;
settingsManager.Transaction(settings =>
{
settings.PreferredWorkflowPackage = newValue;
});
OnLoadedAsync().SafeFireAndForget();
}
[Localizable(false)]
private async Task<List<OpenArtCustomNode>> ParseNodes(List<string> nodes)
{
var indexOfFirstDot = nodes.IndexOf(".");
if (indexOfFirstDot != -1)
{
nodes = nodes[(indexOfFirstDot + 1)..];
}
var installedNodesNames = new HashSet<string>();
var nameToManifestNodes = new Dictionary<string, PackageExtension>();
var packagePair = SelectedPackagePair;
if (packagePair?.BasePackage.ExtensionManager is { } extensionManager)
{
var installedNodes = (
await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage)
).ToList();
var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync(
extensionManager.GetManifests(packagePair.InstalledPackage)
);
// Add manifestExtensions definition to installedNodes if matching git repository url
installedNodes = installedNodes
.Select(installedNode =>
{
if (
installedNode.GitRepositoryUrl is not null
&& manifestExtensionsMap.TryGetValue(
installedNode.GitRepositoryUrl,
out var manifestExtension
)
)
{
installedNode = installedNode with { Definition = manifestExtension };
}
return installedNode;
})
.ToList();
// There may be duplicate titles, deduplicate by using the first one
nameToManifestNodes = manifestExtensionsMap
.GroupBy(x => x.Value.Title)
.ToDictionary(x => x.Key, x => x.First().Value);
installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet();
}
var sections = new List<OpenArtCustomNode>();
OpenArtCustomNode? currentSection = null;
foreach (var node in nodes)
{
if (node is "." or ",")
{
currentSection = null; // End of the current section
continue;
}
if (currentSection == null)
{
currentSection = new OpenArtCustomNode
{
Title = node,
IsInstalled = installedNodesNames.Contains(node)
};
// Add missing nodes to the list
if (
!currentSection.IsInstalled && nameToManifestNodes.TryGetValue(node, out var manifestNode)
)
{
MissingNodes.Add(manifestNode);
}
sections.Add(currentSection);
}
else
{
currentSection.Children.Add(node);
}
}
if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null)
{
sections = sections.Where(x => x.Title != "ComfyUI").ToList();
}
return sections;
}
}

4
StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelItemViewModel.cs

@ -22,9 +22,9 @@ public partial class RecommendedModelItemViewModel : ViewModelBase
private CivitModel civitModel;
public Uri ThumbnailUrl =>
ModelVersion.Images?.FirstOrDefault()?.Url == null
ModelVersion.Images?.FirstOrDefault(x => x.Type == "image")?.Url == null
? Assets.NoImage
: new Uri(ModelVersion.Images.First().Url);
: new Uri(ModelVersion.Images.First(x => x.Type == "image").Url);
[RelayCommand]
public void ToggleSelection() => IsSelected = !IsSelected;

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

@ -75,7 +75,7 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
CivitModels
.Connect()
.DeferUntilLoaded()
.Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0")
.Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0" || f.ModelVersion.BaseModel == "Pony")
.Bind(SdxlModels)
.Subscribe();
}
@ -206,7 +206,10 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
return null;
}
var image = modelVersion.Images[0];
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{

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

@ -99,7 +99,7 @@ public partial class SelectModelVersionViewModel(
var allImages = value
?.ModelVersion
?.Images
?.Where(img => nsfwEnabled || img.NsfwLevel <= 1)
?.Where(img => img.Type == "image" && (nsfwEnabled || img.NsfwLevel <= 1))
?.Select(x => new ImageSource(x.Url))
.ToList();
@ -316,7 +316,9 @@ public partial class SelectModelVersionViewModel(
installLocations.Add(downloadDirectory.ToString().Replace(rootModelsDirectory, "Models"));
foreach (var directory in downloadDirectory.EnumerateDirectories())
foreach (
var directory in downloadDirectory.EnumerateDirectories(searchOption: SearchOption.AllDirectories)
)
{
installLocations.Add(directory.ToString().Replace(rootModelsDirectory, "Models"));
}

9
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
@ -27,11 +28,9 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge =
new()
{
WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
SuccessToolTipText = "Everything looks good!",
FailToolTipText =
"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.",
WorkingToolTipText = Resources.Label_CheckingHardware,
SuccessToolTipText = Resources.Label_EverythingLooksGood,
FailToolTipText = Resources.Label_NvidiaGpuRecommended,
FailColorBrush = ThemeColors.ThemeYellow,
};

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

@ -1,19 +1,20 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -32,7 +33,17 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
[ObservableProperty]
[Required]
private HybridModelFile? selectedPreprocessor;
private ComfyAuxPreprocessor? selectedPreprocessor;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int width;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int height;
[ObservableProperty]
[Required]
@ -62,6 +73,18 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
ClientManager = clientManager;
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]
@ -79,4 +102,57 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
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);
}
}

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

@ -27,9 +27,17 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
IInferenceClientManager inferenceClientManager,
INotificationService notificationService,
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>();
@ -48,26 +56,30 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
_ => Convert.ToUInt64(SeedCardViewModel.Seed)
};
BatchSizeCardViewModel.ApplyStep(args);
var applyArgs = args.ToModuleApplyStepEventArgs();
BatchSizeCardViewModel.ApplyStep(applyArgs);
// Load models
ModelCardViewModel.ApplyStep(args);
ModelCardViewModel.ApplyStep(applyArgs);
// Setup image latent source
SelectImageCardViewModel.ApplyStep(args);
SelectImageCardViewModel.ApplyStep(applyArgs);
// Prompts and loras
PromptCardViewModel.ApplyStep(args);
PromptCardViewModel.ApplyStep(applyArgs);
// Setup Sampler and Refiner if enabled
SamplerCardViewModel.ApplyStep(args);
SamplerCardViewModel.ApplyStep(applyArgs);
// Apply module steps
foreach (var module in ModulesCardViewModel.Cards.OfType<ModuleBase>())
{
module.ApplyStep(args);
module.ApplyStep(applyArgs);
}
applyArgs.InvokeAllPreOutputActions();
builder.SetupOutputImage();
}
@ -77,12 +89,12 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
var mainImages = SelectImageCardViewModel.GetInputImages();
var samplerImages = SamplerCardViewModel
.ModulesCardViewModel
.Cards
.OfType<IInputImageProvider>()
.ModulesCardViewModel.Cards.OfType<IInputImageProvider>()
.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);
}

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

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

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

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

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

@ -59,9 +59,10 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
RunningPackageService runningPackageService
)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{
this.notificationService = notificationService;
this.modelIndexService = modelIndexService;
@ -130,10 +131,12 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
_ => Convert.ToUInt64(SeedCardViewModel.Seed)
};
BatchSizeCardViewModel.ApplyStep(args);
var applyArgs = args.ToModuleApplyStepEventArgs();
BatchSizeCardViewModel.ApplyStep(applyArgs);
// Load models
ModelCardViewModel.ApplyStep(args);
ModelCardViewModel.ApplyStep(applyArgs);
// Setup empty latent
builder.SetupEmptyLatentSource(
@ -144,17 +147,19 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
);
// Prompts and loras
PromptCardViewModel.ApplyStep(args);
PromptCardViewModel.ApplyStep(applyArgs);
// Setup Sampler and Refiner if enabled
SamplerCardViewModel.ApplyStep(args);
SamplerCardViewModel.ApplyStep(applyArgs);
// Hires fix if enabled
foreach (var module in ModulesCardViewModel.Cards.OfType<ModuleBase>())
{
module.ApplyStep(args);
module.ApplyStep(applyArgs);
}
applyArgs.InvokeAllPreOutputActions();
builder.SetupOutputImage();
}

100
StabilityMatrix.Avalonia/ViewModels/Inference/LayerDiffuseCardViewModel.cs

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using KGySoft.CoreLibraries;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[Transient]
[ManagedService]
[View(typeof(LayerDiffuseCard))]
public partial class LayerDiffuseCardViewModel : LoadableViewModelBase, IComfyStep
{
public const string ModuleKey = "LayerDiffuse";
[ObservableProperty]
private LayerDiffuseMode selectedMode = LayerDiffuseMode.None;
public IEnumerable<LayerDiffuseMode> AvailableModes => Enum<LayerDiffuseMode>.GetValues();
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[Range(-1d, 3d)]
private double weight = 1;
/// <inheritdoc />
public void ApplyStep(ModuleApplyStepEventArgs e)
{
if (SelectedMode == LayerDiffuseMode.None)
return;
var sdType = SelectedMode switch
{
LayerDiffuseMode.GenerateForegroundWithTransparencySD15 => "SD15",
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL",
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(),
_ => throw new ArgumentOutOfRangeException()
};
// Choose config based on mode
var config = SelectedMode switch
{
LayerDiffuseMode.GenerateForegroundWithTransparencySD15
=> "SD15, Attention Injection, attn_sharing",
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL, Conv Injection",
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(),
_ => throw new ArgumentOutOfRangeException()
};
foreach (var modelConnections in e.Temp.Models.Values)
{
var layerDiffuseApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LayeredDiffusionApply
{
Name = e.Nodes.GetUniqueName($"LayerDiffuseApply_{modelConnections.Name}"),
Model = modelConnections.Model,
Config = config,
Weight = Weight,
}
);
modelConnections.Model = layerDiffuseApply.Output;
}
// Add pre output action
e.PreOutputActions.Add(applyArgs =>
{
// Use last latent for decode
var latent =
applyArgs.Builder.Connections.LastPrimaryLatent
?? throw new InvalidOperationException("Connections.LastPrimaryLatent not set");
// Convert primary to image if not already
var primaryImage = applyArgs.Builder.GetPrimaryAsImage();
applyArgs.Builder.Connections.Primary = primaryImage;
// Add a Layer Diffuse Decode
var decode = applyArgs.Nodes.AddTypedNode(
new ComfyNodeBuilder.LayeredDiffusionDecodeRgba
{
Name = applyArgs.Nodes.GetUniqueName("LayerDiffuseDecode"),
Samples = latent,
Images = primaryImage,
SdVersion = sdType
}
);
// Set primary to decode output
applyArgs.Builder.Connections.Primary = decode.Output;
});
}
}

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

@ -6,6 +6,9 @@ using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
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;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
@ -39,7 +42,7 @@ public class ControlNetModule : ModuleBase
{
var card = GetCard<ControlNetCardViewModel>();
var imageLoad = e.Nodes.AddTypedNode(
var image = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage
{
Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"),
@ -47,7 +50,101 @@ public class ControlNetModule : ModuleBase
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? 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(
new ComfyNodeBuilder.ControlNetLoader
@ -62,36 +159,36 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("ControlNetApply"),
Image = imageLoad.Output1,
Image = image,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
Positive = e.Temp.Base.Conditioning!.Unwrap().Positive,
Negative = e.Temp.Base.Conditioning.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
}
);
e.Temp.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
e.Temp.Base.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
// Refiner if available
if (e.Temp.RefinerConditioning is not null)
if (e.Temp.Refiner.Conditioning is not null)
{
var controlNetRefinerApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"),
Image = imageLoad.Output1,
Image = image,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.RefinerConditioning.Positive,
Negative = e.Temp.RefinerConditioning.Negative,
Positive = e.Temp.Refiner.Conditioning!.Unwrap().Positive,
Negative = e.Temp.Refiner.Conditioning.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
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
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? 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 = builder.Connections.GetRefinerOrBaseConditioning().Positive,
Negative = builder.Connections.GetRefinerOrBaseConditioning().Negative,
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
// If we need to inherit primary sampler addons, use their temp args
if (samplerCard.InheritPrimarySamplerAddons)
{
e.Temp = e.Builder.Connections.BaseSamplerTemporaryArgs ?? e.CreateTempFromBuilder();
}
else
{
// otherwise just use new ones
e.Temp = e.CreateTempFromBuilder();
}
var hiresSampler = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? 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
builder.Connections.Primary = hiresSampler.Output;

26
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LayerDiffuseModule.cs

@ -0,0 +1,26 @@
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
[ManagedService]
[Transient]
public class LayerDiffuseModule : ModuleBase
{
/// <inheritdoc />
public LayerDiffuseModule(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory)
{
Title = "Layer Diffuse";
AddCards(vmFactory.Get<LayerDiffuseCardViewModel>());
}
/// <inheritdoc />
protected override void OnApplyStep(ModuleApplyStepEventArgs e)
{
var card = GetCard<LayerDiffuseCardViewModel>();
card.ApplyStep(e);
}
}

30
StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs

@ -20,6 +20,7 @@ using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -30,6 +31,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep
{
private readonly IModelIndexService modelIndexService;
private readonly ISettingsManager settingsManager;
/// <summary>
/// Cache of prompt text to tokenized Prompt
@ -48,6 +50,9 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
[ObservableProperty]
private bool isAutoCompletionEnabled;
[ObservableProperty]
private bool isHelpButtonTeachingTipOpen;
/// <inheritdoc />
public PromptCardViewModel(
ICompletionProvider completionProvider,
@ -59,6 +64,7 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
)
{
this.modelIndexService = modelIndexService;
this.settingsManager = settingsManager;
CompletionProvider = completionProvider;
TokenizerProvider = tokenizerProvider;
SharedState = sharedState;
@ -77,6 +83,30 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
);
}
partial void OnIsHelpButtonTeachingTipOpenChanging(bool oldValue, bool newValue)
{
// If the teaching tip is being closed, save the setting
if (oldValue && !newValue)
{
settingsManager.Transaction(settings =>
{
settings.SeenTeachingTips.Add(TeachingTip.InferencePromptHelpButtonTip);
});
}
}
/// <inheritdoc />
public override void OnLoaded()
{
base.OnLoaded();
// Show teaching tip for help button if not seen
if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.InferencePromptHelpButtonTip))
{
IsHelpButtonTeachingTipOpen = true;
}
}
/// <summary>
/// Applies the prompt step.
/// Requires:

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

@ -87,6 +87,11 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
[Required]
private ComfyScheduler? selectedScheduler = ComfyScheduler.Normal;
[ObservableProperty]
[property: Category("Settings")]
[property: DisplayName("Inherit Primary Sampler Addons")]
private bool inheritPrimarySamplerAddons = true;
[JsonPropertyName("Modules")]
public StackEditableCardViewModel ModulesCardViewModel { get; }
@ -104,7 +109,12 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
ModulesCardViewModel = vmFactory.Get<StackEditableCardViewModel>(modulesCard =>
{
modulesCard.Title = Resources.Label_Addons;
modulesCard.AvailableModules = [typeof(FreeUModule), typeof(ControlNetModule)];
modulesCard.AvailableModules =
[
typeof(FreeUModule),
typeof(ControlNetModule),
typeof(LayerDiffuseModule)
];
});
}
@ -130,8 +140,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
// Provide temp values
e.Temp.Conditioning = e.Builder.Connections.Base.Conditioning;
e.Temp.RefinerConditioning = e.Builder.Connections.Refiner.Conditioning;
e.Temp = e.CreateTempFromBuilder();
// Apply steps from our addons
ApplyAddonSteps(e);
@ -142,17 +151,26 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
if (!e.Nodes.ContainsKey("Sampler"))
{
ApplyStepsInitialSampler(e);
// Save temp
e.Builder.Connections.BaseSamplerTemporaryArgs = e.Temp;
}
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)
{
// 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
var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
@ -162,8 +180,8 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
e.Builder.Connections.PrimaryScheduler = primaryScheduler;
// Use Temp Conditioning that may be modified by addons
var conditioning = e.Temp.Conditioning.Unwrap();
var refinerConditioning = e.Temp.RefinerConditioning;
var conditioning = e.Temp.Base.Conditioning.Unwrap();
var refinerConditioning = e.Temp.Refiner.Conditioning;
// Use custom sampler if SDTurbo scheduler is selected
if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo)
@ -209,21 +227,16 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
);
e.Builder.Connections.Primary = sampler.Output1;
return;
}
// 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
var sampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(),
Model = e.Temp.Base.Model!.Unwrap(),
Seed = e.Builder.Connections.Seed,
SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name,
@ -245,7 +258,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(),
Model = e.Temp.Base.Model!.Unwrap(),
AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
@ -261,8 +274,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
var refinerSampler = e.Nodes.AddTypedNode(
e.Builder.Connections.Primary = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler_Refiner",
@ -276,19 +311,15 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
Positive = refinerConditioning!.Positive,
Negative = refinerConditioning.Negative,
// Connect to previous sampler
LatentImage = sampler.Output,
LatentImage = e.Builder.GetPrimaryAsLatent(),
StartAtStep = Steps,
EndAtStep = TotalSteps,
ReturnWithLeftoverNoise = false
}
);
e.Builder.Connections.Primary = refinerSampler.Output;
).Output;
}
}
private void ApplyStepsAdditionalSampler(ModuleApplyStepEventArgs e) { }
/// <summary>
/// Applies each step of our addons
/// </summary>

144
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

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

173
StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs

@ -0,0 +1,173 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
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.Api.OpenArt;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(InstalledWorkflowsPage))]
[Singleton]
public partial class InstalledWorkflowsViewModel(
ISettingsManager settingsManager,
INotificationService notificationService
) : TabViewModelBase, IDisposable
{
public override string Header => Resources.TabLabel_InstalledWorkflows;
private readonly SourceCache<OpenArtMetadata, string> workflowsCache =
new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString());
[ObservableProperty]
private IObservableCollection<OpenArtMetadata> displayedWorkflows =
new ObservableCollectionExtended<OpenArtMetadata>();
protected override async Task OnInitialLoadedAsync()
{
await base.OnInitialLoadedAsync();
workflowsCache.Connect().DeferUntilLoaded().Bind(DisplayedWorkflows).Subscribe();
if (Design.IsDesignMode)
return;
await LoadInstalledWorkflowsAsync();
EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled;
}
[RelayCommand]
private async Task LoadInstalledWorkflowsAsync()
{
workflowsCache.Clear();
if (!Directory.Exists(settingsManager.WorkflowDirectory))
{
Directory.CreateDirectory(settingsManager.WorkflowDirectory);
}
foreach (
var workflowPath in Directory.EnumerateFiles(
settingsManager.WorkflowDirectory,
"*.json",
SearchOption.AllDirectories
)
)
{
try
{
var json = await File.ReadAllTextAsync(workflowPath);
var metadata = JsonSerializer.Deserialize<OpenArtMetadata>(json);
if (metadata?.Workflow == null)
{
metadata = new OpenArtMetadata
{
Workflow = new OpenArtSearchResult
{
Id = Guid.NewGuid().ToString(),
Name = Path.GetFileNameWithoutExtension(workflowPath)
}
};
}
metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)];
workflowsCache.AddOrUpdate(metadata);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
[RelayCommand]
private async Task OpenInExplorer(OpenArtMetadata metadata)
{
if (metadata.FilePath == null)
return;
var path = metadata.FilePath.FirstOrDefault()?.Path.ToString();
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFileBrowser(path);
}
[RelayCommand]
private void OpenOnOpenArt(OpenArtMetadata metadata)
{
if (metadata.Workflow == null)
return;
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}");
}
[RelayCommand]
private async Task DeleteAsync(OpenArtMetadata metadata)
{
var confirmationDialog = new BetterContentDialog
{
Title = Resources.Label_AreYouSure,
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
await using var delay = new MinimumDelay(200, 500);
var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", "");
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
await notificationService.TryAsync(
Task.Run(() => File.Delete(path)),
message: "Error deleting workflow"
);
var id = metadata?.Workflow?.Id;
if (!string.IsNullOrWhiteSpace(id))
{
workflowsCache.Remove(id);
}
}
notificationService.Show(
Resources.Label_WorkflowDeleted,
string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name)
);
}
private void OnWorkflowInstalled(object? sender, EventArgs e)
{
LoadInstalledWorkflowsAsync().SafeFireAndForget();
}
public void Dispose()
{
workflowsCache.Dispose();
EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled;
}
}

33
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
@ -36,6 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IDiscordRichPresenceService discordRichPresenceService;
private readonly IModelIndexService modelIndexService;
private readonly Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler;
public string Greeting => "Welcome to Avalonia!";
[ObservableProperty]
@ -71,7 +73,8 @@ public partial class MainWindowViewModel : ViewModelBase
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler
)
{
this.settingsManager = settingsManager;
@ -79,6 +82,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService;
this.modelIndexService = modelIndexService;
this.modelDownloadLinkHandler = modelDownloadLinkHandler;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
}
@ -107,6 +111,25 @@ public partial class MainWindowViewModel : ViewModelBase
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)
discordRichPresenceService.UpdateState();
@ -139,14 +162,6 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new NewOneClickInstallDialog { DataContext = viewModel },
};
EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{
if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded();
};
var firstDialogResult = await dialog.ShowAsync(App.TopLevel);
if (firstDialogResult != ContentDialogResult.Primary)

3
StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs

@ -14,6 +14,7 @@ using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -47,7 +48,7 @@ public partial class NewCheckpointsPageViewModel(
IMetadataImportService metadataImportService
) : PageViewModelBase
{
public override string Title => "Checkpoint Manager";
public override string Title => Resources.Label_CheckpointManager;
public override IconSource IconSource =>
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 DynamicData;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -18,7 +19,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Singleton]
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 IReadOnlyList<PageViewModelBase> SubPages { get; }
@ -58,6 +59,10 @@ public partial class NewPackageManagerViewModel : PageViewModelBase
{
CurrentPagePath.Add(value);
}
else if (value is RunningPackageViewModel)
{
CurrentPagePath.Add(value);
}
else
{
CurrentPagePath.Clear();

348
StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs

@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Resources = StabilityMatrix.Avalonia.Languages.Resources;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OpenArtBrowserPage))]
[Singleton]
public partial class OpenArtBrowserViewModel(
IOpenArtApi openArtApi,
INotificationService notificationService,
ISettingsManager settingsManager,
IPackageFactory packageFactory,
ServiceManager<ViewModelBase> vmFactory
) : TabViewModelBase, IInfinitelyScroll
{
private const int PageSize = 20;
public override string Header => Resources.Label_OpenArtBrowser;
private readonly SourceCache<OpenArtSearchResult, string> searchResultsCache = new(x => x.Id);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))]
private OpenArtSearchResponse? latestSearchResponse;
[ObservableProperty]
private IObservableCollection<OpenArtSearchResult> searchResults =
new ObservableCollectionExtended<OpenArtSearchResult>();
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))]
private int displayedPageNumber = 1;
public int InternalPageNumber => DisplayedPageNumber - 1;
public int PageCount =>
Math.Max(
1,
Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize)))
);
public bool CanGoBack =>
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0;
public bool CanGoForward =>
!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1;
public bool CanGoToEnd =>
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1;
public IEnumerable<string> AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"];
[ObservableProperty]
private string? selectedSortMode;
protected override void OnInitialLoaded()
{
searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe();
SelectedSortMode = AllSortModes.First();
}
[RelayCommand]
private async Task FirstPage()
{
DisplayedPageNumber = 1;
searchResultsCache.Clear();
await DoSearch();
}
[RelayCommand]
private async Task PreviousPage()
{
DisplayedPageNumber--;
searchResultsCache.Clear();
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task NextPage()
{
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
DisplayedPageNumber++;
}
searchResultsCache.Clear();
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task LastPage()
{
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
DisplayedPageNumber = PageCount;
}
searchResultsCache.Clear();
await DoSearch(PageCount - 1);
}
[Localizable(false)]
[RelayCommand]
private void OpenModel(OpenArtSearchResult workflow)
{
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}");
}
[RelayCommand]
private async Task SearchButton()
{
DisplayedPageNumber = 1;
LatestSearchResponse = null;
searchResultsCache.Clear();
await DoSearch();
}
[RelayCommand]
private async Task OpenWorkflow(OpenArtSearchResult workflow)
{
var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow };
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = true,
IsSecondaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Import,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsFooterVisible = true,
MaxDialogWidth = 750,
MaxDialogHeight = 850,
CloseOnClickOutside = true,
Content = vm
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary)
return;
List<IPackageStep> steps =
[
new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager)
];
// Add install steps if missing nodes and preferred
if (
vm is
{
InstallRequiredNodes: true,
MissingNodes: { Count: > 0 } missingNodes,
SelectedPackage: not null,
SelectedPackagePair: not null
}
)
{
var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!;
steps.AddRange(
missingNodes.Select(
extension =>
new InstallExtensionStep(
extensionManager,
vm.SelectedPackagePair.InstalledPackage,
extension
)
)
);
}
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
ModificationCompleteTitle = Resources.Label_WorkflowImported,
ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);
notificationService.Show(
Resources.Label_WorkflowImported,
Resources.Label_WorkflowImportComplete,
NotificationType.Success
);
EventManager.Instance.OnWorkflowInstalled();
}
[RelayCommand]
private void OpenOnOpenArt(OpenArtSearchResult? workflow)
{
if (workflow?.Id == null)
return;
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}");
}
private async Task DoSearch(int page = 0)
{
IsLoading = true;
try
{
OpenArtSearchResponse? response = null;
if (string.IsNullOrWhiteSpace(SearchQuery))
{
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) };
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
request.Cursor = LatestSearchResponse.NextCursor;
}
response = await openArtApi.GetFeedAsync(request);
}
else
{
response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = page
}
);
}
foreach (var item in response.Items)
{
searchResultsCache.AddOrUpdate(item);
}
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message);
}
finally
{
IsLoading = false;
}
}
partial void OnSelectedSortModeChanged(string? value)
{
if (value is null || SearchResults.Count == 0)
return;
searchResultsCache.Clear();
LatestSearchResponse = null;
DoSearch().SafeFireAndForget();
}
public async Task LoadNextPageAsync()
{
if (!CanGoForward)
return;
try
{
OpenArtSearchResponse? response = null;
if (string.IsNullOrWhiteSpace(SearchQuery))
{
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) };
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
request.Cursor = LatestSearchResponse.NextCursor;
}
response = await openArtApi.GetFeedAsync(request);
}
else
{
DisplayedPageNumber++;
response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = InternalPageNumber
}
);
}
foreach (var item in response.Items)
{
searchResultsCache.AddOrUpdate(item);
}
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show("Unable to load the next page", e.Message);
}
}
private static string GetSortMode(string? sortMode)
{
return sortMode switch
{
"Trending" => "trending",
"Latest" => "latest",
"Most Downloaded" => "most_downloaded",
"Most Liked" => "most_liked",
_ => "trending"
};
}
}

188
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
@ -28,8 +29,6 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
@ -52,19 +51,21 @@ public partial class OutputsPageViewModel : PageViewModelBase
private readonly INotificationService notificationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ILogger<OutputsPageViewModel> logger;
private readonly List<CancellationTokenSource> cancellationTokenSources = [];
public override string Title => Resources.Label_OutputsPageTitle;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
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; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
public IObservableCollection<PackageOutputCategory> Categories { get; set; } =
new ObservableCollectionExtended<PackageOutputCategory>();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
@ -86,6 +87,15 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
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 string NumImagesSelected =>
@ -95,6 +105,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
private string[] allowedExtensions = [".png", ".webp"];
private PackageOutputCategory? lastOutputCategory;
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
@ -113,7 +125,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
// Observable predicate from SearchQuery changes
var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))!
.Throttle(TimeSpan.FromMilliseconds(100))!
.Select(property => searcher.GetPredicate(property.Value))
.AsObservable();
@ -122,28 +134,37 @@ public partial class OutputsPageViewModel : PageViewModelBase
.DeferUntilLoaded()
.Filter(searchPredicate)
.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)
.WhenPropertyChanged(p => p.IsSelected)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
categoriesCache.Connect().DeferUntilLoaded().Bind(Categories).Subscribe();
settingsManager.RelayPropertyFor(
this,
vm => vm.ImageSize,
settings => settings.OutputsImageSize,
delay: TimeSpan.FromMilliseconds(250)
);
}
protected override void OnInitialLoaded()
{
RefreshCategories();
settingsManager.RelayPropertyFor(
this,
vm => vm.ShowFolders,
settings => settings.IsOutputsTreeViewEnabled,
true
);
}
public override void OnLoaded()
protected override void OnInitialLoaded()
{
if (Design.IsDesignMode)
return;
@ -151,14 +172,17 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (!settingsManager.IsLibraryDirSet)
return;
base.OnLoaded();
Directory.CreateDirectory(settingsManager.ImagesDirectory);
RefreshCategories();
SelectedCategory ??= Categories.First();
SelectedOutputType ??= SharedOutputType.All;
SearchQuery = string.Empty;
ImageSize = settingsManager.Settings.OutputsImageSize;
lastOutputCategory = SelectedCategory;
IsChangingCategory = true;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
@ -169,7 +193,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
partial void OnSelectedCategoryChanged(PackageOutputCategory? oldValue, PackageOutputCategory? newValue)
{
if (oldValue == newValue || newValue == null)
if (oldValue == newValue || oldValue == null || newValue == null)
return;
var path =
@ -177,11 +201,12 @@ public partial class OutputsPageViewModel : PageViewModelBase
? Path.Combine(newValue.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
lastOutputCategory = newValue;
}
partial void OnSelectedOutputTypeChanged(SharedOutputType? oldValue, SharedOutputType? newValue)
{
if (oldValue == newValue || newValue == null)
if (oldValue == newValue || oldValue == null || newValue == null)
return;
var path =
@ -262,8 +287,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
public void Refresh()
{
Dispatcher.UIThread.Post(() => RefreshCategories());
Dispatcher.UIThread.Post(OnLoaded);
Dispatcher.UIThread.Post(RefreshCategories);
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
public async Task DeleteImage(OutputImageViewModel? item)
@ -273,8 +303,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
var confirmationDialog = new BetterContentDialog
{
Title = "Are you sure you want to delete this image?",
Content = "This action cannot be undone.",
Title = Resources.Label_AreYouSure,
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
@ -352,8 +382,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
{
var confirmationDialog = new BetterContentDialog
{
Title = $"Are you sure you want to delete {NumItemsSelected} images?",
Content = "This action cannot be undone.",
Title = string.Format(Resources.Label_AreYouSureDeleteImages, NumItemsSelected),
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
@ -365,12 +395,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
var selected = Outputs.Where(o => o.IsSelected).ToList();
Debug.Assert(selected.Count == NumItemsSelected);
var imagesToRemove = new List<LocalImageFile>();
foreach (var output in selected)
{
// Delete the file
var imageFile = new FilePath(output.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
continue;
@ -383,15 +414,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
await notificationService.TryAsync(sideCar.DeleteAsync());
}
OutputsCache.Remove(output.ImageFile);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
imagesToRemove.Add(output.ImageFile);
}
OutputsCache.Remove(imagesToRemove);
NumItemsSelected = 0;
ClearSelection();
}
@ -491,7 +517,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
}
}
OnLoaded();
Refresh();
IsConsolidating = false;
}
@ -517,20 +543,58 @@ public partial class OutputsPageViewModel : PageViewModelBase
return;
}
var files = Directory
.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(path => allowedExtensions.Contains(new FilePath(path).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
if (files.Count == 0)
if (lastOutputCategory?.Path.Equals(directory) is not true)
{
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()
@ -559,7 +623,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
pair.InstalledPackage.FullPath!,
pair.BasePackage.OutputFolderName
),
Name = pair.InstalledPackage.DisplayName ?? ""
Name = pair.InstalledPackage.DisplayName ?? "",
SubDirectories = GetSubfolders(
Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName)
)
}
)
.ToList();
@ -569,18 +636,37 @@ public partial class OutputsPageViewModel : PageViewModelBase
new PackageOutputCategory
{
Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder"
Name = "Shared Output Folder",
SubDirectories = GetSubfolders(settingsManager.ImagesDirectory)
}
);
packageCategories.Insert(
1,
new PackageOutputCategory { Path = settingsManager.ImagesInferenceDirectory, Name = "Inference" }
);
categoriesCache.EditDiff(packageCategories, (a, b) => a.Path == b.Path);
SelectedCategory = previouslySelectedCategory ?? Categories.First();
}
private ObservableCollection<PackageOutputCategory> GetSubfolders(string strPath)
{
var subfolders = new ObservableCollection<PackageOutputCategory>();
if (!Directory.Exists(strPath))
return subfolders;
var directories = Directory.EnumerateDirectories(strPath, "*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
var category = new PackageOutputCategory { Name = Path.GetFileName(dir), Path = dir };
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
if (Directory.GetDirectories(dir, "*", SearchOption.TopDirectoryOnly).Length > 0)
{
category.SubDirectories = GetSubfolders(dir);
}
subfolders.Add(category);
}
SelectedCategory =
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
return subfolders;
}
}

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

@ -1,11 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
@ -26,7 +31,6 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
@ -34,20 +38,23 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
[ManagedService]
[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 readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
private string webUiUrl = string.Empty;
[ObservableProperty]
private InstalledPackage? package;
[ObservableProperty]
private string? cardImageSource;
private Uri? cardImageSource;
[ObservableProperty]
private bool isUpdateAvailable;
@ -82,21 +89,31 @@ public partial class PackageCardViewModel : ProgressViewModel
[ObservableProperty]
private bool canUseExtensions;
public PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService<MainWindowViewModel> navigationService,
ServiceManager<ViewModelBase> vmFactory
)
[ObservableProperty]
private bool isRunning;
[ObservableProperty]
private bool showWebUiButton;
private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
this.logger = logger;
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.navigationService = navigationService;
this.vmFactory = vmFactory;
if (runningPackageService.RunningPackages.Select(x => x.Value) is not { } runningPackages)
return;
var runningViewModel = runningPackages.FirstOrDefault(
x => x.RunningPackage.InstalledPackage.Id == Package?.Id
);
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)
@ -110,7 +127,7 @@ public partial class PackageCardViewModel : ProgressViewModel
)
{
IsUnknownPackage = true;
CardImageSource = "";
CardImageSource = null;
InstalledVersion = "Unknown";
}
else
@ -118,7 +135,7 @@ public partial class PackageCardViewModel : ProgressViewModel
IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString();
CardImageSource = basePackage?.PreviewImageUri ?? Assets.NoImage;
InstalledVersion = value.Version?.DisplayVersion ?? "Unknown";
CanUseConfigMethod =
basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false;
@ -127,11 +144,30 @@ public partial class PackageCardViewModel : ProgressViewModel
UseSharedOutput = Package?.UseSharedOutputFolder ?? false;
CanUseSharedOutput = basePackage?.SharedOutputFolders != null;
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()
{
if (Design.IsDesignMode && Package?.DisplayName == "Running Comfy")
{
IsRunning = true;
IsUpdateAvailable = true;
ShowWebUiButton = true;
}
if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet || Package is not { } currentPackage)
return;
@ -160,18 +196,122 @@ public partial class PackageCardViewModel : ProgressViewModel
}
IsUpdateAvailable = await HasUpdate();
if (
Package != null
&& !IsRunning
&& runningPackageService.RunningPackages.TryGetValue(Package.Id, out var runningPackageVm)
)
{
IsRunning = true;
runningPackageVm.RunningPackage.BasePackage.Exited += BasePackageOnExited;
runningPackageVm.RunningPackage.BasePackage.StartupComplete +=
RunningPackageOnStartupComplete;
webUiUrl = runningPackageVm.WebUiUrl;
ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl);
}
}
}
public override void OnUnloaded()
{
EventManager.Instance.PackageRelaunchRequested -= InstanceOnPackageRelaunchRequested;
runningPackageService.RunningPackages.CollectionChanged -= RunningPackagesOnCollectionChanged;
}
public async Task Launch()
{
if (Package == null)
return;
var packagePair = await runningPackageService.StartPackage(Package);
if (packagePair != null)
{
IsRunning = true;
packagePair.BasePackage.Exited += BasePackageOnExited;
packagePair.BasePackage.StartupComplete += RunningPackageOnStartupComplete;
var vm = runningPackageService.GetRunningPackageViewModel(packagePair.InstalledPackage.Id);
if (vm != null)
{
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
// settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
//
// navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition());
// EventManager.Instance.OnPackageLaunchRequested(Package.Id);
}
public void Launch()
public void NavToConsole()
{
if (Package == null)
return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
var vm = runningPackageService.GetRunningPackageViewModel(Package.Id);
if (vm != null)
{
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
public void LaunchWebUi()
{
if (string.IsNullOrEmpty(webUiUrl))
return;
navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition());
EventManager.Instance.OnPackageLaunchRequested(Package.Id);
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);
}
IsRunning = false;
ShowWebUiButton = false;
})
.SafeFireAndForget();
}
public async Task Uninstall()
@ -286,6 +426,21 @@ public partial class PackageCardViewModel : ProgressViewModel
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(
settingsManager,
Package,
@ -439,6 +594,64 @@ public partial class PackageCardViewModel : ProgressViewModel
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()
{
if (Package == null || IsUnknownPackage || Design.IsDesignMode)
@ -569,4 +782,10 @@ public partial class PackageCardViewModel : ProgressViewModel
IsSharedModelConfig = false;
}
}
private void RunningPackageOnStartupComplete(object? sender, string e)
{
webUiUrl = e.Replace("0.0.0.0", "127.0.0.1");
ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl);
}
}

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

@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.PackageSteps;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -85,6 +86,9 @@ public partial class PackageInstallDetailViewModel(
[ObservableProperty]
private GitCommit? selectedCommit;
[ObservableProperty]
private bool isOutputSharingEnabled = true;
[ObservableProperty]
private bool canInstall;
@ -97,6 +101,8 @@ public partial class PackageInstallDetailViewModel(
OnInstallNameChanged(InstallName);
CanInstall = false;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
@ -183,6 +189,7 @@ public partial class PackageInstallDetailViewModel(
}
var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, SelectedPackage);
var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(Path.Combine(installLocation, "venv"));
var downloadOptions = new DownloadPackageVersionOptions();
var installedVersion = new InstalledPackageVersion();
@ -224,6 +231,8 @@ public partial class PackageInstallDetailViewModel(
installLocation
);
var setupOutputSharingStep = new SetupOutputSharingStep(SelectedPackage, installLocation);
var package = new InstalledPackage
{
DisplayName = InstallName,
@ -234,7 +243,8 @@ public partial class PackageInstallDetailViewModel(
LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = SelectedTorchVersion,
PreferredSharedFolderMethod = SelectedSharedFolderMethod
PreferredSharedFolderMethod = SelectedSharedFolderMethod,
UseSharedOutputFolder = IsOutputSharingEnabled
};
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package);
@ -244,11 +254,17 @@ public partial class PackageInstallDetailViewModel(
setPackageInstallingStep,
prereqStep,
downloadStep,
unpackSiteCustomizeStep,
installStep,
setupModelFoldersStep,
addInstalledPackageStep
};
if (IsOutputSharingEnabled)
{
steps.Insert(steps.IndexOf(addInstalledPackageStep), setupOutputSharingStep);
}
var packageName = SelectedPackage.Name;
var runner = new PackageModificationRunner
@ -281,7 +297,7 @@ public partial class PackageInstallDetailViewModel(
SelectedVersion = !IsReleaseMode
? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch)
?? AvailableVersions?.FirstOrDefault()
: AvailableVersions?.FirstOrDefault();
: AvailableVersions?.FirstOrDefault(v => !v.IsPrerelease);
CanInstall = !ShowDuplicateWarning;
}

17
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -6,8 +6,6 @@ using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
@ -15,18 +13,15 @@ using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
@ -48,7 +43,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
private readonly INavigationService<NewPackageManagerViewModel> packageNavigationService;
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 };
/// <summary>
@ -84,6 +79,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
this.logger = logger;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished;
var installed = installedPackages.Connect();
var unknown = unknownInstalledPackages.Connect();
@ -107,6 +103,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
timer.Tick += async (_, _) => await CheckPackagesForUpdates();
}
private void OnOneClickInstallFinished(object? sender, bool e)
{
OnLoadedAsync().SafeFireAndForget();
}
public void SetPackages(IEnumerable<InstalledPackage> packages)
{
installedPackages.Edit(s => s.Load(packages));
@ -119,7 +120,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
public override async Task OnLoadedAsync()
{
if (Design.IsDesignMode)
if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet)
return;
installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer);

15
StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs

@ -49,10 +49,21 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
Name = packageModificationRunner.CurrentStep?.ProgressTitle;
Failed = packageModificationRunner.Failed;
if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Contains("Downloading..."))
if (e.ProcessOutput == null && string.IsNullOrWhiteSpace(e.Message))
return;
Progress.Console.PostLine(e.Message);
if (!string.IsNullOrWhiteSpace(e.Message) && e.Message.Contains("Downloading..."))
return;
if (e.ProcessOutput != null)
{
Progress.Console.Post(e.ProcessOutput.Value);
}
else
{
Progress.Console.PostLine(e.Message);
}
EventManager.Instance.OnScrollToBottomRequested();
if (

169
StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs

@ -0,0 +1,169 @@
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 async Task Restart()
{
await Stop();
await Task.Delay(100);
LaunchPackage();
}
[RelayCommand]
private void LaunchPackage()
{
EventManager.Instance.OnPackageRelaunchRequested(RunningPackage.InstalledPackage);
}
[RelayCommand]
private async Task Stop()
{
IsRunning = false;
await runningPackageService.StopPackage(RunningPackage.InstalledPackage.Id);
Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}");
await Console.StopUpdatesAsync();
}
[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();
}
}

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

@ -14,6 +14,7 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
@ -129,6 +130,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ObservableProperty]
private HolidayMode holidayModeSetting;
[ObservableProperty]
private bool infinitelyScrollWorkflowBrowser;
#region System Info
private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } =
@ -217,6 +221,13 @@ public partial class MainSettingsViewModel : PageViewModelBase
settings => settings.HolidayModeSetting
);
settingsManager.RelayPropertyFor(
this,
vm => vm.InfinitelyScrollWorkflowBrowser,
settings => settings.IsWorkflowInfiniteScrollEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick;
@ -354,7 +365,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current = settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
var current = settingsManager.Settings.UserEnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
@ -374,7 +385,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
.EnvVars.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
settingsManager.Transaction(s => s.EnvironmentVariables = newEnvVars);
settingsManager.Transaction(s => s.UserEnvironmentVariables = newEnvVars);
}
}
@ -774,7 +785,10 @@ public partial class MainSettingsViewModel : PageViewModelBase
[
new CommandItem(DebugFindLocalModelFromIndexCommand),
new CommandItem(DebugExtractDmgCommand),
new CommandItem(DebugShowNativeNotificationCommand)
new CommandItem(DebugShowNativeNotificationCommand),
new CommandItem(DebugClearImageCacheCommand),
new CommandItem(DebugGCCollectCommand),
new CommandItem(DebugExtractImagePromptsToTxtCommand)
];
[RelayCommand]
@ -889,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
#region Info Section

39
StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(WorkflowsPage))]
[Singleton]
public partial class WorkflowsPageViewModel : PageViewModelBase
{
public override string Title => Resources.Label_Workflows;
public override IconSource IconSource => new FASymbolIconSource { Symbol = "fa-solid fa-circle-nodes" };
public IReadOnlyList<TabItem> Pages { get; }
[ObservableProperty]
private TabItem? selectedPage;
/// <inheritdoc/>
public WorkflowsPageViewModel(
OpenArtBrowserViewModel openArtBrowserViewModel,
InstalledWorkflowsViewModel installedWorkflowsViewModel
)
{
Pages = new List<TabItem>(
new List<TabViewModelBase>([openArtBrowserViewModel, installedWorkflowsViewModel]).Select(
vm => new TabItem { Header = vm.Header, Content = vm }
)
);
SelectedPage = Pages.FirstOrDefault();
}
}

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

Loading…
Cancel
Save