Browse Source

Merge branch 'dev' into headless-kohya

pull/629/head
JT 8 months ago committed by GitHub
parent
commit
0409e0354d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.Gif/Avalonia.Gif.csproj
  2. 29
      CHANGELOG.md
  3. 51
      README.md
  4. 6
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  5. 7
      StabilityMatrix.Avalonia/App.axaml
  6. 22
      StabilityMatrix.Avalonia/App.axaml.cs
  7. 208
      StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs
  8. 6
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  9. 8
      StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml
  10. 91
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  11. 144
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  12. 48
      StabilityMatrix.Avalonia/Languages/Resources.resx
  13. 8
      StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs
  14. 3
      StabilityMatrix.Avalonia/Models/ImageSource.cs
  15. 9
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  16. 10
      StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs
  17. 22
      StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs
  18. 8
      StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs
  19. 248
      StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs
  20. 40
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  21. 29
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  22. 20
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  23. 178
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  24. 7
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  25. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs
  26. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  27. 185
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs
  28. 6
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs
  29. 19
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  30. 168
      StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs
  31. 25
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  32. 348
      StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
  33. 12
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  34. 6
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  35. 2
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  36. 10
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  37. 39
      StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs
  38. 26
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  39. 93
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  40. 16
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  41. 39
      StabilityMatrix.Avalonia/Views/Dialogs/NewOneClickInstallDialog.axaml
  42. 136
      StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml
  43. 13
      StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml.cs
  44. 4
      StabilityMatrix.Avalonia/Views/Dialogs/RecommendedModelsDialog.axaml
  45. 254
      StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml
  46. 13
      StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs
  47. 4
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  48. 360
      StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml
  49. 41
      StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs
  50. 4
      StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml
  51. 17
      StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml
  52. 5
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  53. 202
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
  54. 12
      StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml
  55. 13
      StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs
  56. 17
      StabilityMatrix.Core/Api/IOpenArtApi.cs
  57. 3
      StabilityMatrix.Core/Helper/EventManager.cs
  58. 23
      StabilityMatrix.Core/Helper/Utilities.cs
  59. 14
      StabilityMatrix.Core/Models/Api/CivitMetadata.cs
  60. 7
      StabilityMatrix.Core/Models/Api/CivitModel.cs
  61. 5
      StabilityMatrix.Core/Models/Api/CivitModelStats.cs
  62. 21
      StabilityMatrix.Core/Models/Api/CivitModelVersion.cs
  63. 60
      StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs
  64. 11
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
  65. 15
      StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs
  66. 24
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs
  67. 14
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs
  68. 15
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs
  69. 12
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs
  70. 21
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs
  71. 18
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs
  72. 15
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs
  73. 33
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs
  74. 33
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs
  75. 15
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs
  76. 8420
      StabilityMatrix.Core/Models/ComfyNodeMap.cs
  77. 12
      StabilityMatrix.Core/Models/PackageDifficulty.cs
  78. 36
      StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs
  79. 2
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  80. 2
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  81. 2
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  82. 8
      StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs
  83. 2
      StabilityMatrix.Core/Models/Packages/SDWebForge.cs
  84. 14
      StabilityMatrix.Core/Models/Settings/Settings.cs
  85. 1
      StabilityMatrix.Core/Services/ISettingsManager.cs
  86. 1
      StabilityMatrix.Core/Services/SettingsManager.cs
  87. 12
      StabilityMatrix.Core/StabilityMatrix.Core.csproj
  88. 8
      StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
  89. 8
      StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

2
Avalonia.Gif/Avalonia.Gif.csproj

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

29
CHANGELOG.md

@ -5,6 +5,34 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.10.0-preview.1
### Added
- Added OpenArt.AI workflow browser for ComfyUI workflows
### Changed
- Changed to a new image control for pages with many images
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Improved performance when deleting many images from the Outputs page
## 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 ## v2.10.0-dev.2
### Added ### 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. - 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.
@ -82,6 +110,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 - Added copy image support on linux and macOS for Inference outputs viewer menu
### Fixed ### Fixed
- Fixed StableSwarmUI not installing properly on macOS - 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 - 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 default command line args for SDWebUI Forge on macOS
- Fixed output paths and output sharing for SDWebUI Forge - Fixed output paths and output sharing for SDWebUI Forge

51
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) [![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) [![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 [release]: https://github.com/LykosAI/StabilityMatrix/releases/latest
[download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip [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 [download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg
[auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui [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 [webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux
[comfy]: https://github.com/comfyanonymous/ComfyUI [comfy]: https://github.com/comfyanonymous/ComfyUI
[sdnext]: https://github.com/vladmandic/automatic [sdnext]: https://github.com/vladmandic/automatic
@ -25,44 +26,29 @@
[civitai]: https://civitai.com/ [civitai]: https://civitai.com/
[huggingface]: https://huggingface.co/ [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 ### 🖱 One click install and update for Stable Diffusion Web UI Packages
- Supports: - Supports:
- [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext] - [Automatic1111][auto1111], [Stable Diffusion WebUI DirectML][sdwebui-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext]
- [Fooocus][fooocus], [Fooocus MRE][fooocus-mre], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus] - [Fooocus][fooocus], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus]
- [ComfyUI][comfy] - [ComfyUI][comfy]
- [VoltaML][voltaml] - [VoltaML][voltaml]
- [InvokeAI][invokeai] - [InvokeAI][invokeai]
- [Kohya's GUI][kohya-ss] - [Kohya's GUI][kohya-ss]
- [OneTrainer][onetrainer] - [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 - Easily install or update Python dependencies for each package
- Embedded Git and Python dependencies, with no need for either to be globally installed - 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 - 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 ### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix
- 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
- Powerful auto-completion and syntax highlighting using a formal language grammar - Powerful auto-completion and syntax highlighting using a formal language grammar
- Workspaces open in tabs that save and load from `.smproj` project files - Workspaces open in tabs that save and load from `.smproj` project files
@ -76,13 +62,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=""/> <img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/4341cc34-a584-4e9c-bb3b-276009bdae80" alt=""/>
</p> </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"> <p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/75456866-9d95-47c6-8c0a-fdc19443ee02" alt=""/> <img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/75456866-9d95-47c6-8c0a-fdc19443ee02" alt=""/>
</p> </p>
### Model browser powered by [Civit AI][civitai] ### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Downloads new models, automatically uses the appropriate shared model directory - 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 - Pause and resume downloads, even after closing the app
<p align="center"> <p align="center">

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

@ -19,12 +19,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.9" /> <PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" 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" /> <PackageReference Include="NLog" Version="5.2.8" />
</ItemGroup> </ItemGroup>

7
StabilityMatrix.Avalonia/App.axaml

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

22
StabilityMatrix.Avalonia/App.axaml.cs

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

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

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

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

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

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

91
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -36,6 +36,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.PackageModification;
@ -51,6 +52,7 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData public static class DesignData
{ {
@ -325,7 +327,7 @@ public static class DesignData
); );
}*/ }*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel> CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{ {
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm => dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{ {
@ -417,7 +419,8 @@ public static class DesignData
new PackageInstallProgressItemViewModel( new PackageInstallProgressItemViewModel(
new PackageModificationRunner new PackageModificationRunner
{ {
CurrentProgress = new ProgressReport(0.5f, "Installing package...") CurrentProgress = new ProgressReport(0.5f, "Installing package..."),
ModificationCompleteMessage = "Package installed successfully"
} }
) )
} }
@ -920,6 +923,36 @@ The gallery images are often inpainted, but you will get something very similar
vm.IsBatchIndexEnabled = true; vm.IsBatchIndexEnabled = true;
}); });
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 => public static IList<ICompletionData> SampleCompletionData =>
new List<ICompletionData> new List<ICompletionData>
{ {
@ -1031,6 +1064,60 @@ The gallery images are often inpainted, but you will get something very similar
public static ControlNetCardViewModel ControlNetCardViewModel => public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<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 string CurrentDirectory => Directory.GetCurrentDirectory();
public static Indexer Types { get; } = new(); public static Indexer Types { get; } = new();

144
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> /// <summary>
/// Looks up a localized string similar to Open Project.... /// Looks up a localized string similar to Open Project....
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to API Key. /// Looks up a localized string similar to API Key.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Everything looks good!. /// Looks up a localized string similar to Everything looks good!.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to First Page. /// Looks up a localized string similar to First Page.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Inner exception. /// Looks up a localized string similar to Inner exception.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to No extensions found.. /// Looks up a localized string similar to No extensions found..
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Output Folder. /// Looks up a localized string similar to Output Folder.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Steps. /// Looks up a localized string similar to Steps.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to You&apos;re up to date. /// Looks up a localized string similar to You&apos;re up to date.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Add a package to get started!. /// Looks up a localized string similar to Add a package to get started!.
/// </summary> /// </summary>

48
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -972,9 +972,30 @@
<data name="Label_SelectDownloadLocation" xml:space="preserve"> <data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Select Download Location:</value> <value>Select Download Location:</value>
</data> </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"> <data name="Label_Config" xml:space="preserve">
<value>Config</value> <value>Config</value>
</data> </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"> <data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Preview Preprocessor</value> <value>Preview Preprocessor</value>
</data> </data>
@ -1020,4 +1041,31 @@
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve"> <data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>The 'Open Web UI' button has moved to the command bar</value> <value>The 'Open Web UI' button has moved to the command bar</value>
</data> </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>
</root> </root>

8
StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Models;
public interface IInfinitelyScroll
{
Task LoadNextPageAsync();
}

3
StabilityMatrix.Avalonia/Models/ImageSource.cs

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

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

@ -2,8 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Hashing; using System.IO.Hashing;
using System.Linq;
using System.Text; using System.Text;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Avalonia.Models.Inference;
@ -36,7 +38,12 @@ public class ModuleApplyStepEventArgs : EventArgs
{ {
Primary = Builder.Connections.Primary, Primary = Builder.Connections.Primary,
PrimaryVAE = Builder.Connections.PrimaryVAE, PrimaryVAE = Builder.Connections.PrimaryVAE,
Models = Builder.Connections.Models Models = new Dictionary<string, ModelConnections>(
Builder.Connections.Models.ToDictionary(
pair => pair.Key,
pair => new ModelConnections(pair.Value)
)
)
}; };
} }

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

8
StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs

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

248
StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs

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

40
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

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

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

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

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

@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
@ -216,7 +217,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
MaxDialogHeight = 950, MaxDialogHeight = 950,
}; };
var prunedDescription = PruneDescription(model); var prunedDescription = Utilities.RemoveHtml(model.Description);
var viewModel = dialogFactory.Get<SelectModelVersionViewModel>(); var viewModel = dialogFactory.Get<SelectModelVersionViewModel>();
viewModel.Dialog = dialog; viewModel.Dialog = dialog;
@ -263,23 +264,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
await DoImport(model, downloadPath, selectedVersion, selectedFile); 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( private static async Task<FilePath> SaveCmInfo(
CivitModel model, CivitModel model,
CivitModelVersion modelVersion, CivitModelVersion modelVersion,

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

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

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

@ -8,6 +8,7 @@ using Avalonia.Data;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
@ -136,6 +137,12 @@ public partial class CheckpointFile : ViewModelBase
{ {
await Task.Run(() => File.Delete(cmInfoPath)); await Task.Run(() => File.Delete(cmInfoPath));
} }
var settingsManager = App.Services.GetRequiredService<ISettingsManager>();
settingsManager.Transaction(s =>
{
s.InstalledModelHashes?.Remove(ConnectedModel.Hashes.BLAKE3);
});
} }
} }
catch (IOException ex) catch (IOException ex)

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

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

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

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

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

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

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

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

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

168
StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs

@ -0,0 +1,168 @@
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();
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;
}
}

25
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

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

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

12
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -395,12 +395,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
var selected = Outputs.Where(o => o.IsSelected).ToList(); var selected = Outputs.Where(o => o.IsSelected).ToList();
Debug.Assert(selected.Count == NumItemsSelected); Debug.Assert(selected.Count == NumItemsSelected);
var imagesToRemove = new List<LocalImageFile>();
foreach (var output in selected) foreach (var output in selected)
{ {
// Delete the file // Delete the file
var imageFile = new FilePath(output.ImageFile.AbsolutePath); var imageFile = new FilePath(output.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync()); var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful) if (!result.IsSuccessful)
{ {
continue; continue;
@ -413,15 +414,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
await notificationService.TryAsync(sideCar.DeleteAsync()); await notificationService.TryAsync(sideCar.DeleteAsync());
} }
OutputsCache.Remove(output.ImageFile); imagesToRemove.Add(output.ImageFile);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
} }
OutputsCache.Remove(imagesToRemove);
NumItemsSelected = 0; NumItemsSelected = 0;
ClearSelection(); ClearSelection();
} }

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

@ -54,7 +54,7 @@ public partial class PackageCardViewModel(
private InstalledPackage? package; private InstalledPackage? package;
[ObservableProperty] [ObservableProperty]
private string? cardImageSource; private Uri? cardImageSource;
[ObservableProperty] [ObservableProperty]
private bool isUpdateAvailable; private bool isUpdateAvailable;
@ -127,7 +127,7 @@ public partial class PackageCardViewModel(
) )
{ {
IsUnknownPackage = true; IsUnknownPackage = true;
CardImageSource = ""; CardImageSource = null;
InstalledVersion = "Unknown"; InstalledVersion = "Unknown";
} }
else else
@ -135,7 +135,7 @@ public partial class PackageCardViewModel(
IsUnknownPackage = false; IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName]; var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString(); CardImageSource = basePackage?.PreviewImageUri ?? Assets.NoImage;
InstalledVersion = value.Version?.DisplayVersion ?? "Unknown"; InstalledVersion = value.Version?.DisplayVersion ?? "Unknown";
CanUseConfigMethod = CanUseConfigMethod =
basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false; basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false;

2
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

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

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

@ -130,6 +130,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private HolidayMode holidayModeSetting; private HolidayMode holidayModeSetting;
[ObservableProperty]
private bool infinitelyScrollWorkflowBrowser;
#region System Info #region System Info
private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } = private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } =
@ -218,6 +221,13 @@ public partial class MainSettingsViewModel : PageViewModelBase
settings => settings.HolidayModeSetting settings => settings.HolidayModeSetting
); );
settingsManager.RelayPropertyFor(
this,
vm => vm.InfinitelyScrollWorkflowBrowser,
settings => settings.IsWorkflowInfiniteScrollEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick; hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick;

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

26
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -13,6 +13,7 @@
xmlns:api="clr-namespace:StabilityMatrix.Core.Models.Api;assembly=StabilityMatrix.Core" xmlns:api="clr-namespace:StabilityMatrix.Core.Models.Api;assembly=StabilityMatrix.Core"
xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections" xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}"
x:CompileBindings="True" x:CompileBindings="True"
x:DataType="vm:CheckpointsPageViewModel" x:DataType="vm:CheckpointsPageViewModel"
@ -138,7 +139,7 @@
TextWrapping="WrapWithOverflow" TextWrapping="WrapWithOverflow"
IsVisible="{Binding IsConnectedModel}" /> IsVisible="{Binding IsConnectedModel}" />
<!-- Image --> <!-- Image -->
<controls:BetterAdvancedImage <labs:AsyncImage
Margin="0,0,0,4" Margin="0,0,0,4"
CornerRadius="4" CornerRadius="4"
Source="{Binding PreviewImagePath}" Source="{Binding PreviewImagePath}"
@ -175,18 +176,19 @@
IsVisible="{Binding IsConnectedModel}" /> IsVisible="{Binding IsConnectedModel}" />
</StackPanel> </StackPanel>
<controls:BetterAdvancedImage Grid.Row="1" <labs:AsyncImage Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Margin="0, 4,0,4" Margin="0, 4,0,4"
CornerRadius="8" CornerRadius="8"
Source="{Binding PreviewImagePath}" Source="{Binding PreviewImagePath}"
Stretch="UniformToFill" ClipToBounds="True"
Height="300" Stretch="UniformToFill"
Width="300" Height="300"
IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}" /> Width="300"
IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Grid.Column="1" Grid.Column="1"
Grid.Row="0" Grid.Row="0"
HorizontalAlignment="Right"> HorizontalAlignment="Right">
<controls:Card <controls:Card

93
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml

@ -15,7 +15,8 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}" d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="800" d:DesignWidth="800"
@ -31,7 +32,7 @@
</Transitions> </Transitions>
</Setter> </Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage"> <Style Selector="^ labs|AsyncImage">
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<TransformOperationsTransition Property="RenderTransform" <TransformOperationsTransition Property="RenderTransform"
@ -47,7 +48,7 @@
<Style Selector="^:pointerover"> <Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/> <Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage"> <Style Selector="^ labs|AsyncImage">
<Setter Property="CornerRadius" Value="12"/> <Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/> <Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style> </Style>
@ -59,7 +60,7 @@
<Style Selector="^:not(:pointerover)"> <Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/> <Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" /> <Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage"> <Style Selector="^ labs|AsyncImage">
<Setter Property="CornerRadius" Value="8"/> <Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/> <Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style> </Style>
@ -71,6 +72,7 @@
</UserControl.Styles> </UserControl.Styles>
<UserControl.Resources> <UserControl.Resources>
<system:Boolean x:Key="False">False</system:Boolean>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/> <converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}"> <DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border <Border
@ -101,20 +103,20 @@
CommandParameter="{Binding CivitModel}" CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}"> IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto"> <Grid RowDefinitions="*, Auto">
<controls1:AsyncImage Grid.Row="0" <labs:AsyncImage Grid.Row="0"
CornerRadius="8" CornerRadius="8"
VerticalAlignment="Top" VerticalAlignment="Top"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Height="75" Height="75"
ZIndex="10" ZIndex="10"
IsVisible="{Binding ShowSantaHats}" IsVisible="{Binding ShowSantaHats, FallbackValue=False}"
Margin="0,8,0,0" Margin="0,8,0,0"
Source="avares://StabilityMatrix.Avalonia/Assets/santahat.png"> Source="avares://StabilityMatrix.Avalonia/Assets/santahat.png">
<!-- <controls:BetterAdvancedImage.RenderTransform> --> <!-- <controls:BetterAdvancedImage.RenderTransform> -->
<!-- <RotateTransform Angle="315"></RotateTransform> --> <!-- <RotateTransform Angle="315"></RotateTransform> -->
<!-- </controls:BetterAdvancedImage.RenderTransform> --> <!-- </controls:BetterAdvancedImage.RenderTransform> -->
</controls1:AsyncImage> </labs:AsyncImage>
<controls1:AsyncImage <labs:AsyncImage
Grid.Row="0" Grid.Row="0"
Grid.RowSpan="2" Grid.RowSpan="2"
CornerRadius="8" CornerRadius="8"
@ -180,14 +182,15 @@
Classes="transparent" Classes="transparent"
Padding="10,4"> Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<controls1:AsyncImage <labs:AsyncImage
Width="22" Width="22"
Height="22" Height="22"
Effect="{StaticResource ImageDropShadowEffect}" Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11" CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality" RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/> Source="{Binding CivitModel.Creator.Image}">
</labs:AsyncImage>
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}" Effect="{StaticResource TextDropShadowEffect}"
@ -319,11 +322,11 @@
Background="#66000000" Background="#66000000"
FontSize="16" FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}" Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" /> Value="{Binding CivitModel.ModelVersionStats.Rating}" />
<TextBlock <TextBlock
Margin="4,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}" Text="{Binding CivitModel.ModelVersionStats.RatingCount}"
TextAlignment="Center" /> TextAlignment="Center" />
</StackPanel> </StackPanel>
@ -336,7 +339,7 @@
<TextBlock <TextBlock
Margin="4,0" Margin="4,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" /> Text="{Binding CivitModel.Stats.ThumbsUpCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" /> <avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock <TextBlock
@ -402,6 +405,7 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes="accent" Classes="accent"
Command="{Binding SearchModelsCommand}" Command="{Binding SearchModelsCommand}"
CommandParameter="{StaticResource False}"
IsDefault="True"> IsDefault="True">
<Grid> <Grid>
<controls:ProgressRing <controls:ProgressRing
@ -472,57 +476,18 @@
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </ScrollViewer>
<TextBlock <TextBlock Grid.Row="2" Text="End of results"
Grid.Row="2" TextAlignment="Center"
Margin="8,8" Margin="0,0,0,8">
VerticalAlignment="Center" <TextBlock.IsVisible>
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" /> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="HasSearched"/>
<StackPanel Grid.Row="2" <Binding Path="NextPageCursor"
HorizontalAlignment="Center" Converter="{x:Static StringConverters.IsNullOrEmpty}"/>
IsVisible="{Binding HasSearched}" </MultiBinding>
Margin="0,8,0,8" </TextBlock.IsVisible>
Orientation="Horizontal"> </TextBlock>
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"

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

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

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

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

136
StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml

@ -0,0 +1,136 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.OpenArtWorkflowDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
d:DataContext="{x:Static designData:DesignData.OpenArtWorkflowViewModel}"
d:DesignHeight="650"
d:DesignWidth="600"
x:DataType="dialogs:OpenArtWorkflowViewModel"
mc:Ignorable="d">
<Grid
Width="600"
HorizontalAlignment="Stretch"
ColumnDefinitions="*, 2*"
RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="8,8,0,4"
FontSize="20"
TextAlignment="Left"
ToolTip.Tip="{Binding Workflow.Name}">
<Run Text="{Binding Workflow.Name}" />
<Run Text="- by" />
<Run Text="{Binding Workflow.Creator.Name}" />
</TextBlock>
<labs:AsyncImage
Grid.Row="2"
Grid.Column="0"
Height="300"
Margin="8"
CornerRadius="8"
Source="{Binding Workflow.Thumbnails[0].Url}"
Stretch="UniformToFill" />
<controls:Card
Grid.Row="2"
Grid.Column="1"
Margin="8"
VerticalAlignment="Top">
<ScrollViewer MaxHeight="270">
<TextBlock
Margin="4"
Text="{Binding PrunedDescription}"
TextWrapping="Wrap" />
</ScrollViewer>
</controls:Card>
<Expander
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="8,8"
ExpandDirection="Down"
Header="{x:Static lang:Resources.Label_NodeDetails}">
<ScrollViewer MaxHeight="225">
<ItemsControl ItemsSource="{Binding CustomNodes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type models:OpenArtCustomNode}">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock
FontSize="16"
FontWeight="SemiBold"
Text="{Binding Title}" />
<avalonia:Icon
Margin="4"
VerticalAlignment="Center"
Foreground="Lime"
IsVisible="{Binding IsInstalled}"
Value="fa-solid fa-circle-check" />
</StackPanel>
<ItemsControl Margin="0,4" ItemsSource="{Binding Children}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type system:String}">
<StackPanel Orientation="Vertical">
<TextBlock Margin="4,0,0,0" Text="{Binding ., StringFormat={} - {0}}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Expander>
<Expander
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="8"
Header="Options">
<StackPanel Spacing="4">
<ui:SettingsExpanderItem Content="Install Required Nodes">
<ui:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding InstallRequiredNodes}"
IsEnabled="{Binding AvailablePackages.Count}"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Target Package">
<ui:SettingsExpanderItem.Footer>
<ComboBox
DisplayMemberBinding="{Binding DisplayName}"
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage}"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</StackPanel>
</Expander>
</Grid>
</controls:UserControlBase>

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

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

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

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

254
StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml

@ -0,0 +1,254 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:openArt="clr-namespace:StabilityMatrix.Core.Models.Api.OpenArt"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
d:DataContext="{x:Static designData:DesignData.InstalledWorkflowsViewModel}"
x:DataType="viewModels:InstalledWorkflowsViewModel"
x:Class="StabilityMatrix.Avalonia.Views.InstalledWorkflowsPage">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237" />
</Transitions>
</Setter>
<Style Selector="^ labs|AsyncImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut />
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000" />
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12" />
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)" />
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000" />
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="RenderTransform" Value="scale(1, 1)" />
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<input:StandardUICommand
x:Key="OpenInExplorerCommand"
Command="{Binding OpenInExplorerCommand}" />
<input:StandardUICommand
x:Key="OpenOnOpenArtCommand"
Command="{Binding OpenOnOpenArtCommand}" />
<input:StandardUICommand
x:Key="DeleteCommand"
Command="{Binding DeleteCommand}" />
</UserControl.Resources>
<Grid RowDefinitions="Auto, *">
<controls1:CommandBar Grid.Row="0" Grid.Column="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
VerticalContentAlignment="Center"
DefaultLabelPosition="Right">
<controls1:CommandBar.PrimaryCommands>
<controls1:CommandBarButton
IconSource="Refresh"
VerticalAlignment="Center"
Label="{x:Static lang:Resources.Action_Refresh}"
Command="{Binding LoadInstalledWorkflowsCommand}" />
<controls1:CommandBarSeparator />
<controls1:CommandBarElementContainer>
<StackPanel Orientation="Horizontal">
<avalonia:Icon FontSize="18"
Value="fa-solid fa-info"
Margin="8,0" />
<TextBlock Text="Drag &amp; drop one of the cards below into ComfyUI to load the workflow"
VerticalAlignment="Center" />
</StackPanel>
</controls1:CommandBarElementContainer>
</controls1:CommandBar.PrimaryCommands>
</controls1:CommandBar>
<ScrollViewer Grid.Column="0"
Grid.Row="1">
<ItemsRepeater ItemsSource="{Binding DisplayedWorkflows}">
<ItemsRepeater.Layout>
<!-- <UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4"/> -->
<UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="{x:Type models:OpenArtMetadata}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Interaction.Behaviors>
<BehaviorCollection>
<controls:BetterContextDragBehavior
Context="{Binding FilePath}"
DataFormat="Files"
HorizontalDragThreshold="6"
VerticalDragThreshold="6" />
</BehaviorCollection>
</Interaction.Behaviors>
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnOpenArt}"
IsVisible="{Binding HasMetadata}"
Command="{StaticResource OpenOnOpenArtCommand}"
CommandParameter="{Binding }">
<MenuItem.Icon>
<fluent:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Action_OpenInExplorer}"
Command="{StaticResource OpenInExplorerCommand}"
CommandParameter="{Binding }">
<MenuItem.Icon>
<fluent:SymbolIcon Symbol="Folder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Action_Delete}"
Command="{StaticResource DeleteCommand}"
CommandParameter="{Binding }">
<MenuItem.Icon>
<fluent:SymbolIcon Symbol="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8">
<Grid RowDefinitions="*, Auto">
<labs:AsyncImage
Grid.Row="0"
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding FirstThumbnail}"
IsVisible="{Binding FirstThumbnail, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Stretch="UniformToFill" />
<avalonia:Icon Grid.Row="0"
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
FontSize="100"
IsVisible="{Binding FirstThumbnail, Converter={x:Static ObjectConverters.IsNull}, FallbackValue=False}"
Value="fa-regular fa-file-code" />
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9" />
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2" />
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding Workflow.Creator.DevProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<labs:AsyncImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding Workflow.Creator.Avatar, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding Workflow.Creator.Avatar}" />
<TextBlock
VerticalAlignment="Center"
Foreground="{DynamicResource TextControlForeground}"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding Workflow.Creator.Name}" />
</StackPanel>
</Button>
</Border>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<TextBlock
Padding="16"
Margin="8,0,8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
TextWrapping="Wrap"
MaxWidth="315"
Text="{Binding Workflow.Name}"
ToolTip.Tip="{Binding Workflow.Name}" />
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs

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

4
StabilityMatrix.Avalonia/Views/MainWindow.axaml

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

360
StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml

@ -0,0 +1,360 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:openArt="clr-namespace:StabilityMatrix.Core.Models.Api.OpenArt;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
x:DataType="viewModels:OpenArtBrowserViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.OpenArtBrowserPage">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237" />
</Transitions>
</Setter>
<Style Selector="^ labs|AsyncImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut />
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000" />
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ labs|AsyncImage">
<Setter Property="CornerRadius" Value="12" />
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)" />
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000" />
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ labs|AsyncImage">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="RenderTransform" Value="scale(1, 1)" />
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<input:StandardUICommand
x:Key="OpenModelCommand"
Command="{Binding OpenModelCommand}" />
<input:StandardUICommand
x:Key="OpenOnOpenArtCommand"
Command="{Binding OpenOnOpenArtCommand}" />
<input:StandardUICommand
x:Key="OpenWorkflowCommand"
Command="{Binding OpenWorkflowCommand}" />
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter" />
<DataTemplate x:Key="OpenArtWorkflowTemplate" DataType="{x:Type openArt:OpenArtSearchResult}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnOpenArt}"
Command="{StaticResource OpenOnOpenArtCommand}"
CommandParameter="{Binding }">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{StaticResource OpenWorkflowCommand}"
CommandParameter="{Binding }">
<Grid RowDefinitions="*, Auto">
<labs:AsyncImage
Grid.Row="0"
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding Thumbnails[0].Url}"
Stretch="UniformToFill" />
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
ClipToBounds="True"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9" />
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2" />
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding Creator.DevProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<labs:AsyncImage
Width="22"
Height="22"
ClipToBounds="True"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding Creator.Avatar, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding Creator.Avatar}">
</labs:AsyncImage>
<TextBlock
VerticalAlignment="Center"
Foreground="{DynamicResource TextControlForeground}"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding Creator.Name}" />
</StackPanel>
</Button>
</Border>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding Name}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding Stats.NumReviews}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding Stats.NumLikes, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding Stats.NumDownloads, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{StaticResource OpenModelCommand}"
CommandParameter="{Binding }"
Header="{x:Static lang:Resources.Action_OpenOnOpenArt}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid RowDefinitions="Auto, Auto, *, Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto"
Margin="8">
<TextBox
HorizontalAlignment="Stretch"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Action_Search}"
Classes="search"/>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchButtonCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchButtonCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchButtonCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<StackPanel Grid.Row="1"
Margin="8,0,0,8"
Orientation="Vertical"
IsVisible="{Binding SearchQuery, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SelectedSortMode}"/>
</StackPanel>
<controls:ProgressRing Grid.Row="2"
IsVisible="{Binding IsLoading}"
IsIndeterminate="True"
Width="128"
Height="128"/>
<ScrollViewer Grid.Row="2"
ScrollChanged="ScrollViewer_OnScrollChanged"
IsVisible="{Binding !IsLoading}">
<ItemsRepeater ItemsSource="{Binding SearchResults}"
ItemTemplate="{StaticResource OpenArtWorkflowTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4"/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<StackPanel Grid.Row="3"
HorizontalAlignment="Center"
Margin="0,8,0,8"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPageCommand}"
IsEnabled="{Binding CanGoBack}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPageCommand}"
IsEnabled="{Binding CanGoBack}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding DisplayedPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding PageCount, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPageCommand}"
IsEnabled="{Binding CanGoForward}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPageCommand}"
IsEnabled="{Binding CanGoToEnd}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</Grid>
</controls:UserControlBase>

41
StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs

@ -0,0 +1,41 @@
using System;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class OpenArtBrowserPage : UserControlBase
{
private readonly ISettingsManager settingsManager;
public OpenArtBrowserPage(ISettingsManager settingsManager)
{
this.settingsManager = settingsManager;
InitializeComponent();
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
if (scrollViewer.Offset.Y == 0)
return;
var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f;
if (
isAtEnd
&& settingsManager.Settings.IsWorkflowInfiniteScrollEnabled
&& DataContext is IInfinitelyScroll scroll
)
{
scroll.LoadNextPageAsync().SafeFireAndForget();
}
}
}

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

@ -34,8 +34,8 @@
<Grid ColumnDefinitions="Auto, *, Auto"> <Grid ColumnDefinitions="Auto, *, Auto">
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Grid.Column="0" Grid.Column="0"
MaxHeight="128" Height="128"
MaxWidth="128" Width="128"
Stretch="UniformToFill" Stretch="UniformToFill"
CornerRadius="4" CornerRadius="4"
Source="{Binding PreviewImageUri}" /> Source="{Binding PreviewImageUri}" />

17
StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml

@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
@ -10,24 +9,26 @@
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:extensions="clr-namespace:StabilityMatrix.Core.Models.Packages.Extensions;assembly=StabilityMatrix.Core"
xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager" xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="packageManager:PackageInstallDetailViewModel" x:DataType="packageManager:PackageInstallDetailViewModel"
x:CompileBindings="True" x:CompileBindings="True"
d:DataContext="{x:Static designData:DesignData.PackageInstallDetailViewModel}" d:DataContext="{x:Static designData:DesignData.PackageInstallDetailViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.PackageManager.PackageInstallDetailView"> x:Class="StabilityMatrix.Avalonia.Views.PackageManager.PackageInstallDetailView">
<Grid ColumnDefinitions="Auto, *" <Grid ColumnDefinitions="Auto, *"
RowDefinitions="Auto, *"> RowDefinitions="Auto, *">
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
MaxHeight="300" Height="300"
MaxWidth="300" Width="300"
Margin="16,16,8,8"
CornerRadius="8"
VerticalAlignment="Top" VerticalAlignment="Top"
Stretch="UniformToFill" Stretch="UniformToFill"
Source="{Binding SelectedPackage.PreviewImageUri}" /> Margin="16, 16, 8, 8"
CornerRadius="8"
Source="{Binding SelectedPackage.PreviewImageUri}">
</controls:BetterAdvancedImage>
<StackPanel Grid.Row="0" Grid.Column="1" <StackPanel Grid.Row="0" Grid.Column="1"
Margin="8" Margin="8"
Orientation="Vertical"> Orientation="Vertical">

5
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -13,6 +13,7 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:avalonia="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" xmlns:avalonia="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:markupExtensions="clr-namespace:StabilityMatrix.Avalonia.MarkupExtensions" xmlns:markupExtensions="clr-namespace:StabilityMatrix.Avalonia.MarkupExtensions"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450"
x:DataType="viewModels:PackageManagerViewModel" x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True" x:CompileBindings="True"
@ -44,15 +45,13 @@
</MultiBinding> </MultiBinding>
</controls:Card.Background> </controls:Card.Background>
<Grid ColumnDefinitions="Auto, *"> <Grid ColumnDefinitions="Auto, *">
<controls:BetterAdvancedImage <labs:AsyncImage
Grid.Column="0" Grid.Column="0"
Height="150" Height="150"
Width="150" Width="150"
Margin="4, 8" Margin="4, 8"
CornerRadius="8" CornerRadius="8"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalContentAlignment="Top"
HorizontalContentAlignment="Center"
Source="{Binding CardImageSource}" Source="{Binding CardImageSource}"
IsVisible="{Binding !IsUnknownPackage}" IsVisible="{Binding !IsUnknownPackage}"
Stretch="UniformToFill" /> Stretch="UniformToFill" />

202
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml

@ -45,77 +45,6 @@
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8"> <StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Appearance}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
DisplayMemberBinding="{Binding Converter={StaticResource CultureInfoDisplayConverter}}"
ItemsSource="{Binding AvailableLanguages}"
SelectedItem="{Binding SelectedLanguage}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
Header="Inference (Test)"
IconSource="Code"
IsClickEnabled="True"
IsVisible="{Binding SharedState.IsDebugMode}" />
</Grid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_CheckpointManager}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Description="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown_Details}"
Header="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown}"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding RemoveSymlinksOnShutdown}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Description="{x:Static lang:Resources.Label_ResetCheckpointsCache_Details}"
Header="{x:Static lang:Resources.Label_ResetCheckpointsCache}"
IconSource="Refresh">
<ui:SettingsExpander.Footer>
<Button Command="{Binding ResetCheckpointCache}" Content="{x:Static lang:Resources.Label_ResetCheckpointsCache}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- General --> <!-- General -->
<sg:SpacedGrid RowDefinitions="Auto,*,*,*" RowSpacing="4"> <sg:SpacedGrid RowDefinitions="Auto,*,*,*" RowSpacing="4">
<TextBlock <TextBlock
@ -167,6 +96,68 @@
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</sg:SpacedGrid> </sg:SpacedGrid>
<!-- Integrations -->
<sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,4"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Integrations}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:AccountSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Accounts}"
IsClickEnabled="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="Person" />
</ui:SettingsExpander.IconSource>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_DiscordRichPresence}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</sg:SpacedGrid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_CheckpointManager}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Description="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown_Details}"
Header="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown}"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding RemoveSymlinksOnShutdown}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Description="{x:Static lang:Resources.Label_ResetCheckpointsCache_Details}"
Header="{x:Static lang:Resources.Label_ResetCheckpointsCache}"
IconSource="Refresh">
<ui:SettingsExpander.Footer>
<Button Command="{Binding ResetCheckpointCache}" Content="{x:Static lang:Resources.Label_ResetCheckpointsCache}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Environment Options --> <!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto"> <Grid RowDefinitions="Auto, Auto, Auto">
@ -204,40 +195,67 @@
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</Grid> </Grid>
<!-- Integrations --> <sg:SpacedGrid RowDefinitions="Auto, Auto">
<sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,8"
FontWeight="Medium" FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Integrations}" /> Text="{x:Static lang:Resources.Label_WorkflowBrowser}" />
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0" Margin="8,0,8,4"
ActionIconSource="ChevronRight" Header="{x:Static lang:Resources.Label_InfiniteScrolling}">
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:AccountSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Accounts}"
IsClickEnabled="True">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource <controls:FASymbolIconSource Symbol="fa-solid fa-list" />
FontSize="10"
IsFilled="True"
Symbol="Person" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding InfinitelyScrollWorkflowBrowser}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</sg:SpacedGrid>
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Appearance}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="2" Grid.Row="2"
Margin="8,0,8,4" Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_DiscordRichPresence}"> Header="{x:Static lang:Resources.Label_Language}"
<ui:SettingsExpander.IconSource> IconSource="Character">
<controls:FASymbolIconSource Symbol="fa-brands fa-discord" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" /> <ComboBox
MinWidth="100"
DisplayMemberBinding="{Binding Converter={StaticResource CultureInfoDisplayConverter}}"
ItemsSource="{Binding AvailableLanguages}"
SelectedItem="{Binding SelectedLanguage}" />
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</sg:SpacedGrid> <ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
Header="Inference (Test)"
IconSource="Code"
IsClickEnabled="True"
IsVisible="{Binding SharedState.IsDebugMode}" />
</Grid>
<!-- System Options --> <!-- System Options -->
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4"> <sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4">

12
StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml

@ -0,0 +1,12 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:WorkflowsPageViewModel"
x:Class="StabilityMatrix.Avalonia.Views.WorkflowsPage">
<TabControl ItemsSource="{Binding Pages}"
SelectedItem="{Binding SelectedPage}"/>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs

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

17
StabilityMatrix.Core/Api/IOpenArtApi.cs

@ -0,0 +1,17 @@
using Refit;
using StabilityMatrix.Core.Models.Api.OpenArt;
namespace StabilityMatrix.Core.Api;
[Headers("User-Agent: StabilityMatrix")]
public interface IOpenArtApi
{
[Get("/feed")]
Task<OpenArtSearchResponse> GetFeedAsync([Query] OpenArtFeedRequest request);
[Get("/list")]
Task<OpenArtSearchResponse> SearchAsync([Query] OpenArtSearchRequest request);
[Post("/download")]
Task<OpenArtDownloadResponse> DownloadWorkflowAsync([Body] OpenArtDownloadRequest request);
}

3
StabilityMatrix.Core/Helper/EventManager.cs

@ -46,6 +46,7 @@ public class EventManager
public event EventHandler<int>? NavigateAndFindCivitModelRequested; public event EventHandler<int>? NavigateAndFindCivitModelRequested;
public event EventHandler? DownloadsTeachingTipRequested; public event EventHandler? DownloadsTeachingTipRequested;
public event EventHandler? RecommendedModelsDialogClosed; public event EventHandler? RecommendedModelsDialogClosed;
public event EventHandler? WorkflowInstalled;
public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress);
@ -108,4 +109,6 @@ public class EventManager
public void OnPackageRelaunchRequested(InstalledPackage package) => public void OnPackageRelaunchRequested(InstalledPackage package) =>
PackageRelaunchRequested?.Invoke(this, package); PackageRelaunchRequested?.Invoke(this, package);
public void OnWorkflowInstalled() => WorkflowInstalled?.Invoke(this, EventArgs.Empty);
} }

23
StabilityMatrix.Core/Helper/Utilities.cs

@ -1,8 +1,9 @@
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
namespace StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Helper;
public static class Utilities public static partial class Utilities
{ {
public static string GetAppVersion() public static string GetAppVersion()
{ {
@ -63,4 +64,24 @@ public static class Utilities
return stream; return stream;
} }
public static string RemoveHtml(string? stringWithHtml)
{
var pruned =
stringWithHtml
?.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;
pruned = HtmlRegex().Replace(pruned, string.Empty);
return pruned;
}
[GeneratedRegex("<[^>]+>")]
private static partial Regex HtmlRegex();
} }

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

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

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

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

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

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

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

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

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

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

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

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

15
StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class NodesCount
{
[JsonPropertyName("total")]
public long Total { get; set; }
[JsonPropertyName("primitive")]
public long Primitive { get; set; }
[JsonPropertyName("custom")]
public long Custom { get; set; }
}

24
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtCreator
{
[JsonPropertyName("uid")]
public string Uid { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("bio")]
public string Bio { get; set; }
[JsonPropertyName("avatar")]
public Uri Avatar { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("dev_profile_url")]
public string DevProfileUrl { get; set; }
}

14
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtDateTime
{
[JsonPropertyName("_seconds")]
public long Seconds { get; set; }
public DateTimeOffset ToDateTimeOffset()
{
return DateTimeOffset.FromUnixTimeSeconds(Seconds);
}
}

15
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using Refit;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtDownloadRequest
{
[AliasAs("workflow_id")]
[JsonPropertyName("workflow_id")]
public required string WorkflowId { get; set; }
[AliasAs("version_tag")]
[JsonPropertyName("version_tag")]
public string VersionTag { get; set; } = "latest";
}

12
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtDownloadResponse
{
[JsonPropertyName("filename")]
public string Filename { get; set; }
[JsonPropertyName("payload")]
public string Payload { get; set; }
}

21
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs

@ -0,0 +1,21 @@
using Refit;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
/// <summary>
/// Note that parameters Category, Custom Node and Sort should be used separately
/// </summary>
public class OpenArtFeedRequest
{
[AliasAs("category")]
public string Category { get; set; }
[AliasAs("sort")]
public string Sort { get; set; }
[AliasAs("custom_node")]
public string CustomNode { get; set; }
[AliasAs("cursor")]
public string Cursor { get; set; }
}

18
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs

@ -0,0 +1,18 @@
using Refit;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtSearchRequest
{
[AliasAs("keyword")]
public required string Keyword { get; set; }
[AliasAs("pageSize")]
public int PageSize { get; set; } = 30;
/// <summary>
/// 0-based index of the page to retrieve
/// </summary>
[AliasAs("currentPage")]
public int CurrentPage { get; set; } = 0;
}

15
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtSearchResponse
{
[JsonPropertyName("items")]
public IEnumerable<OpenArtSearchResult> Items { get; set; }
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; set; }
}

33
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtSearchResult
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("creator")]
public OpenArtCreator Creator { get; set; }
[JsonPropertyName("stats")]
public OpenArtStats Stats { get; set; }
[JsonPropertyName("nodes_index")]
public IEnumerable<string> NodesIndex { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("categories")]
public IEnumerable<string> Categories { get; set; }
[JsonPropertyName("thumbnails")]
public List<OpenArtThumbnail> Thumbnails { get; set; }
[JsonPropertyName("nodes_count")]
public NodesCount NodesCount { get; set; }
}

33
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtStats
{
[JsonPropertyName("num_shares")]
public int NumShares { get; set; }
[JsonPropertyName("num_bookmarks")]
public int NumBookmarks { get; set; }
[JsonPropertyName("num_reviews")]
public int NumReviews { get; set; }
[JsonPropertyName("rating")]
public double Rating { get; set; }
[JsonPropertyName("num_comments")]
public int NumComments { get; set; }
[JsonPropertyName("num_likes")]
public int NumLikes { get; set; }
[JsonPropertyName("num_downloads")]
public int NumDownloads { get; set; }
[JsonPropertyName("num_runs")]
public int NumRuns { get; set; }
[JsonPropertyName("num_views")]
public int NumViews { get; set; }
}

15
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtThumbnail
{
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("url")]
public Uri Url { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
}

8420
StabilityMatrix.Core/Models/ComfyNodeMap.cs

File diff suppressed because it is too large Load Diff

12
StabilityMatrix.Core/Models/PackageDifficulty.cs

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

36
StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs

@ -0,0 +1,36 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.PackageModification;
public class DownloadOpenArtWorkflowStep(
IOpenArtApi openArtApi,
OpenArtSearchResult workflow,
ISettingsManager settingsManager
) : IPackageStep
{
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
var workflowData = await openArtApi
.DownloadWorkflowAsync(new OpenArtDownloadRequest { WorkflowId = workflow.Id })
.ConfigureAwait(false);
var workflowJson = JsonSerializer.SerializeToNode(workflow);
Directory.CreateDirectory(settingsManager.WorkflowDirectory);
var filePath = Path.Combine(settingsManager.WorkflowDirectory, $"{workflowData.Filename}.json");
var jsonObject = JsonNode.Parse(workflowData.Payload) as JsonObject;
jsonObject?.Add("sm_workflow_data", workflowJson);
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(jsonObject)).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Downloaded OpenArt Workflow"));
}
public string ProgressTitle => "Downloading OpenArt Workflow";
}

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

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

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

@ -80,7 +80,7 @@ public class PackageModificationRunner : IPackageModificationRunner
public string? ModificationCompleteTitle { get; init; } = "Install Complete"; public string? ModificationCompleteTitle { get; init; } = "Install Complete";
public string? ModificationCompleteMessage { get; init; } public required string ModificationCompleteMessage { get; init; }
public string? ModificationFailedTitle { get; init; } = "Install Failed"; public string? ModificationFailedTitle { get; init; } = "Install Failed";

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

@ -43,7 +43,7 @@ public class ComfyUI(
public override bool ShouldIgnoreReleases => true; public override bool ShouldIgnoreReleases => true;
public override bool IsInferenceCompatible => true; public override bool IsInferenceCompatible => true;
public override string OutputFolderName => "output"; public override string OutputFolderName => "output";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.InferenceCompatible;
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration;

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

@ -89,6 +89,14 @@ public interface IPackageExtensionManager
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
/// <summary>
/// Like <see cref="GetInstalledExtensionsAsync"/>, but does not check version.
/// </summary>
Task<IEnumerable<InstalledPackageExtension>> GetInstalledExtensionsLiteAsync(
InstalledPackage installedPackage,
CancellationToken cancellationToken = default
);
/// <summary> /// <summary>
/// Get updated info (version) for an installed extension. /// Get updated info (version) for an installed extension.
/// </summary> /// </summary>

2
StabilityMatrix.Core/Models/Packages/SDWebForge.cs

@ -39,6 +39,8 @@ public class SDWebForge(
public override bool ShouldIgnoreReleases => true; public override bool ShouldIgnoreReleases => true;
public override IPackageExtensionManager ExtensionManager => null; public override IPackageExtensionManager ExtensionManager => null;
public override string OutputFolderName => "output"; public override string OutputFolderName => "output";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.ReallyRecommended;
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders => public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() new()
{ {

14
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -34,6 +34,19 @@ public class Settings
set => ActiveInstalledPackageId = value?.Id; set => ActiveInstalledPackageId = value?.Id;
} }
[JsonPropertyName("PreferredWorkflowPackage")]
public Guid? PreferredWorkflowPackageId { get; set; }
[JsonIgnore]
public InstalledPackage? PreferredWorkflowPackage
{
get =>
PreferredWorkflowPackageId == null
? null
: InstalledPackages.FirstOrDefault(x => x.Id == PreferredWorkflowPackageId);
set => PreferredWorkflowPackageId = value?.Id;
}
public bool HasSeenWelcomeNotification { get; set; } public bool HasSeenWelcomeNotification { get; set; }
public List<string>? PathExtensions { get; set; } public List<string>? PathExtensions { get; set; }
public string? WebApiHost { get; set; } public string? WebApiHost { get; set; }
@ -120,6 +133,7 @@ public class Settings
public Size InferenceImageSize { get; set; } = new(150, 190); public Size InferenceImageSize { get; set; } = new(150, 190);
public Size OutputsImageSize { get; set; } = new(300, 300); public Size OutputsImageSize { get; set; } = new(300, 300);
public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic; public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic;
public bool IsWorkflowInfiniteScrollEnabled { get; set; } = true;
public bool IsOutputsTreeViewEnabled { get; set; } = true; public bool IsOutputsTreeViewEnabled { get; set; } = true;
[JsonIgnore] [JsonIgnore]

1
StabilityMatrix.Core/Services/ISettingsManager.cs

@ -22,6 +22,7 @@ public interface ISettingsManager
Settings Settings { get; } Settings Settings { get; }
List<string> PackageInstallsInProgress { get; set; } List<string> PackageInstallsInProgress { get; set; }
DirectoryPath WorkflowDirectory { get; }
/// <summary> /// <summary>
/// Event fired when the library directory is changed /// Event fired when the library directory is changed

1
StabilityMatrix.Core/Services/SettingsManager.cs

@ -64,6 +64,7 @@ public class SettingsManager : ISettingsManager
private FilePath SettingsFile => LibraryDir.JoinFile("settings.json"); private FilePath SettingsFile => LibraryDir.JoinFile("settings.json");
public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models");
public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads");
public DirectoryPath WorkflowDirectory => LibraryDir.JoinDir("Workflows");
public DirectoryPath TagsDirectory => LibraryDir.JoinDir("Tags"); public DirectoryPath TagsDirectory => LibraryDir.JoinDir("Tags");
public DirectoryPath ImagesDirectory => LibraryDir.JoinDir("Images"); public DirectoryPath ImagesDirectory => LibraryDir.JoinDir("Images");
public DirectoryPath ImagesInferenceDirectory => ImagesDirectory.JoinDir("Inference"); public DirectoryPath ImagesInferenceDirectory => ImagesDirectory.JoinDir("Inference");

12
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -17,8 +17,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncAwaitBestPractices" Version="7.0.0" /> <PackageReference Include="AsyncAwaitBestPractices" Version="7.0.0" />
<PackageReference Include="AutoCtor" Version="2.1.1" /> <PackageReference Include="AutoCtor" Version="2.2.0" />
<PackageReference Include="Blake3" Version="1.0.0" /> <PackageReference Include="Blake3" Version="1.1.0" />
<PackageReference Include="Crc32.NET" Version="1.2.0" /> <PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="CSharpDiscriminatedUnion" Version="2.0.1" /> <PackageReference Include="CSharpDiscriminatedUnion" Version="2.0.1" />
<PackageReference Include="DeviceId" Version="6.6.0" /> <PackageReference Include="DeviceId" Version="6.6.0" />
@ -27,10 +27,10 @@
<PackageReference Include="DeviceId.Windows" Version="6.6.0" /> <PackageReference Include="DeviceId.Windows" Version="6.6.0" />
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.6.0" /> <PackageReference Include="DeviceId.Windows.Wmi" Version="6.6.0" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DynamicData" Version="8.3.27" /> <PackageReference Include="DynamicData" Version="8.4.1" />
<PackageReference Include="ExifLibNet" Version="2.1.4" /> <PackageReference Include="ExifLibNet" Version="2.1.4" />
<PackageReference Include="FreneticLLC.FreneticUtilities" Version="1.0.24" /> <PackageReference Include="FreneticLLC.FreneticUtilities" Version="1.0.24" />
<PackageReference Include="Hardware.Info" Version="100.0.1.1" /> <PackageReference Include="Hardware.Info" Version="100.1.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="KGySoft.Drawing.Core" Version="8.1.0" /> <PackageReference Include="KGySoft.Drawing.Core" Version="8.1.0" />
<PackageReference Include="KGySoft.Drawing.SkiaSharp" Version="8.1.0" /> <PackageReference Include="KGySoft.Drawing.SkiaSharp" Version="8.1.0" />
@ -45,7 +45,7 @@
<PackageReference Include="Octokit" Version="10.0.0" /> <PackageReference Include="Octokit" Version="10.0.0" />
<PackageReference Include="OneOf" Version="3.0.263" /> <PackageReference Include="OneOf" Version="3.0.263" />
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263" /> <PackageReference Include="OneOf.SourceGenerator" 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.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="pythonnet" Version="3.0.3" /> <PackageReference Include="pythonnet" Version="3.0.3" />
<PackageReference Include="Refit" Version="7.0.0" /> <PackageReference Include="Refit" Version="7.0.0" />
@ -53,7 +53,7 @@
<PackageReference Include="RockLib.Reflection.Optimized" Version="3.0.0" /> <PackageReference Include="RockLib.Reflection.Optimized" Version="3.0.0" />
<PackageReference Include="Salaros.ConfigParser" Version="0.3.8" /> <PackageReference Include="Salaros.ConfigParser" Version="0.3.8" />
<PackageReference Include="Semver" Version="3.0.0-beta.1" /> <PackageReference Include="Semver" Version="3.0.0-beta.1" />
<PackageReference Include="Sentry.NLog" Version="4.1.2" /> <PackageReference Include="Sentry.NLog" Version="4.2.1" />
<PackageReference Include="SharpCompress" Version="0.36.0" /> <PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SkiaSharp" Version="2.88.7" /> <PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="Websocket.Client" Version="5.1.1" /> <PackageReference Include="Websocket.Client" Version="5.1.1" />

8
StabilityMatrix.Tests/StabilityMatrix.Tests.csproj

@ -17,18 +17,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DotNext" Version="5.1.0" /> <PackageReference Include="DotNext" Version="5.3.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" 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.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" /> <PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" /> <PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
<PackageReference Include="coverlet.collector" Version="6.0.1"> <PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<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.Contrib.WaitAndRetry" Version="1.1.1" />
</ItemGroup> </ItemGroup>

8
StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

@ -15,16 +15,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.9" /> <PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.10" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.2" /> <PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="Verify" Version="23.2.2" /> <PackageReference Include="Verify" Version="23.5.2" />
<PackageReference Include="Verify.CommunityToolkit.Mvvm" Version="0.1.0" /> <PackageReference Include="Verify.CommunityToolkit.Mvvm" Version="0.1.0" />
<PackageReference Include="Verify.Avalonia" Version="1.0.1" /> <PackageReference Include="Verify.Avalonia" Version="1.0.1" />
<PackageReference Include="Verify.Phash" Version="3.1.0" /> <PackageReference Include="Verify.Phash" Version="3.1.0" />
<PackageReference Include="Verify.Xunit" Version="23.2.2" /> <PackageReference Include="Verify.Xunit" Version="23.5.2" />
<PackageReference Include="xunit" Version="2.7.0" /> <PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7"> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Loading…
Cancel
Save