Browse Source

Merge branch 'dev' into downmerge

pull/240/head
JT 1 year ago committed by GitHub
parent
commit
ee9937beab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      CHANGELOG.md
  2. 5
      Jenkinsfile
  3. 8
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  4. 2
      StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
  5. 1
      StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs
  6. 4
      StabilityMatrix.Avalonia/App.axaml
  7. 297
      StabilityMatrix.Avalonia/App.axaml.cs
  8. 40
      StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs
  9. 1
      StabilityMatrix.Avalonia/Behaviors/TextEditorToolTipBehavior.cs
  10. 1
      StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
  11. 10
      StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs
  12. 3
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  13. 4
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs
  14. 4
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs
  15. 10
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs
  16. 22
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs
  17. 10
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs
  18. 2
      StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs
  19. 2
      StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs
  20. 23
      StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs
  21. 2
      StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs
  22. 165
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml
  23. 42
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs
  24. 11
      StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml
  25. 6
      StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml.cs
  26. 1
      StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
  27. 156
      StabilityMatrix.Avalonia/Controls/ModelCard.axaml
  28. 10
      StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs
  29. 1
      StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs
  30. 5
      StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs
  31. 2
      StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs
  32. 6
      StabilityMatrix.Avalonia/Controls/SamplerCard.axaml.cs
  33. 14
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs
  34. 69
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml
  35. 5
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs
  36. 6
      StabilityMatrix.Avalonia/Controls/SeedCard.axaml.cs
  37. 5
      StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs
  38. 51
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  39. 56
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  40. 6
      StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs
  41. 8
      StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs
  42. 2
      StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
  43. 5
      StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs
  44. 123
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  45. 12
      StabilityMatrix.Avalonia/DesignData/MockApiFactory.cs
  46. 18
      StabilityMatrix.Avalonia/DesignData/MockDiscordRichPresenceService.cs
  47. 50
      StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs
  48. 12
      StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs
  49. 69
      StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs
  50. 12
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  51. 62
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  52. 47
      StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs
  53. 26
      StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs
  54. 22
      StabilityMatrix.Avalonia/DesignData/MockTrackedDownloadService.cs
  55. 30
      StabilityMatrix.Avalonia/DialogHelper.cs
  56. 16
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  57. 27
      StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
  58. 95
      StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs
  59. 12
      StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs
  60. 3
      StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs
  61. 36
      StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
  62. 13
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  63. 127
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  64. 200
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  65. 68
      StabilityMatrix.Avalonia/Languages/Resources.resx
  66. 72
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs
  67. 7
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs
  68. 197
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs
  69. 8
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs
  70. 1
      StabilityMatrix.Avalonia/Models/Inference/Prompt.cs
  71. 1
      StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs
  72. 3
      StabilityMatrix.Avalonia/Models/Inference/ViewState.cs
  73. 7
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  74. 5
      StabilityMatrix.Avalonia/Models/SharedState.cs
  75. 3
      StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs
  76. 4
      StabilityMatrix.Avalonia/Models/TagCompletion/TextCompletionRequest.cs
  77. 2
      StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs
  78. 3
      StabilityMatrix.Avalonia/Models/TextEditorPreset.cs
  79. 13
      StabilityMatrix.Avalonia/Program.cs
  80. 1
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  81. 26
      StabilityMatrix.Avalonia/Services/INotificationService.cs
  82. 46
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  83. 4
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  84. 41
      StabilityMatrix.Avalonia/Services/NotificationService.cs
  85. 15
      StabilityMatrix.Avalonia/Services/ServiceManager.cs
  86. 38
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  87. 3
      StabilityMatrix.Avalonia/Styles/ThemeColors.cs
  88. 6
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  89. 194
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  90. 2
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  91. 1
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
  92. 10
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  93. 4
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  94. 9
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  95. 5
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  96. 1
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  97. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs
  98. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  99. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs
  100. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs
  101. Some files were not shown because too many files have changed in this diff Show More

36
CHANGELOG.md

@ -5,10 +5,42 @@ 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.6.0
### Added
- Added **Output Sharing** option for all packages in the three-dots menu on the Packages page
- This will link the package's output folders to the relevant subfolders in the "Outputs" directory
- When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled
- Added **Outputs page** for viewing generated images from any package, or the shared output folder
- Added [Stable Diffusion WebUI/UX](https://github.com/anapnoe/stable-diffusion-webui-ux) package
- Added [Stable Diffusion WebUI-DirectML](https://github.com/lshqqytiger/stable-diffusion-webui-directml) package
- Added [kohya_ss](https://github.com/bmaltais/kohya_ss) package
- Added [Fooocus-ControlNet-SDXL](https://github.com/fenneishi/Fooocus-ControlNet-SDXL) package
- Added GPU compatibility badges to the installers
- Added filtering of "incompatible" packages (ones that do not support your GPU) to all installers
- This can be overridden by checking the new "Show All Packages" checkbox
- Added more launch options for Fooocus, such as the `--preset` option
- Added Ctrl+ScrollWheel to change image size in the inference output gallery and new Outputs page
- Added "No Images Found" placeholder for non-connected models on the Checkpoints tab
### Changed
- If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page
- Changed all package installs & updates to use git commands instead of downloading zip files
- The One-Click Installer now uses the new progress dialog with console
- NVIDIA GPU users will be updated to use CUDA 12.1 for ComfyUI & Fooocus packages for a slight performance improvement
- Update will occur the next time the package is updated, or on a fresh install
- Note: CUDA 12.1 is only available on Maxwell (GTX 900 series) and newer GPUs
- Improved Model Browser download stability with automatic retries for download errors
- Optimized page navigation and syntax formatting configurations to improve startup time
### Fixed
- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer
- Fixed Inference popup Install button not working on One-Click Installer
- Fixed Inference Prompt Completion window sometimes not showing while typing
- Fixed "Show Model Images" toggle on Checkpoints page sometimes displaying cut-off model images
- Fixed missing httpx package during Automatic1111 install
- Fixed some instances of localized text being cut off from controls being too small
## v2.5.7 ## v2.5.7
### Fixed ### Fixed
- Fixed error `got an unexpected keyword argument 'socket_options'` on fresh installs of Automatic1111 Stable Diffusion WebUI - Fixed error `got an unexpected keyword argument 'socket_options'` on fresh installs of Automatic1111 Stable Diffusion WebUI due to missing httpx dependency specification from gradio
due to missing httpx dependency specification from gradio
## v2.5.6 ## v2.5.6
### Added ### Added

5
Jenkinsfile vendored

@ -35,11 +35,6 @@ node("Diligence") {
stage('Publish Linux') { stage('Publish Linux') {
sh "/home/jenkins/.dotnet/tools/pupnet --runtime linux-x64 --kind appimage --app-version ${version} --clean -y" sh "/home/jenkins/.dotnet/tools/pupnet --runtime linux-x64 --kind appimage --app-version ${version} --clean -y"
} }
stage ('Archive Artifacts') {
archiveArtifacts artifacts: 'out/*.exe', followSymlinks: false
archiveArtifacts artifacts: 'Release/linux-x64/*.AppImage', followSymlinks: false
}
} }
} finally { } finally {
stage('Cleanup') { stage('Cleanup') {

8
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,12 +19,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.4" /> <PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.4" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="NLog" Version="5.2.3" /> <PackageReference Include="NLog" Version="5.2.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

2
StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs

@ -1,12 +1,10 @@
using System; using System;
using System.Threading; using System.Threading;
using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using FluentAvalonia.UI.Media.Animation;
namespace StabilityMatrix.Avalonia.Animations; namespace StabilityMatrix.Avalonia.Animations;

1
StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs

@ -2,7 +2,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace StabilityMatrix.Avalonia.Animations; namespace StabilityMatrix.Avalonia.Animations;

4
StabilityMatrix.Avalonia/App.axaml

@ -20,6 +20,8 @@
<ResourceInclude Source="Controls/CodeCompletion/CompletionListThemes.axaml"/> <ResourceInclude Source="Controls/CodeCompletion/CompletionListThemes.axaml"/>
<ResourceInclude Source="Styles/ContextMenuStyles.axaml"/> <ResourceInclude Source="Styles/ContextMenuStyles.axaml"/>
<ResourceInclude Source="Controls/EditorFlyouts.axaml"/> <ResourceInclude Source="Controls/EditorFlyouts.axaml"/>
<ResourceInclude Source="Controls/Scroll/BetterScrollViewer.axaml"/>
<ResourceInclude Source="Controls/ImageFolderCard.axaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<idcr:ControlRecycling x:Key="ControlRecyclingKey" /> <idcr:ControlRecycling x:Key="ControlRecyclingKey" />
@ -56,10 +58,10 @@
<StyleInclude Source="Controls/FrameCarousel.axaml"/> <StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/> <StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
<StyleInclude Source="Controls/SelectImageCard.axaml"/> <StyleInclude Source="Controls/SelectImageCard.axaml"/>
<StyleInclude Source="Controls/ImageFolderCard.axaml"/>
<StyleInclude Source="Controls/SharpenCard.axaml"/> <StyleInclude Source="Controls/SharpenCard.axaml"/>
<StyleInclude Source="Controls/FreeUCard.axaml"/> <StyleInclude Source="Controls/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Paginator.axaml"/> <StyleInclude Source="Controls/Paginator.axaml"/>
<StyleInclude Source="Controls/SelectableImageCard/SelectableImageButton.axaml"/>
<Style Selector="DockControl"> <Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" /> <Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

297
StabilityMatrix.Avalonia/App.axaml.cs

@ -39,42 +39,25 @@ using Polly.Extensions.Http;
using Polly.Timeout; using Polly.Timeout;
using Refit; using Refit;
using Sentry; using Sentry;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Avalonia.Views.Inference;
using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Converters.Json; 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.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory;
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.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color; using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@ -249,18 +232,6 @@ public sealed class App : Application
internal static void ConfigurePageViewModels(IServiceCollection services) internal static void ConfigurePageViewModels(IServiceCollection services)
{ {
services
.AddSingleton<PackageManagerViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<InferenceSettingsViewModel>()
.AddSingleton<CheckpointBrowserViewModel>()
.AddSingleton<CheckpointsPageViewModel>()
.AddSingleton<NewCheckpointsPageViewModel>()
.AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>()
.AddSingleton<InferenceViewModel>()
.AddSingleton<ProgressManagerViewModel>();
services.AddSingleton<MainWindowViewModel>( services.AddSingleton<MainWindowViewModel>(
provider => provider =>
new MainWindowViewModel( new MainWindowViewModel(
@ -278,6 +249,7 @@ public sealed class App : Application
provider.GetRequiredService<PackageManagerViewModel>(), provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(), provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(), provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
}, },
FooterPages = { provider.GetRequiredService<SettingsViewModel>() } FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
} }
@ -287,186 +259,103 @@ public sealed class App : Application
services.AddSingleton<IDisposable>(p => p.GetRequiredService<LaunchPageViewModel>()); services.AddSingleton<IDisposable>(p => p.GetRequiredService<LaunchPageViewModel>());
} }
internal static void ConfigureDialogViewModels(IServiceCollection services) internal static void ConfigureDialogViewModels(
{ IServiceCollection services,
// Dialog view models (transient) Type[] exportedTypes
services.AddTransient<InstallerViewModel>(); )
services.AddTransient<OneClickInstallViewModel>(); {
services.AddTransient<SelectModelVersionViewModel>();
services.AddTransient<SelectDataDirectoryViewModel>();
services.AddTransient<LaunchOptionsViewModel>();
services.AddTransient<ExceptionViewModel>();
services.AddTransient<EnvVarsViewModel>();
services.AddTransient<ImageViewerViewModel>();
services.AddTransient<PackageImportViewModel>();
services.AddTransient<InferenceConnectionHelpViewModel>();
services.AddTransient<DownloadResourceViewModel>();
// Dialog view models (singleton)
services.AddSingleton<FirstLaunchSetupViewModel>();
services.AddSingleton<UpdateViewModel>();
// Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>();
services.AddTransient<CheckpointFile>();
services.AddTransient<InferenceTextToImageViewModel>();
services.AddTransient<InferenceImageUpscaleViewModel>();
services.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<PackageCardViewModel>();
// Global progress
services.AddSingleton<ProgressManagerViewModel>();
// Controls
services.AddTransient<RefreshBadgeViewModel>();
// Inference controls
services.AddTransient<SeedCardViewModel>();
services.AddTransient<SamplerCardViewModel>();
services.AddTransient<UpscalerCardViewModel>();
services.AddTransient<ImageGalleryCardViewModel>();
services.AddTransient<ImageFolderCardViewModel>();
services.AddTransient<PromptCardViewModel>();
services.AddTransient<StackCardViewModel>();
services.AddTransient<StackExpanderViewModel>();
services.AddTransient<ModelCardViewModel>();
services.AddTransient<BatchSizeCardViewModel>();
services.AddTransient<SelectImageCardViewModel>();
services.AddTransient<SharpenCardViewModel>();
services.AddTransient<FreeUCardViewModel>();
// Dialog factory // Dialog factory
services.AddSingleton<ServiceManager<ViewModelBase>>( services.AddSingleton<ServiceManager<ViewModelBase>>(provider =>
provider => {
new ServiceManager<ViewModelBase>() var serviceManager = new ServiceManager<ViewModelBase>();
.Register(provider.GetRequiredService<InstallerViewModel>)
.Register(provider.GetRequiredService<OneClickInstallViewModel>) var serviceManagedTypes = exportedTypes
.Register(provider.GetRequiredService<SelectModelVersionViewModel>) .Select(
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>) t =>
.Register(provider.GetRequiredService<LaunchOptionsViewModel>) new
.Register(provider.GetRequiredService<UpdateViewModel>) {
.Register(provider.GetRequiredService<CheckpointBrowserCardViewModel>) t,
.Register(provider.GetRequiredService<CheckpointFolder>) attributes = t.GetCustomAttributes(
.Register(provider.GetRequiredService<CheckpointFile>) typeof(ManagedServiceAttribute),
.Register(provider.GetRequiredService<PackageCardViewModel>) true
.Register(provider.GetRequiredService<RefreshBadgeViewModel>) )
.Register(provider.GetRequiredService<ExceptionViewModel>) }
.Register(provider.GetRequiredService<EnvVarsViewModel>) )
.Register(provider.GetRequiredService<ProgressManagerViewModel>) .Where(t1 => t1.attributes is { Length: > 0 })
.Register(provider.GetRequiredService<InferenceTextToImageViewModel>) .Select(t1 => t1.t)
.Register(provider.GetRequiredService<InferenceImageUpscaleViewModel>) .ToList();
.Register(provider.GetRequiredService<SeedCardViewModel>)
.Register(provider.GetRequiredService<SamplerCardViewModel>) foreach (var type in serviceManagedTypes)
.Register(provider.GetRequiredService<ImageGalleryCardViewModel>) {
.Register(provider.GetRequiredService<ImageFolderCardViewModel>) ViewModelBase? GetInstance() => provider.GetRequiredService(type) as ViewModelBase;
.Register(provider.GetRequiredService<PromptCardViewModel>) serviceManager.Register(type, GetInstance);
.Register(provider.GetRequiredService<StackCardViewModel>) }
.Register(provider.GetRequiredService<StackExpanderViewModel>)
.Register(provider.GetRequiredService<UpscalerCardViewModel>) return serviceManager;
.Register(provider.GetRequiredService<ModelCardViewModel>) });
.Register(provider.GetRequiredService<BatchSizeCardViewModel>)
.Register(provider.GetRequiredService<ImageViewerViewModel>)
.Register(provider.GetRequiredService<FirstLaunchSetupViewModel>)
.Register(provider.GetRequiredService<PackageImportViewModel>)
.Register(provider.GetRequiredService<SelectImageCardViewModel>)
.Register(provider.GetRequiredService<InferenceConnectionHelpViewModel>)
.Register(provider.GetRequiredService<SharpenCardViewModel>)
.Register(provider.GetRequiredService<DownloadResourceViewModel>)
.Register(provider.GetRequiredService<FreeUCardViewModel>)
);
} }
internal static void ConfigureViews(IServiceCollection services) internal static IServiceCollection ConfigureServices()
{
// Pages
services.AddSingleton<CheckpointsPage>();
services.AddSingleton<LaunchPageView>();
services.AddSingleton<PackageManagerPage>();
services.AddSingleton<SettingsPage>();
services.AddSingleton<InferenceSettingsPage>();
services.AddSingleton<CheckpointBrowserPage>();
services.AddSingleton<ProgressManagerPage>();
services.AddSingleton<InferencePage>();
services.AddSingleton<NewCheckpointsPage>();
// Inference tabs
services.AddTransient<InferenceTextToImageView>();
services.AddTransient<InferenceImageUpscaleView>();
// Inference controls
services.AddTransient<ImageGalleryCard>();
services.AddTransient<ImageFolderCard>();
services.AddTransient<SeedCard>();
services.AddTransient<SamplerCard>();
services.AddTransient<PromptCard>();
services.AddTransient<StackCard>();
services.AddTransient<StackExpander>();
services.AddTransient<UpscalerCard>();
services.AddTransient<ModelCard>();
services.AddTransient<BatchSizeCard>();
services.AddTransient<SelectImageCard>();
services.AddTransient<SharpenCard>();
services.AddTransient<FreeUCard>();
// Dialogs
services.AddTransient<SelectDataDirectoryDialog>();
services.AddTransient<LaunchOptionsDialog>();
services.AddTransient<UpdateDialog>();
services.AddTransient<ExceptionDialog>();
services.AddTransient<EnvVarsDialog>();
services.AddTransient<ImageViewerDialog>();
services.AddTransient<PackageImportDialog>();
services.AddTransient<InferenceConnectionHelpDialog>();
services.AddTransient<DownloadResourceDialog>();
// Controls
services.AddTransient<RefreshBadge>();
// Windows
services.AddSingleton<MainWindow>();
services.AddSingleton<FirstLaunchSetupWindow>();
}
internal static void ConfigurePackages(IServiceCollection services)
{
services.AddSingleton<BasePackage, A3WebUI>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, VladAutomatic>();
services.AddSingleton<BasePackage, InvokeAI>();
services.AddSingleton<BasePackage, ComfyUI>();
services.AddSingleton<BasePackage, VoltaML>();
services.AddSingleton<BasePackage, FooocusMre>();
}
private static IServiceCollection ConfigureServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddMemoryCache(); services.AddMemoryCache();
var exportedTypes = AppDomain.CurrentDomain
.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
var transientTypes = exportedTypes
.Select(
t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), true) }
)
.Where(
t1 =>
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] });
foreach (var typePair in transientTypes)
{
if (typePair.Attribute.InterfaceType is null)
{
services.AddTransient(typePair.Type);
}
else
{
services.AddTransient(typePair.Attribute.InterfaceType, typePair.Type);
}
}
var singletonTypes = exportedTypes
.Select(
t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), true) }
)
.Where(
t1 =>
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (SingletonAttribute)t1.attributes[0] });
foreach (var typePair in singletonTypes)
{
if (typePair.Attribute.InterfaceType is null)
{
services.AddSingleton(typePair.Type);
}
else
{
services.AddSingleton(typePair.Attribute.InterfaceType, typePair.Type);
}
}
ConfigurePageViewModels(services); ConfigurePageViewModels(services);
ConfigureDialogViewModels(services); ConfigureDialogViewModels(services, exportedTypes);
ConfigurePackages(services);
// Other services // Other services
services.AddSingleton<ISettingsManager, SettingsManager>();
services.AddSingleton<ISharedFolders, SharedFolders>();
services.AddSingleton<SharedState>();
services.AddSingleton<ModelFinder>();
services.AddSingleton<IPackageFactory, PackageFactory>();
services.AddSingleton<IDownloadService, DownloadService>();
services.AddSingleton<IGithubApiCache, GithubApiCache>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IPyRunner, PyRunner>();
services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IInferenceClientManager, InferenceClientManager>();
services.AddSingleton<ICompletionProvider, CompletionProvider>();
services.AddSingleton<ITokenizerProvider, TokenizerProvider>();
services.AddSingleton<IModelIndexService, ModelIndexService>();
services.AddTransient<IPackageModificationRunner, PackageModificationRunner>();
services.AddSingleton<IImageIndexService, ImageIndexService>();
services.AddSingleton<ITrackedDownloadService, TrackedDownloadService>(); services.AddSingleton<ITrackedDownloadService, TrackedDownloadService>();
services.AddSingleton<IDisposable>( services.AddSingleton<IDisposable>(
provider => (IDisposable)provider.GetRequiredService<ITrackedDownloadService>() provider => (IDisposable)provider.GetRequiredService<ITrackedDownloadService>()
@ -494,13 +383,7 @@ public sealed class App : Application
services.AddSingleton<IPrerequisiteHelper, UnixPrerequisiteHelper>(); services.AddSingleton<IPrerequisiteHelper, UnixPrerequisiteHelper>();
} }
ConfigureViews(services); if (!Design.IsDesignMode)
if (Design.IsDesignMode)
{
services.AddSingleton<ILiteDbContext, MockLiteDbContext>();
}
else
{ {
services.AddSingleton<ILiteDbContext, LiteDbContext>(); services.AddSingleton<ILiteDbContext, LiteDbContext>();
services.AddSingleton<IDisposable>(p => p.GetRequiredService<ILiteDbContext>()); services.AddSingleton<IDisposable>(p => p.GetRequiredService<ILiteDbContext>());

40
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -1,9 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Runtime.CompilerServices;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Xaml.Interactivity; using Avalonia.Xaml.Interactivity;
@ -100,6 +99,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
return window; return window;
} }
[MethodImpl(MethodImplOptions.Synchronized)]
public void InvokeManualCompletion() public void InvokeManualCompletion()
{ {
if (CompletionProvider is null) if (CompletionProvider is null)
@ -109,11 +109,13 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
// If window already open, skip since handled by completion window // If window already open, skip since handled by completion window
// Unless this is an end char, where we'll open a new window // Unless this is an end char, where we'll open a new window
if (completionWindow is { IsOpen: true }) if (completionWindow is { ToolTipIsOpen: true })
{ {
Logger.ConditionalTrace("Skipping, completion window already open"); Logger.ConditionalTrace("Skipping, completion window already open");
return; return;
} }
completionWindow?.Hide();
completionWindow = null;
// Get the segment of the token the caret is currently in // Get the segment of the token the caret is currently in
if (GetCaretCompletionToken() is not { } completionRequest) if (GetCaretCompletionToken() is not { } completionRequest)
@ -138,23 +140,37 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
var token = textEditor.Document.GetText(tokenSegment); var token = textEditor.Document.GetText(tokenSegment);
Logger.ConditionalTrace("Using token {Token} ({@Segment})", token, tokenSegment); Logger.ConditionalTrace("Using token {Token} ({@Segment})", token, tokenSegment);
completionWindow = CreateCompletionWindow(textEditor.TextArea); var newWindow = CreateCompletionWindow(textEditor.TextArea);
completionWindow.StartOffset = tokenSegment.Offset; newWindow.StartOffset = tokenSegment.Offset;
completionWindow.EndOffset = tokenSegment.EndOffset; newWindow.EndOffset = tokenSegment.EndOffset;
newWindow.UpdateQuery(completionRequest);
newWindow.Closed += CompletionWindow_OnClosed;
completionWindow.UpdateQuery(completionRequest); completionWindow = newWindow;
completionWindow.Closed += (_, _) => newWindow.Show();
}
private void CompletionWindow_OnClosed(object? sender, EventArgs e)
{
if (ReferenceEquals(sender, completionWindow))
{ {
completionWindow = null; completionWindow = null;
}; }
Logger.ConditionalTrace("Completion window closed");
completionWindow.Show(); if (sender is CompletionWindow window)
{
window.Closed -= CompletionWindow_OnClosed;
}
} }
private void TextArea_TextEntered(object? sender, TextInputEventArgs e) private void TextArea_TextEntered(object? sender, TextInputEventArgs e)
{ {
Logger.ConditionalTrace($"Text entered: {e.Text.ToRepr()}"); Logger.ConditionalTrace("Text entered: {Text}", e.Text);
if (!IsEnabled || CompletionProvider is null) if (!IsEnabled || CompletionProvider is null)
{ {
@ -174,7 +190,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
return; return;
} }
InvokeManualCompletion(); Dispatcher.UIThread.Post(InvokeManualCompletion, DispatcherPriority.Input);
} }
private void TextArea_KeyDown(object? sender, KeyEventArgs e) private void TextArea_KeyDown(object? sender, KeyEventArgs e)

1
StabilityMatrix.Avalonia/Behaviors/TextEditorToolTipBehavior.cs

@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Xaml.Interactivity; using Avalonia.Xaml.Interactivity;

1
StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs

@ -22,7 +22,6 @@ using AsyncAwaitBestPractices;
using AsyncImageLoader; using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;

10
StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs

@ -1,9 +1,7 @@
using Avalonia; using Avalonia.Controls.Primitives;
using Avalonia.Controls; using StabilityMatrix.Core.Attributes;
using Avalonia.Controls.Primitives;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
public class BatchSizeCard : TemplatedControl [Transient]
{ public class BatchSizeCard : TemplatedControl { }
}

3
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Reflection; using System.Reflection;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
@ -11,8 +10,6 @@ using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using Brushes = Avalonia.Media.Brushes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;

4
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs

@ -1,9 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Media;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
using AvaloniaEdit.Editing; using AvaloniaEdit.Editing;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;

4
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs

@ -1,6 +1,4 @@
using System; using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Projektanker.Icons.Avalonia;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.Styles;

10
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs

@ -20,10 +20,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -31,8 +29,6 @@ using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion;
@ -202,11 +198,11 @@ public class CompletionList : TemplatedControl
{ {
case Key.Down: case Key.Down:
e.Handled = true; e.Handled = true;
_listBox.SelectIndex(_listBox.SelectedIndex + 1); _listBox.SelectNextIndexWithLoop();
break; break;
case Key.Up: case Key.Up:
e.Handled = true; e.Handled = true;
_listBox.SelectIndex(_listBox.SelectedIndex - 1); _listBox.SelectPreviousIndexWithLoop();
break; break;
case Key.PageDown: case Key.PageDown:
e.Handled = true; e.Handled = true;
@ -325,7 +321,7 @@ public class CompletionList : TemplatedControl
return; return;
} }
using var _ = new CodeTimer(); using var _ = CodeTimer.StartDebug();
if (_listBox == null) if (_listBox == null)
{ {

22
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs

@ -91,6 +91,28 @@ public class CompletionListBox : ListBox
SelectedIndex = -1; SelectedIndex = -1;
} }
/// <summary>
/// Selects the next item. If the last item is already selected, selects the first item.
/// </summary>
public void SelectNextIndexWithLoop()
{
if (ItemCount <= 0)
return;
SelectIndex((SelectedIndex + 1) % ItemCount);
}
/// <summary>
/// Selects the previous item. If the first item is already selected, selects the last item.
/// </summary>
public void SelectPreviousIndexWithLoop()
{
if (ItemCount <= 0)
return;
SelectIndex((SelectedIndex - 1 + ItemCount) % ItemCount);
}
/// <summary> /// <summary>
/// Selects the item with the specified index and scrolls it into view. /// Selects the item with the specified index and scrolls it into view.
/// </summary> /// </summary>

10
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs

@ -18,8 +18,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -44,6 +42,8 @@ public class CompletionWindow : CompletionWindowBase
private PopupWithCustomPosition? _toolTip; private PopupWithCustomPosition? _toolTip;
private ContentControl? _toolTipContent; private ContentControl? _toolTipContent;
public bool ToolTipIsOpen => _toolTip is not null;
/// <summary> /// <summary>
/// Max number of items in the completion list. /// Max number of items in the completion list.
/// </summary> /// </summary>
@ -93,7 +93,11 @@ public class CompletionWindow : CompletionWindowBase
LogicalChildren.Add(_toolTip); LogicalChildren.Add(_toolTip);
//_toolTip.Closed += (o, e) => ((Popup)o).Child = null; _toolTip.Closed += (s, e) =>
{
Hide();
// (s as Popup)!.Child = null;
};
AttachEvents(); AttachEvents();
} }

2
StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs

@ -19,10 +19,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Media;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
using AvaloniaEdit.Editing; using AvaloniaEdit.Editing;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion;

2
StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
@ -9,7 +8,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Threading; using Avalonia.Threading;
using Dock.Avalonia.Controls; using Dock.Avalonia.Controls;
using Dock.Model; using Dock.Model;
using Dock.Model.Avalonia.Json;
using Dock.Model.Core; using Dock.Model.Core;
using Dock.Serializer; using Dock.Serializer;
using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Models.Inference;

23
StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs

@ -15,11 +15,13 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")]
public class FASymbolIconSource : PathIconSource public class FASymbolIconSource : PathIconSource
{ {
public static readonly StyledProperty<string> SymbolProperty = public static readonly StyledProperty<string> SymbolProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<FASymbolIconSource, string>(nameof(Symbol)); FASymbolIconSource,
string
public static readonly StyledProperty<double> FontSizeProperty = TextElement.FontSizeProperty.AddOwner<FASymbolIconSource>(); >(nameof(Symbol));
public static readonly StyledProperty<double> FontSizeProperty =
TextElement.FontSizeProperty.AddOwner<FASymbolIconSource>();
public FASymbolIconSource() public FASymbolIconSource()
{ {
@ -52,7 +54,7 @@ public class FASymbolIconSource : PathIconSource
private void InvalidateData() private void InvalidateData()
{ {
var path = IconProvider.Current.GetIconPath(Symbol); var path = IconProvider.Current.GetIcon(Symbol).Path;
var geometry = Geometry.Parse(path); var geometry = Geometry.Parse(path);
var scale = FontSize / 20; var scale = FontSize / 20;
@ -74,14 +76,15 @@ public class FASymbolIconSourceConverter : TypeConverter
return base.CanConvertFrom(context, sourceType); return base.CanConvertFrom(context, sourceType);
} }
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) public override object? ConvertFrom(
ITypeDescriptorContext? context,
CultureInfo? culture,
object value
)
{ {
return value switch return value switch
{ {
string val => new FASymbolIconSource string val => new FASymbolIconSource { Symbol = val, },
{
Symbol = val,
},
_ => base.ConvertFrom(context, culture, value) _ => base.ConvertFrom(context, culture, value)
}; };
} }

2
StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs

@ -1,5 +1,7 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class FreeUCard : TemplatedControl { } public class FreeUCard : TemplatedControl { }

165
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -1,17 +1,14 @@
<Styles <ResourceDictionary
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:input="using:FluentAvalonia.UI.Input"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:input="using:FluentAvalonia.UI.Input"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
xmlns:animations="clr-namespace:StabilityMatrix.Avalonia.Animations"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors"
x:DataType="vmInference:ImageFolderCardViewModel"> x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith> <Design.PreviewWith>
@ -22,15 +19,11 @@
</Panel> </Panel>
</Design.PreviewWith> </Design.PreviewWith>
<ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<!--<Style Selector="ListBox /template/ VirtualizingStackPanel"> <Setter Property="Background" Value="Transparent" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>-->
<Style Selector="controls|ImageFolderCard">
<!-- Set Defaults --> <!-- Set Defaults -->
<Setter Property="CornerRadius" Value="8" /> <Setter Property="CornerRadius" Value="8" />
<Setter Property="Focusable" Value="True" />
<!-- Template --> <!-- Template -->
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
@ -50,10 +43,10 @@
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<TextBox <TextBox
x:Name="SearchBox" x:Name="SearchBox"
Text="{Binding SearchQuery}"
Margin="6,4,6,8" Margin="6,4,6,8"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}"> Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent> <TextBox.InnerRightContent>
<ui:SymbolIcon <ui:SymbolIcon
@ -63,72 +56,73 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
<ScrollViewer <scroll:BetterScrollViewer
Grid.Row="1" Grid.Row="1"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory"
BringIntoViewOnFocusChange="False" BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory">
<ItemsRepeater <ItemsRepeater
Name="ImageRepeater"
x:Name="ImageRepeater"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
VerticalCacheLength="8" ItemsSource="{Binding LocalImages}"
ItemsSource="{Binding LocalImages}"> VerticalCacheLength="8">
<ItemsRepeater.Resources> <ItemsRepeater.Resources>
<input:StandardUICommand <input:StandardUICommand x:Key="ImageClickCommand" Command="{Binding ImageClickCommand}" />
x:Key="ImageClickCommand"
Command="{Binding ImageClickCommand}"/>
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageDeleteCommand" x:Key="ImageDeleteCommand"
Label="{x:Static lang:Resources.Action_Delete}" Command="{Binding ImageDeleteCommand}"
IconSource="Delete" IconSource="Delete"
Command="{Binding ImageDeleteCommand}"/> Label="{x:Static lang:Resources.Action_Delete}" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageCopyCommand" x:Key="ImageCopyCommand"
Label="Copy" Command="{Binding ImageCopyCommand}"
IconSource="Copy" IconSource="Copy"
Command="{Binding ImageCopyCommand}"/> Label="Copy" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageOpenCommand" x:Key="ImageOpenCommand"
Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer}, macOS={x:Static lang:Resources.Action_OpenInFinder}}" Command="{Binding ImageOpenCommand}"
IconSource="Folder" IconSource="Folder"
Command="{Binding ImageOpenCommand}"/> Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer},
macOS={x:Static lang:Resources.Action_OpenInFinder}}" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageExportPngWithMetadataCommand" x:Key="ImageExportPngWithMetadataCommand"
Label="Png with Metadata" Command="{Binding ImageExportPngWithMetadataCommand}"
Command="{Binding ImageExportPngWithMetadataCommand}"/> Label="Png with Metadata" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageExportPngCommand" x:Key="ImageExportPngCommand"
Label="Png" Command="{Binding ImageExportPngCommand}"
Command="{Binding ImageExportPngCommand}"/> Label="Png" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageExportJpegCommand" x:Key="ImageExportJpegCommand"
Label="Jpeg" Command="{Binding ImageExportJpegCommand}"
Command="{Binding ImageExportJpegCommand}"/> Label="Jpeg" />
<input:StandardUICommand <input:StandardUICommand
x:Key="ImageExportWebpCommand" x:Key="ImageExportWebpCommand"
Label="Webp" Command="{Binding ImageExportWebpCommand}"
Command="{Binding ImageExportWebpCommand}"/> Label="Webp" />
</ItemsRepeater.Resources> </ItemsRepeater.Resources>
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<!--<WrapLayout HorizontalSpacing="2" VerticalSpacing="2" />--> <!--<WrapLayout HorizontalSpacing="2" VerticalSpacing="2" />-->
<UniformGridLayout <UniformGridLayout
Orientation="Horizontal"
MinColumnSpacing="2" MinColumnSpacing="2"
MinRowSpacing="2" /> MinRowSpacing="2"
Orientation="Horizontal" />
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type dbModels:LocalImageFile}"> <DataTemplate DataType="{x:Type dbModels:LocalImageFile}">
<Button <Button
ToolTip.Placement="LeftEdgeAlignedTop" Padding="4"
Classes="transparent-full"
Command="{StaticResource ImageClickCommand}" Command="{StaticResource ImageClickCommand}"
CommandParameter="{Binding $self.DataContext}" CommandParameter="{Binding $self.DataContext}"
CornerRadius="8"
HotKey="{x:Null}" HotKey="{x:Null}"
Padding="4" ToolTip.Placement="LeftEdgeAlignedTop">
Classes="transparent-full"
CornerRadius="8">
<Interaction.Behaviors> <Interaction.Behaviors>
<BehaviorCollection> <BehaviorCollection>
@ -139,66 +133,95 @@
<Button.ContextFlyout> <Button.ContextFlyout>
<ui:FAMenuFlyout> <ui:FAMenuFlyout>
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageDeleteCommand}" Command="{StaticResource ImageDeleteCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageCopyCommand}" Command="{StaticResource ImageCopyCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutSeparator /> <ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageOpenCommand}" Command="{StaticResource ImageOpenCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
<ui:MenuFlyoutSubItem Text="Export" IconSource="Share"> HotKey="{x:Null}" />
<ui:MenuFlyoutSubItem IconSource="Share" Text="Export">
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportPngWithMetadataCommand}" Command="{StaticResource ImageExportPngWithMetadataCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportPngCommand}" Command="{StaticResource ImageExportPngCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportJpegCommand}" Command="{StaticResource ImageExportJpegCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem <ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportWebpCommand}" Command="{StaticResource ImageExportWebpCommand}"
CommandParameter="{Binding $self.DataContext}"/> CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
</ui:MenuFlyoutSubItem> </ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout> </ui:FAMenuFlyout>
</Button.ContextFlyout> </Button.ContextFlyout>
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="{Binding GenerationParameters.PositivePrompt}"/> <StackPanel Spacing="4">
<StackPanel.Styles>
<Style Selector="StackPanel > TextBlock">
<Setter Property="Theme" Value="{DynamicResource BodyTextBlockStyle}"/>
</Style>
<Style Selector="SelectableTextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Cascadia Code,Consolas,Menlo,Monospace"/>
</Style>
<Style Selector="controls|Card">
<Setter Property="Padding" Value="4,4" />
</Style>
</StackPanel.Styles>
<TextBlock Text="{Binding FileName}" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<controls:Card>
<SelectableTextBlock Text="{Binding GenerationParameters.PositivePrompt, FallbackValue=''}" />
</controls:Card>
<controls:Card>
<SelectableTextBlock Text="{Binding GenerationParameters.NegativePrompt, FallbackValue=''}" />
</controls:Card>
<TextBlock Text="{Binding GenerationParameters.ModelName, FallbackValue=''}" />
<TextBlock Text="{Binding GenerationParameters.Sampler, FallbackValue=''}" />
<TextBlock Text="{Binding GenerationParameters.Seed, FallbackValue=''}" />
</StackPanel>
</ToolTip.Tip> </ToolTip.Tip>
<Border ClipToBounds="True" CornerRadius="8"> <Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Width="150" Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="190" Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding GlobalFullPath}" Source="{Binding AbsolutePath}"
Stretch="UniformToFill" Stretch="UniformToFill"
StretchDirection="Both" /> StretchDirection="Both" />
<Border <Border
Grid.Row="1" Grid.Row="1"
Classes="theme-dark"
MinHeight="20" MinHeight="20"
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Padding="4,0,0,0" Padding="4,0,0,0"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Classes="theme-dark"
Opacity="0.9"> Opacity="0.9">
<TextBlock <TextBlock
FontSize="12"
VerticalAlignment="Center" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" FontSize="12"
Text="{Binding FileNameWithoutExtension}"/> Text="{Binding FileNameWithoutExtension}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
@ -206,10 +229,10 @@
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </scroll:BetterScrollViewer>
</Grid> </Grid>
</controls:Card> </controls:Card>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </ControlTheme>
</Styles> </ResourceDictionary>

42
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs

@ -1,9 +1,23 @@
using Avalonia.Input; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class ImageFolderCard : DropTargetTemplatedControlBase public class ImageFolderCard : DropTargetTemplatedControlBase
{ {
private ItemsRepeater? imageRepeater;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
imageRepeater = e.NameScope.Find<ItemsRepeater>("ImageRepeater");
base.OnApplyTemplate(e);
}
/// <inheritdoc /> /// <inheritdoc />
protected override void DropHandler(object? sender, DragEventArgs e) protected override void DropHandler(object? sender, DragEventArgs e)
{ {
@ -17,4 +31,30 @@ public class ImageFolderCard : DropTargetTemplatedControlBase
base.DragOverHandler(sender, e); base.DragOverHandler(sender, e);
e.Handled = true; e.Handled = true;
} }
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
if (e.KeyModifiers != KeyModifiers.Control)
return;
if (DataContext is not ImageFolderCardViewModel vm)
return;
if (e.Delta.Y > 0)
{
if (vm.ImageSize.Height >= 500)
return;
vm.ImageSize += new Size(15, 19);
}
else
{
if (vm.ImageSize.Height <= 200)
return;
vm.ImageSize -= new Size(15, 19);
}
imageRepeater?.InvalidateArrange();
imageRepeater?.InvalidateMeasure();
e.Handled = true;
}
} }

11
StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml

@ -13,11 +13,6 @@
</Grid> </Grid>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="controls|ImageGalleryCard"> <Style Selector="controls|ImageGalleryCard">
<!-- Set Defaults --> <!-- Set Defaults -->
<Setter Property="Template"> <Setter Property="Template">
@ -31,6 +26,12 @@
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>
</Grid.Styles>
<!-- Main image view --> <!-- Main image view -->
<Border <Border
Classes="theme-dark" Classes="theme-dark"

6
StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml.cs

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

1
StabilityMatrix.Avalonia/Controls/LineDashFrame.cs

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;

156
StabilityMatrix.Avalonia/Controls/ModelCard.axaml

@ -1,15 +1,16 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="inference:ModelCardViewModel"> x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith> <Design.PreviewWith>
<Panel Width="400" Height="200"> <Panel Width="400" Height="200">
<StackPanel VerticalAlignment="Center" Width="300"> <StackPanel Width="300" VerticalAlignment="Center">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" /> <controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" />
</StackPanel> </StackPanel>
</Panel> </Panel>
@ -20,22 +21,125 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<controls:Card Padding="12"> <controls:Card Padding="12">
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<!-- Model --> <!-- Model -->
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
TextAlignment="Left"
VerticalAlignment="Center"
MinWidth="60" MinWidth="60"
Text="{x:Static lang:Resources.Label_Model}" /> VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox <ui:FAComboBox
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
IsTextSearchEnabled="True"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}" ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedModel}"> SelectedItem="{Binding SelectedModel}">
<ui:FAComboBox.Styles>
<Style Selector="ui|FAComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid RowDefinitions="Auto,Auto,*" VerticalAlignment="Stretch" MaxWidth="300">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding RelativePath}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="ui|FAComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:FAComboBox.Styles>
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<StackPanel ToolTip.Placement="RightEdgeAlignedTop">
<!--<ToolTip.Tip>
<StackPanel Orientation="Horizontal" Spacing="6">
~1~ Image @1@
<controls:BetterAdvancedImage
RenderOptions.BitmapInterpolationMode="HighQuality"
Height="96"
Width="64"
CornerRadius="6"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}" />
<StackPanel Spacing="4">
~1~ Title @1@
<TextBlock
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Left"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"/>
~1~ Version @1@
<TextBlock
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Left"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" />
~1~ Path @1@
<TextBlock
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Left"
Text="{Binding FileName}"
TextWrapping="WrapWithOverflow"/>
</StackPanel>
</StackPanel>
</ToolTip.Tip>-->
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
<!--<ui:FAComboBox.ItemTemplate> <!--<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile"> <DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*"> <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
@ -75,60 +179,56 @@
<ui:SymbolIcon FontSize="16" Symbol="Setting" /> <ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout> <Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft"> <ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem <ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
IsChecked="{Binding IsRefinerSelectionEnabled}" <ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
Text="{x:Static lang:Resources.Label_Refiner}"/>
<ui:ToggleMenuFlyoutItem
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"/>
</ui:FAMenuFlyout> </ui:FAMenuFlyout>
</Button.Flyout> </Button.Flyout>
</Button> </Button>
<!-- Refiner --> <!-- Refiner -->
<TextBlock <TextBlock
Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Grid.Column="0"
MinWidth="60" MinWidth="60"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Margin="0,8,0,0" Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Refiner}" /> IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox <ui:FAComboBox
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Margin="0,8,0,0" Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True" IsTextSearchEnabled="True"
IsVisible="{Binding IsRefinerSelectionEnabled}" IsVisible="{Binding IsRefinerSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}" ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedRefiner}" /> SelectedItem="{Binding SelectedRefiner}" />
<!-- VAE --> <!-- VAE -->
<TextBlock <TextBlock
Grid.Column="0"
Grid.Row="2" Grid.Row="2"
Grid.Column="0"
MinWidth="60" MinWidth="60"
IsVisible="{Binding IsVaeSelectionEnabled}"
Margin="0,8,0,0" Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VAE}" /> IsVisible="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"
TextAlignment="Left" />
<ui:FAComboBox <ui:FAComboBox
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Margin="0,8,0,0" Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True" IsTextSearchEnabled="True"
IsVisible="{Binding IsVaeSelectionEnabled}" IsVisible="{Binding IsVaeSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.VaeModels}" ItemsSource="{Binding ClientManager.VaeModels}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedVae}" /> SelectedItem="{Binding SelectedVae}" />
</Grid> </Grid>

10
StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs

@ -1,9 +1,7 @@
using Avalonia; using Avalonia.Controls.Primitives;
using Avalonia.Controls; using StabilityMatrix.Core.Attributes;
using Avalonia.Controls.Primitives;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
public class ModelCard : TemplatedControl [Transient]
{ public class ModelCard : TemplatedControl { }
}

1
StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs

@ -2,7 +2,6 @@
using System.Windows.Input; using System.Windows.Input;
using Avalonia; using Avalonia;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;

5
StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs

@ -6,9 +6,12 @@ using AvaloniaEdit;
using AvaloniaEdit.Editing; using AvaloniaEdit.Editing;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptCard : TemplatedControl public class PromptCard : TemplatedControl
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -31,7 +34,7 @@ public class PromptCard : TemplatedControl
{ {
if (editor is not null) if (editor is not null)
{ {
TextEditorConfigs.ConfigForPrompt(editor); TextEditorConfigs.Configure(editor, TextEditorPreset.Prompt);
editor.TextArea.Margin = new Thickness(0, 0, 4, 0); editor.TextArea.Margin = new Thickness(0, 0, 4, 0);
if (editor.TextArea.ActiveInputHandler is TextAreaInputHandler inputHandler) if (editor.TextArea.ActiveInputHandler is TextAreaInputHandler inputHandler)

2
StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs

@ -1,7 +1,9 @@
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public partial class RefreshBadge : UserControlBase public partial class RefreshBadge : UserControlBase
{ {
public RefreshBadge() public RefreshBadge()

6
StabilityMatrix.Avalonia/Controls/SamplerCard.axaml.cs

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

14
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs

@ -0,0 +1,14 @@
using Avalonia.Controls.Presenters;
using Avalonia.Input;
namespace StabilityMatrix.Avalonia.Controls.Scroll;
public class BetterScrollContentPresenter : ScrollContentPresenter
{
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
if (e.KeyModifiers == KeyModifiers.Control)
return;
base.OnPointerWheelChanged(e);
}
}

69
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml

@ -0,0 +1,69 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll">
<Design.PreviewWith>
<scroll:BetterScrollViewer Width="200" Height="200"
HorizontalScrollBarVisibility="Auto">
<StackPanel Spacing="20" Width="210">
<TextBlock>Item 1</TextBlock>
<TextBlock>Item 2</TextBlock>
<TextBlock>Item 3</TextBlock>
<TextBlock>Item 4</TextBlock>
<TextBlock>Item 5</TextBlock>
<TextBlock>Item 6</TextBlock>
<TextBlock>Item 7</TextBlock>
<TextBlock>Item 8</TextBlock>
<TextBlock>Item 9</TextBlock>
</StackPanel>
</scroll:BetterScrollViewer>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type scroll:BetterScrollViewer}" TargetType="scroll:BetterScrollViewer">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<scroll:BetterScrollContentPresenter x:Name="PART_ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
Padding="{TemplateBinding Padding}"
ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}">
<scroll:BetterScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}" />
</scroll:BetterScrollContentPresenter.GestureRecognizers>
</scroll:BetterScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1" />
<ScrollBar Name="PART_VerticalScrollBar"
Orientation="Vertical"
Grid.Column="1" />
<Panel x:Name="PART_ScrollBarsSeparator"
Grid.Row="1"
Grid.Column="1"
Background="{DynamicResource ScrollViewerScrollBarsSeparatorBackground}"
Opacity="0">
<Panel.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.1" />
</Transitions>
</Panel.Transitions>
</Panel>
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^[IsExpanded=true] /template/ Panel#PART_ScrollBarsSeparator">
<Setter Property="Opacity" Value="1" />
</Style>
</ControlTheme>
</ResourceDictionary>

5
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs

@ -0,0 +1,5 @@
using Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Controls.Scroll;
public class BetterScrollViewer : ScrollViewer { }

6
StabilityMatrix.Avalonia/Controls/SeedCard.axaml.cs

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

5
StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs

@ -1,3 +1,6 @@
namespace StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class SelectImageCard : DropTargetTemplatedControlBase { } public class SelectImageCard : DropTargetTemplatedControlBase { }

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

@ -0,0 +1,51 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<selectableImageCard:SelectableImageButton
Source="https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" />
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Style Selector="selectableImageCard|SelectableImageButton">
<Setter Property="Template">
<ControlTemplate>
<Grid
Width="{Binding ImageWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Height="{Binding ImageHeight, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<CheckBox VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="14,8"
Padding="0"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ZIndex="100">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
<CheckBox.Styles>
<Style Selector="CheckBox">
<Setter Property="CornerRadius" Value="16" />
</Style>
</CheckBox.Styles>
</CheckBox>
<Button
Margin="0"
Padding="4"
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"
Source="{TemplateBinding Source}" />
</Button>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

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

@ -0,0 +1,56 @@
using System;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard;
public class SelectableImageButton : Button
{
public static readonly StyledProperty<bool?> IsSelectedProperty =
ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty =
AdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register<
SelectableImageButton,
double
>("ImageWidth", 300);
public static readonly StyledProperty<double> ImageHeightProperty = AvaloniaProperty.Register<
SelectableImageButton,
double
>("ImageHeight", 300);
static SelectableImageButton()
{
AffectsRender<SelectableImageButton>(ImageWidthProperty, ImageHeightProperty);
AffectsArrange<SelectableImageButton>(ImageWidthProperty, ImageHeightProperty);
}
public double ImageHeight
{
get => GetValue(ImageHeightProperty);
set => SetValue(ImageHeightProperty, value);
}
public double ImageWidth
{
get => GetValue(ImageWidthProperty);
set => SetValue(ImageWidthProperty, value);
}
public bool? IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public string? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
}

6
StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs

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

8
StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs

@ -1,14 +1,18 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[Transient]
public class StackCard : TemplatedControl public class StackCard : TemplatedControl
{ {
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<StackCard, int>( public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<
"Spacing", 8); StackCard,
int
>("Spacing", 8);
public int Spacing public int Spacing
{ {

2
StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs

@ -1,8 +1,10 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class StackExpander : TemplatedControl public class StackExpander : TemplatedControl
{ {
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register< public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<

5
StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs

@ -1,13 +1,14 @@
using System; using AsyncAwaitBestPractices;
using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class UpscalerCard : TemplatedControl public class UpscalerCard : TemplatedControl
{ {
/// <inheritdoc /> /// <inheritdoc />

123
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -1,14 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls; using NSubstitute;
using NSubstitute.ReturnsExtensions;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
@ -20,6 +22,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
@ -29,7 +32,9 @@ 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.Database;
using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -52,7 +57,7 @@ public static class DesignData
if (isInitialized) if (isInitialized)
throw new InvalidOperationException("DesignData is already initialized."); throw new InvalidOperationException("DesignData is already initialized.");
var services = new ServiceCollection(); var services = App.ConfigureServices();
var activePackageId = Guid.NewGuid(); var activePackageId = Guid.NewGuid();
services.AddSingleton<ISettingsManager, MockSettingsManager>( services.AddSingleton<ISettingsManager, MockSettingsManager>(
@ -106,18 +111,18 @@ public static class DesignData
// Mock services // Mock services
services services
.AddSingleton<INotificationService, MockNotificationService>() .AddSingleton(Substitute.For<INotificationService>())
.AddSingleton<ISharedFolders, MockSharedFolders>() .AddSingleton(Substitute.For<ISharedFolders>())
.AddSingleton<IDownloadService, MockDownloadService>() .AddSingleton(Substitute.For<IDownloadService>())
.AddSingleton<IHttpClientFactory, MockHttpClientFactory>() .AddSingleton(Substitute.For<IHttpClientFactory>())
.AddSingleton<IApiFactory, MockApiFactory>() .AddSingleton(Substitute.For<IApiFactory>())
.AddSingleton(Substitute.For<IDiscordRichPresenceService>())
.AddSingleton(Substitute.For<ITrackedDownloadService>())
.AddSingleton(Substitute.For<ILiteDbContext>())
.AddSingleton<IInferenceClientManager, MockInferenceClientManager>() .AddSingleton<IInferenceClientManager, MockInferenceClientManager>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>()
.AddSingleton<ICompletionProvider, MockCompletionProvider>() .AddSingleton<ICompletionProvider, MockCompletionProvider>()
.AddSingleton<IModelIndexService, MockModelIndexService>() .AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<IImageIndexService, MockImageIndexService>() .AddSingleton<IImageIndexService, MockImageIndexService>();
.AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>()
.AddSingleton<IPackageModificationRunner, PackageModificationRunner>();
// Placeholder services that nobody should need during design time // Placeholder services that nobody should need during design time
services services
@ -128,12 +133,6 @@ public static class DesignData
.AddSingleton<ITokenizerProvider>(_ => null!) .AddSingleton<ITokenizerProvider>(_ => null!)
.AddSingleton<IPrerequisiteHelper>(_ => null!); .AddSingleton<IPrerequisiteHelper>(_ => null!);
// Using some default service implementations from App
App.ConfigurePackages(services);
App.ConfigurePageViewModels(services);
App.ConfigureDialogViewModels(services);
App.ConfigureViews(services);
// Override Launch page with mock // Override Launch page with mock
services.Remove(ServiceDescriptor.Singleton<LaunchPageViewModel, LaunchPageViewModel>()); services.Remove(ServiceDescriptor.Singleton<LaunchPageViewModel, LaunchPageViewModel>());
services.AddSingleton<LaunchPageViewModel, MockLaunchPageViewModel>(); services.AddSingleton<LaunchPageViewModel, MockLaunchPageViewModel>();
@ -172,12 +171,61 @@ public static class DesignData
LaunchOptionsViewModel.UpdateFilterCards(); LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>(); InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = packageFactory InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
.GetAllAvailablePackages() packageFactory.GetAllAvailablePackages()
.ToImmutableArray(); );
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0]; InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note."; InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
ObservableCacheEx.AddOrUpdate(
CheckpointsPageViewModel.CheckpointFoldersCache,
new CheckpointFolder[]
{
new(settingsManager, downloadService, modelFinder, notificationService)
{
DirectoryPath = "Models/StableDiffusion",
DisplayedCheckpointFiles = new ObservableCollectionExtended<CheckpointFile>()
{
new()
{
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background",
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
BaseModel = "SD 1.5",
ModelName = "Auroral Background",
ModelType = CivitModelType.Model,
FileMetadata = new CivitFileMetadata
{
Format = CivitModelFormat.SafeTensor,
Fp = CivitModelFpType.fp16,
Size = CivitModelSize.pruned,
}
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
},
},
new(settingsManager, downloadService, modelFinder, notificationService)
{
Title = "Lora",
DirectoryPath = "Packages/Lora",
DisplayedCheckpointFiles = new ObservableCollectionExtended<CheckpointFile>
{
new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", }
}
}
}
);
/*// Checkpoints page /*// Checkpoints page
CheckpointsPageViewModel.CheckpointFolders = CheckpointsPageViewModel.CheckpointFolders =
new CheckpointFolder[] new CheckpointFolder[]
@ -336,6 +384,26 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel => public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>(); Services.GetRequiredService<LaunchPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel
{
get
{
var vm = Services.GetRequiredService<OutputsPageViewModel>();
vm.Outputs = new ObservableCollectionExtended<OutputImageViewModel>
{
new(
new LocalImageFile
{
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ImageType = LocalImageFileType.TextToImage
}
)
};
return vm;
}
}
public static PackageManagerViewModel PackageManagerViewModel public static PackageManagerViewModel PackageManagerViewModel
{ {
get get
@ -465,6 +533,15 @@ The gallery images are often inpainted, but you will get something very similar
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair> { new("UWU", "TRUE"), }; viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair> { new("UWU", "TRUE"), };
}); });
public static PythonPackagesViewModel PythonPackagesViewModel =>
DialogFactory.Get<PythonPackagesViewModel>(vm =>
{
vm.AddPackages(
new PipPackageInfo("pip", "1.0.0"),
new PipPackageInfo("torch", "2.1.0+cu121")
);
});
public static InferenceTextToImageViewModel InferenceTextToImageViewModel => public static InferenceTextToImageViewModel InferenceTextToImageViewModel =>
DialogFactory.Get<InferenceTextToImageViewModel>(vm => DialogFactory.Get<InferenceTextToImageViewModel>(vm =>
{ {
@ -603,8 +680,8 @@ The gallery images are often inpainted, but you will get something very similar
get get
{ {
var list = new CompletionList { IsFiltering = true }; var list = new CompletionList { IsFiltering = true };
list.CompletionData.AddRange(SampleCompletionData); ExtensionMethods.AddRange(list.CompletionData, SampleCompletionData);
list.FilteredCompletionData.AddRange(list.CompletionData); ExtensionMethods.AddRange(list.FilteredCompletionData, list.CompletionData);
list.SelectItem("te", true); list.SelectItem("te", true);
return list; return list;
} }

12
StabilityMatrix.Avalonia/DesignData/MockApiFactory.cs

@ -1,12 +0,0 @@
using System;
using StabilityMatrix.Core.Api;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockApiFactory : IApiFactory
{
public T CreateRefitClient<T>(Uri baseAddress)
{
throw new NotImplementedException();
}
}

18
StabilityMatrix.Avalonia/DesignData/MockDiscordRichPresenceService.cs

@ -1,18 +0,0 @@
using System;
using StabilityMatrix.Avalonia.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockDiscordRichPresenceService : IDiscordRichPresenceService
{
/// <inheritdoc />
public void Dispose()
{
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void UpdateState()
{
}
}

50
StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs

@ -1,50 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockDownloadService : IDownloadService
{
public Task DownloadToFileAsync(
string downloadUrl,
string downloadPath,
IProgress<ProgressReport>? progress = null,
string? httpClientName = null,
CancellationToken cancellationToken = default
)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task ResumeDownloadToFileAsync(
string downloadUrl,
string downloadPath,
long existingFileSize,
IProgress<ProgressReport>? progress = null,
string? httpClientName = null,
CancellationToken cancellationToken = default
)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<long> GetFileSizeAsync(
string downloadUrl,
string? httpClientName = null,
CancellationToken cancellationToken = default
)
{
return Task.FromResult(0L);
}
public Task<Stream?> GetImageStreamFromUrl(string url)
{
return Task.FromResult(new MemoryStream(new byte[24]) as Stream)!;
}
}

12
StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs

@ -1,12 +0,0 @@
using System;
using System.Net.Http;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
throw new NotImplementedException();
}
}

69
StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using DynamicData;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
@ -10,58 +10,55 @@ namespace StabilityMatrix.Avalonia.DesignData;
public class MockImageIndexService : IImageIndexService public class MockImageIndexService : IImageIndexService
{ {
/// <inheritdoc /> /// <inheritdoc />
public IndexCollection<LocalImageFile, string> InferenceImages { get; } = public IndexCollection<LocalImageFile, string> InferenceImages { get; }
new IndexCollection<LocalImageFile, string>(null!, file => file.RelativePath)
{
RelativePath = "inference"
};
/// <inheritdoc /> public MockImageIndexService()
public Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix)
{
return Task.FromResult(
(IReadOnlyList<LocalImageFile>)
new LocalImageFile[]
{
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152",
},
new()
{ {
RelativePath = InferenceImages = new IndexCollection<LocalImageFile, string>(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024", this,
}, file => file.AbsolutePath
new() )
{ {
RelativePath = RelativePath = "Inference"
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152", };
}
}
);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task RefreshIndexForAllCollections() public Task RefreshIndexForAllCollections()
{ {
return Task.CompletedTask; return RefreshIndex(InferenceImages);
} }
/// <inheritdoc /> private static LocalImageFile GetSampleImage(string url)
public Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
{ {
return Task.CompletedTask; return new LocalImageFile
{
AbsolutePath = url,
GenerationParameters = GenerationParameters.GetSample(),
ImageSize = new System.Drawing.Size(1024, 1024)
};
} }
/// <inheritdoc /> /// <inheritdoc />
public void BackgroundRefreshIndex() public Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
{ {
throw new System.NotImplementedException(); var toAdd = new[]
{
GetSampleImage(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024"
),
GetSampleImage(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152"
)
};
indexCollection.ItemsSource.EditDiff(toAdd);
return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public Task RemoveImage(LocalImageFile imageFile) public void BackgroundRefreshIndex()
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

12
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
@ -48,6 +49,17 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
/// <inheritdoc /> /// <inheritdoc />
public bool CanUserDisconnect => IsConnected && !IsConnecting; public bool CanUserDisconnect => IsConnected && !IsConnecting;
public MockInferenceClientManager()
{
Models.AddRange(
new[]
{
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("artshaper1.safetensors"),
}
);
}
/// <inheritdoc /> /// <inheritdoc />
public Task CopyImageToInputAsync( public Task CopyImageToInputAsync(
FilePath imageFile, FilePath imageFile,

62
StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LiteDB.Async;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockLiteDbContext : ILiteDbContext
{
public LiteDatabaseAsync Database => throw new NotImplementedException();
public ILiteCollectionAsync<CivitModel> CivitModels => throw new NotImplementedException();
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
throw new NotImplementedException();
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
throw new NotImplementedException();
public ILiteCollectionAsync<LocalModelFile> LocalModelFiles =>
throw new NotImplementedException();
public ILiteCollectionAsync<InferenceProjectEntry> InferenceProjects =>
throw new NotImplementedException();
public ILiteCollectionAsync<LocalImageFile> LocalImageFiles =>
throw new NotImplementedException();
public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
{
return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null));
}
public Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{
return Task.FromResult(true);
}
public Task<bool> UpsertCivitModelAsync(IEnumerable<CivitModel> civitModels)
{
return Task.FromResult(true);
}
public Task<bool> UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry)
{
return Task.FromResult(true);
}
public Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey)
{
return Task.FromResult<GithubCacheEntry?>(null);
}
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry)
{
return Task.FromResult(true);
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}

47
StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs

@ -1,47 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockNotificationService : INotificationService
{
public void Initialize(Visual? visual,
NotificationPosition position = NotificationPosition.BottomRight, int maxItems = 3)
{
}
public void Show(INotification notification)
{
}
public Task<TaskResult<T>> TryAsync<T>(Task<T> task, string title = "Error", string? message = null,
NotificationType appearance = NotificationType.Error)
{
return Task.FromResult(new TaskResult<T>(default!));
}
public Task<TaskResult<bool>> TryAsync(Task task, string title = "Error", string? message = null,
NotificationType appearance = NotificationType.Error)
{
return Task.FromResult(new TaskResult<bool>(true));
}
public void Show(
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null)
{
}
public void ShowPersistent(
string title,
string message,
NotificationType appearance = NotificationType.Information)
{
}
}

26
StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs

@ -1,26 +0,0 @@
using System.Threading.Tasks;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockSharedFolders : ISharedFolders
{
public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory)
{
}
public Task UpdateLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory)
{
return Task.CompletedTask;
}
public void RemoveLinksForAllPackages()
{
}
public void SetupSharedModelFolders()
{
}
}

22
StabilityMatrix.Avalonia/DesignData/MockTrackedDownloadService.cs

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockTrackedDownloadService : ITrackedDownloadService
{
/// <inheritdoc />
public IEnumerable<TrackedDownload> Downloads => Array.Empty<TrackedDownload>();
/// <inheritdoc />
public event EventHandler<TrackedDownload>? DownloadAdded;
/// <inheritdoc />
public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath)
{
throw new NotImplementedException();
}
}

30
StabilityMatrix.Avalonia/DialogHelper.cs

@ -9,16 +9,15 @@ using System.Text.Json;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using AvaloniaEdit; using AvaloniaEdit;
using AvaloniaEdit.TextMate; using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.ComponentModel.__Internals;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Markdown.Avalonia; using Markdown.Avalonia;
using Markdown.Avalonia.SyntaxHigh.Extensions;
using NLog; using NLog;
using Refit; using Refit;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
@ -26,7 +25,6 @@ using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars; using TextMateSharp.Grammars;
using Process = FuzzySharp.Process; using Process = FuzzySharp.Process;
@ -96,6 +94,18 @@ public static class DialogHelper
Watermark = field.Watermark, Watermark = field.Watermark,
DataContext = field, DataContext = field,
}; };
if (!string.IsNullOrEmpty(field.InnerLeftText))
{
textBox.InnerLeftContent = new TextBlock()
{
Text = field.InnerLeftText,
Foreground = Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, -4, 0)
};
}
stackPanel.Children.Add(textBox); stackPanel.Children.Add(textBox);
// When IsValid property changes, update invalid count and primary button // When IsValid property changes, update invalid count and primary button
@ -427,7 +437,7 @@ public static class DialogHelper
AllowScrollBelowDocument = false AllowScrollBelowDocument = false
} }
}; };
TextEditorConfigs.ConfigForPrompt(textEditor); TextEditorConfigs.Configure(textEditor, TextEditorPreset.Prompt);
textEditor.Document.Text = errorLineFormatted; textEditor.Document.Text = errorLineFormatted;
textEditor.TextArea.Caret.Offset = textEditor.Document.Lines[0].EndOffset; textEditor.TextArea.Caret.Offset = textEditor.Document.Lines[0].EndOffset;
@ -518,15 +528,6 @@ public static class DialogHelper
XamlRoot = App.VisualRoot XamlRoot = App.VisualRoot
}; };
} }
/// <summary>
/// Creates a connection help dialog.
/// </summary>
public static BetterContentDialog CreateConnectionHelpDialog()
{
// TODO
return new BetterContentDialog();
}
} }
// Text fields // Text fields
@ -541,6 +542,9 @@ public sealed class TextBoxField : INotifyPropertyChanged
// Watermark text // Watermark text
public string Watermark { get; init; } = string.Empty; public string Watermark { get; init; } = string.Empty;
// Inner left value
public string? InnerLeftText { get; init; }
/// <summary> /// <summary>
/// Validation action on text changes. Throw exception if invalid. /// Validation action on text changes. Throw exception if invalid.
/// </summary> /// </summary>

16
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -184,7 +184,7 @@ public static class ComfyNodeBuilderExtensions
var checkpointLoader = builder.Nodes.AddNamedNode( var checkpointLoader = builder.Nodes.AddNamedNode(
ComfyNodeBuilder.CheckpointLoaderSimple( ComfyNodeBuilder.CheckpointLoaderSimple(
"Refiner_CheckpointLoader", "Refiner_CheckpointLoader",
modelCardViewModel.SelectedRefiner?.FileName modelCardViewModel.SelectedRefiner?.RelativePath
?? throw new NullReferenceException("Model not selected") ?? throw new NullReferenceException("Model not selected")
) )
); );
@ -282,20 +282,16 @@ public static class ComfyNodeBuilderExtensions
builder.Connections.ImageSize = builder.Connections.LatentSize; builder.Connections.ImageSize = builder.Connections.LatentSize;
} }
var saveImage = builder.Nodes.AddNamedNode( var previewImage = builder.Nodes.AddNamedNode(
new NamedComfyNode("SaveImage") new NamedComfyNode("SaveImage")
{ {
ClassType = "SaveImage", ClassType = "PreviewImage",
Inputs = new Dictionary<string, object?> Inputs = new Dictionary<string, object?> { ["images"] = builder.Connections.Image }
{
["filename_prefix"] = "Inference/TextToImage",
["images"] = builder.Connections.Image
}
} }
); );
builder.Connections.OutputNodes.Add(saveImage); builder.Connections.OutputNodes.Add(previewImage);
return saveImage.Name; return previewImage.Name;
} }
} }

27
StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs

@ -13,8 +13,10 @@ public static class ImageProcessor
/// </summary> /// </summary>
public static (int rows, int columns) GetGridDimensionsFromImageCount(int count) public static (int rows, int columns) GetGridDimensionsFromImageCount(int count)
{ {
if (count <= 1) return (1, 1); if (count <= 1)
if (count == 2) return (1, 2); return (1, 1);
if (count == 2)
return (1, 2);
// Prefer one extra row over one extra column, // Prefer one extra row over one extra column,
// the row count will be the floor of the square root // the row count will be the floor of the square root
@ -24,10 +26,11 @@ public static class ImageProcessor
return (rows, columns); return (rows, columns);
} }
public static SKImage CreateImageGrid( public static SKImage CreateImageGrid(IReadOnlyList<SKImage> images, int spacing = 0)
IReadOnlyList<SKImage> images,
int spacing = 0)
{ {
if (images.Count == 0)
throw new ArgumentException("Must have at least one image");
var (rows, columns) = GetGridDimensionsFromImageCount(images.Count); var (rows, columns) = GetGridDimensionsFromImageCount(images.Count);
var singleWidth = images[0].Width; var singleWidth = images[0].Width;
@ -36,17 +39,20 @@ public static class ImageProcessor
// Make output image // Make output image
using var output = new SKBitmap( using var output = new SKBitmap(
singleWidth * columns + spacing * (columns - 1), singleWidth * columns + spacing * (columns - 1),
singleHeight * rows + spacing * (rows - 1)); singleHeight * rows + spacing * (rows - 1)
);
// Draw images // Draw images
using var canvas = new SKCanvas(output); using var canvas = new SKCanvas(output);
foreach (var (row, column) in foreach (
Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns))) var (row, column) in Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns))
)
{ {
// Stop if we have drawn all images // Stop if we have drawn all images
var index = row * columns + column; var index = row * columns + column;
if (index >= images.Count) break; if (index >= images.Count)
break;
// Get image // Get image
var image = images[index]; var image = images[index];
@ -56,7 +62,8 @@ public static class ImageProcessor
singleWidth * column + spacing * column, singleWidth * column + spacing * column,
singleHeight * row + spacing * row, singleHeight * row + spacing * row,
singleWidth * column + spacing * column + image.Width, singleWidth * column + spacing * column + image.Width,
singleHeight * row + spacing * row + image.Height); singleHeight * row + spacing * row + image.Height
);
canvas.DrawImage(image, destination); canvas.DrawImage(image, destination);
} }

95
StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs

@ -0,0 +1,95 @@
using System;
using FuzzySharp;
using FuzzySharp.PreProcess;
using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Avalonia.Helpers;
public class ImageSearcher
{
public int MinimumFuzzScore { get; init; } = 80;
public ImageSearchOptions SearchOptions { get; init; } = ImageSearchOptions.All;
public Func<LocalImageFile, bool> GetPredicate(string? searchQuery)
{
if (string.IsNullOrEmpty(searchQuery))
{
return _ => true;
}
return file =>
{
if (file.FileName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (
SearchOptions.HasFlag(ImageSearchOptions.FileName)
&& Fuzz.WeightedRatio(searchQuery, file.FileName, PreprocessMode.Full)
> MinimumFuzzScore
)
{
return true;
}
// Generation params
if (file.GenerationParameters is { } parameters)
{
if (
SearchOptions.HasFlag(ImageSearchOptions.PositivePrompt)
&& (
parameters.PositivePrompt?.Contains(
searchQuery,
StringComparison.OrdinalIgnoreCase
) ?? false
)
|| SearchOptions.HasFlag(ImageSearchOptions.NegativePrompt)
&& (
parameters.NegativePrompt?.Contains(
searchQuery,
StringComparison.OrdinalIgnoreCase
) ?? false
)
|| SearchOptions.HasFlag(ImageSearchOptions.Seed)
&& parameters.Seed
.ToString()
.StartsWith(searchQuery, StringComparison.OrdinalIgnoreCase)
|| SearchOptions.HasFlag(ImageSearchOptions.Sampler)
&& (
parameters.Sampler?.StartsWith(
searchQuery,
StringComparison.OrdinalIgnoreCase
) ?? false
)
|| SearchOptions.HasFlag(ImageSearchOptions.ModelName)
&& (
parameters.ModelName?.StartsWith(
searchQuery,
StringComparison.OrdinalIgnoreCase
) ?? false
)
)
{
return true;
}
}
return false;
};
}
[Flags]
public enum ImageSearchOptions
{
None = 0,
FileName = 1 << 0,
PositivePrompt = 1 << 1,
NegativePrompt = 1 << 2,
Seed = 1 << 3,
Sampler = 1 << 4,
ModelName = 1 << 5,
All = int.MaxValue
}
}

12
StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs

@ -3,7 +3,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Avalonia;
using Force.Crc32; using Force.Crc32;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -16,6 +15,17 @@ public static class PngDataHelper
private static readonly byte[] Text = { 0x74, 0x45, 0x58, 0x74 }; private static readonly byte[] Text = { 0x74, 0x45, 0x58, 0x74 };
private static readonly byte[] Iend = { 0x49, 0x45, 0x4E, 0x44 }; private static readonly byte[] Iend = { 0x49, 0x45, 0x4E, 0x44 };
public static byte[] AddMetadata(
Stream inputStream,
GenerationParameters generationParameters,
InferenceProjectDocument projectDocument
)
{
using var ms = new MemoryStream();
inputStream.CopyTo(ms);
return AddMetadata(ms.ToArray(), generationParameters, projectDocument);
}
public static byte[] AddMetadata( public static byte[] AddMetadata(
byte[] inputImage, byte[] inputImage,
GenerationParameters generationParameters, GenerationParameters generationParameters,

3
StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs

@ -1,12 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using Sylvan.Data.Csv; using Sylvan.Data.Csv;
using Sylvan; using Sylvan;
using Sylvan.Data;
namespace StabilityMatrix.Avalonia.Helpers; namespace StabilityMatrix.Avalonia.Helpers;

36
StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs

@ -1,5 +1,5 @@
using System.IO; using System;
using System.Reflection; using System.IO;
using Avalonia.Media; using Avalonia.Media;
using AvaloniaEdit; using AvaloniaEdit;
using AvaloniaEdit.TextMate; using AvaloniaEdit.TextMate;
@ -18,13 +18,22 @@ public static class TextEditorConfigs
{ {
public static void Configure(TextEditor editor, TextEditorPreset preset) public static void Configure(TextEditor editor, TextEditorPreset preset)
{ {
if (preset == TextEditorPreset.Prompt) switch (preset)
{ {
case TextEditorPreset.Prompt:
ConfigForPrompt(editor); ConfigForPrompt(editor);
break;
case TextEditorPreset.Console:
ConfigForConsole(editor);
break;
case TextEditorPreset.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
} }
} }
public static void ConfigForPrompt(TextEditor editor) private static void ConfigForPrompt(TextEditor editor)
{ {
const ThemeName themeName = ThemeName.DimmedMonokai; const ThemeName themeName = ThemeName.DimmedMonokai;
var registryOptions = new RegistryOptions(themeName); var registryOptions = new RegistryOptions(themeName);
@ -58,6 +67,25 @@ public static class TextEditorConfigs
installation.SetTheme(theme); installation.SetTheme(theme);
} }
private static void ConfigForConsole(TextEditor editor)
{
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
// Config hyperlinks
editor.TextArea.Options.EnableHyperlinks = true;
editor.TextArea.Options.RequireControlModifierForHyperlinkClick = false;
editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Coral;
var textMate = editor.InstallTextMate(registryOptions);
var scope = registryOptions.GetScopeByLanguageId("log");
if (scope is null)
throw new InvalidOperationException("Scope is null");
textMate.SetGrammar(scope);
textMate.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus));
}
private static IRawTheme GetThemeFromStream(Stream stream) private static IRawTheme GetThemeFromStream(Stream stream)
{ {
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);

13
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -126,7 +126,11 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
} }
} }
public async Task RunGit(string? workingDirectory = null, params string[] args) public async Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
)
{ {
var command = var command =
args.Length == 0 ? "git" : "git " + string.Join(" ", args.Select(ProcessRunner.Quote)); args.Length == 0 ? "git" : "git " + string.Join(" ", args.Select(ProcessRunner.Quote));
@ -229,6 +233,13 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
throw new NotImplementedException(); throw new NotImplementedException();
} }
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)
{
throw new PlatformNotSupportedException();
}
[UnsupportedOSPlatform("Linux")] [UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")] [UnsupportedOSPlatform("macOS")]
public Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null) public Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null)

127
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -23,6 +23,8 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe"; private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe";
private const string TkinterDownloadUrl =
"https://cdn.lykos.ai/tkinter-cpython-embedded-3.10.11-win-x64.zip";
private string HomeDir => settingsManager.LibraryDir; private string HomeDir => settingsManager.LibraryDir;
@ -36,12 +38,16 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll"); private string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip"); private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip");
private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
// Temporary directory to extract venv to during python install // Temporary directory to extract venv to during python install
private string VenvTempDir => Path.Combine(PythonDir, "venv"); private string VenvTempDir => Path.Combine(PythonDir, "venv");
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit"); private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit");
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe"); private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe");
private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe"); private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe");
private string TkinterZipPath => Path.Combine(AssetsDir, "tkinter.zip");
private string TkinterExtractPath => PythonDir;
private string TkinterExistsPath => Path.Combine(PythonDir, "tkinter");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin"); public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public bool IsPythonInstalled => File.Exists(PythonDllPath); public bool IsPythonInstalled => File.Exists(PythonDllPath);
@ -49,21 +55,30 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
public WindowsPrerequisiteHelper( public WindowsPrerequisiteHelper(
IGitHubClient gitHubClient, IGitHubClient gitHubClient,
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager) ISettingsManager settingsManager
)
{ {
this.gitHubClient = gitHubClient; this.gitHubClient = gitHubClient;
this.downloadService = downloadService; this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
} }
public async Task RunGit(string? workingDirectory = null, params string[] args) public async Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
)
{ {
var process = ProcessRunner.StartAnsiProcess(GitExePath, args, var process = ProcessRunner.StartAnsiProcess(
GitExePath,
args,
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string> environmentVariables: new Dictionary<string, string>
{ {
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
}); },
outputDataReceived: onProcessOutput
);
await ProcessRunner.WaitForExitConditionAsync(process); await ProcessRunner.WaitForExitConditionAsync(process);
} }
@ -71,12 +86,14 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args) public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{ {
var process = await ProcessRunner.GetProcessOutputAsync( var process = await ProcessRunner.GetProcessOutputAsync(
GitExePath, string.Join(" ", args), GitExePath,
string.Join(" ", args),
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string> environmentVariables: new Dictionary<string, string>
{ {
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
}); }
);
return process; return process;
} }
@ -98,7 +115,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
(Assets.SevenZipLicense, AssetsDir), (Assets.SevenZipLicense, AssetsDir),
}; };
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); progress?.Report(
new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)
);
Directory.CreateDirectory(AssetsDir); Directory.CreateDirectory(AssetsDir);
foreach (var (asset, extractDir) in assets) foreach (var (asset, extractDir) in assets)
@ -106,7 +125,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
await asset.ExtractToDir(extractDir); await asset.ExtractToDir(extractDir);
} }
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); progress?.Report(
new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)
);
} }
public async Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null) public async Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null)
@ -143,8 +164,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{ {
var fileExists = File.Exists(PythonDownloadPath); var fileExists = File.Exists(PythonDownloadPath);
var fileSize = new FileInfo(PythonDownloadPath).Length; var fileSize = new FileInfo(PythonDownloadPath).Length;
var msg = $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " + var msg =
$"(file exists: {fileExists}, size: {fileSize})"; $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} "
+ $"(file exists: {fileExists}, size: {fileSize})";
throw new Exception(msg); throw new Exception(msg);
} }
@ -206,6 +228,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
pythonPthContent = pythonPthContent.Replace("#import site", "import site"); pythonPthContent = pythonPthContent.Replace("#import site", "import site");
await File.WriteAllTextAsync(pythonPthPath, pythonPthContent); await File.WriteAllTextAsync(pythonPthPath, pythonPthContent);
// Install TKinter
await InstallTkinterIfNecessary(progress);
progress?.Report(new ProgressReport(1f, "Python install complete")); progress?.Report(new ProgressReport(1f, "Python install complete"));
} }
finally finally
@ -218,6 +243,39 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
} }
} }
[SupportedOSPlatform("windows")]
public async Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (!Directory.Exists(TkinterExistsPath))
{
Logger.Info("Downloading Tkinter");
await downloadService.DownloadToFileAsync(
TkinterDownloadUrl,
TkinterZipPath,
progress: progress
);
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Tkinter download complete",
type: ProgressType.Download
)
);
await ArchiveHelper.Extract(TkinterZipPath, TkinterExtractPath, progress);
File.Delete(TkinterZipPath);
}
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Tkinter install complete",
type: ProgressType.Generic
)
);
}
public async Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null) public async Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null)
{ {
if (File.Exists(GitExePath)) if (File.Exists(GitExePath))
@ -233,7 +291,11 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
if (!File.Exists(PortableGitDownloadPath)) if (!File.Exists(PortableGitDownloadPath))
{ {
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress); await downloadService.DownloadToFileAsync(
portableGitUrl,
PortableGitDownloadPath,
progress: progress
);
progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete")); progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete"));
} }
@ -245,7 +307,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{ {
var registry = Registry.LocalMachine; var registry = Registry.LocalMachine;
var key = registry.OpenSubKey( var key = registry.OpenSubKey(
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false); @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64",
false
);
if (key != null) if (key != null)
{ {
var buildId = Convert.ToUInt32(key.GetValue("Bld")); var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -257,16 +321,40 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Logger.Info("Downloading VC Redist"); Logger.Info("Downloading VC Redist");
await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress); await downloadService.DownloadToFileAsync(
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete", VcRedistDownloadUrl,
type: ProgressType.Download)); VcRedistDownloadPath,
progress: progress
);
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ download complete",
type: ProgressType.Download
)
);
Logger.Info("Installing VC Redist"); Logger.Info("Installing VC Redist");
progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites...")); progress?.Report(
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart"); new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
var process = ProcessRunner.StartAnsiProcess(
VcRedistDownloadPath,
"/install /quiet /norestart"
);
await process.WaitForExitAsync(); await process.WaitForExitAsync();
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete", progress?.Report(
type: ProgressType.Generic)); new ProgressReport(
progress: 1f,
message: "Visual C++ install complete",
type: ProgressType.Generic
)
);
File.Delete(VcRedistDownloadPath); File.Delete(VcRedistDownloadPath);
} }
@ -286,5 +374,4 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
File.Delete(PortableGitDownloadPath); File.Delete(PortableGitDownloadPath);
} }
} }

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

@ -113,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Clear Selection.
/// </summary>
public static string Action_ClearSelection {
get {
return ResourceManager.GetString("Action_ClearSelection", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Close. /// Looks up a localized string similar to Close.
/// </summary> /// </summary>
@ -131,6 +140,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Consolidate.
/// </summary>
public static string Action_Consolidate {
get {
return ResourceManager.GetString("Action_Consolidate", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Continue. /// Looks up a localized string similar to Continue.
/// </summary> /// </summary>
@ -140,6 +158,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Copy.
/// </summary>
public static string Action_Copy {
get {
return ResourceManager.GetString("Action_Copy", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Delete. /// Looks up a localized string similar to Delete.
/// </summary> /// </summary>
@ -149,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Downgrade.
/// </summary>
public static string Action_Downgrade {
get {
return ResourceManager.GetString("Action_Downgrade", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Edit. /// Looks up a localized string similar to Edit.
/// </summary> /// </summary>
@ -248,6 +284,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Open in Image Viewer.
/// </summary>
public static string Action_OpenInViewer {
get {
return ResourceManager.GetString("Action_OpenInViewer", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Open on CivitAI. /// Looks up a localized string similar to Open on CivitAI.
/// </summary> /// </summary>
@ -284,6 +329,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Refresh.
/// </summary>
public static string Action_Refresh {
get {
return ResourceManager.GetString("Action_Refresh", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Relaunch. /// Looks up a localized string similar to Relaunch.
/// </summary> /// </summary>
@ -383,6 +437,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Select All.
/// </summary>
public static string Action_SelectAll {
get {
return ResourceManager.GetString("Action_SelectAll", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Select Directory. /// Looks up a localized string similar to Select Directory.
/// </summary> /// </summary>
@ -410,6 +473,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Send to Inference.
/// </summary>
public static string Action_SendToInference {
get {
return ResourceManager.GetString("Action_SendToInference", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Show in Explorer. /// Looks up a localized string similar to Show in Explorer.
/// </summary> /// </summary>
@ -446,6 +518,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Upgrade.
/// </summary>
public static string Action_Upgrade {
get {
return ResourceManager.GetString("Action_Upgrade", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Yes. /// Looks up a localized string similar to Yes.
/// </summary> /// </summary>
@ -509,6 +590,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Are you sure?.
/// </summary>
public static string Label_AreYouSure {
get {
return ResourceManager.GetString("Label_AreYouSure", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Automatically scroll to end of console output. /// Looks up a localized string similar to Automatically scroll to end of console output.
/// </summary> /// </summary>
@ -662,6 +752,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to This will move all generated images from the selected packages to the Consolidated directory of the shared outputs folder. This action cannot be undone..
/// </summary>
public static string Label_ConsolidateExplanation {
get {
return ResourceManager.GetString("Label_ConsolidateExplanation", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Current directory:. /// Looks up a localized string similar to Current directory:.
/// </summary> /// </summary>
@ -888,7 +987,16 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Import as Connected. /// Looks up a localized string similar to Image to Image.
/// </summary>
public static string Label_ImageToImage {
get {
return ResourceManager.GetString("Label_ImageToImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import with Metadata.
/// </summary> /// </summary>
public static string Label_ImportAsConnected { public static string Label_ImportAsConnected {
get { get {
@ -932,6 +1040,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Inpainting.
/// </summary>
public static string Label_Inpainting {
get {
return ResourceManager.GetString("Label_Inpainting", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Input. /// Looks up a localized string similar to Input.
/// </summary> /// </summary>
@ -1148,6 +1265,24 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to {0} images selected.
/// </summary>
public static string Label_NumImagesSelected {
get {
return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1 image selected.
/// </summary>
public static string Label_OneImageSelected {
get {
return ResourceManager.GetString("Label_OneImageSelected", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Only available on Windows. /// Looks up a localized string similar to Only available on Windows.
/// </summary> /// </summary>
@ -1157,6 +1292,33 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Output Folder.
/// </summary>
public static string Label_OutputFolder {
get {
return ResourceManager.GetString("Label_OutputFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Browser.
/// </summary>
public static string Label_OutputsPageTitle {
get {
return ResourceManager.GetString("Label_OutputsPageTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Type.
/// </summary>
public static string Label_OutputType {
get {
return ResourceManager.GetString("Label_OutputType", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Package Environment. /// Looks up a localized string similar to Package Environment.
/// </summary> /// </summary>
@ -1247,6 +1409,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Python Packages.
/// </summary>
public static string Label_PythonPackages {
get {
return ResourceManager.GetString("Label_PythonPackages", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Python Version Info. /// Looks up a localized string similar to Python Version Info.
/// </summary> /// </summary>
@ -1481,6 +1652,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Text to Image.
/// </summary>
public static string Label_TextToImage {
get {
return ResourceManager.GetString("Label_TextToImage", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Theme. /// Looks up a localized string similar to Theme.
/// </summary> /// </summary>
@ -1526,6 +1706,24 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Upscale.
/// </summary>
public static string Label_Upscale {
get {
return ResourceManager.GetString("Label_Upscale", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Sharing.
/// </summary>
public static string Label_UseSharedOutputFolder {
get {
return ResourceManager.GetString("Label_UseSharedOutputFolder", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to VAE. /// Looks up a localized string similar to VAE.
/// </summary> /// </summary>

68
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -379,7 +379,7 @@
<value>Drop file here to import</value> <value>Drop file here to import</value>
</data> </data>
<data name="Label_ImportAsConnected" xml:space="preserve"> <data name="Label_ImportAsConnected" xml:space="preserve">
<value>Import as Connected</value> <value>Import with Metadata</value>
</data> </data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve"> <data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Search for connected metadata on new local imports</value> <value>Search for connected metadata on new local imports</value>
@ -678,7 +678,73 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve"> <data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restore Default Layout</value> <value>Restore Default Layout</value>
</data> </data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Output Sharing</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve"> <data name="Label_BatchIndex" xml:space="preserve">
<value>Batch Index</value> <value>Batch Index</value>
</data> </data>
<data name="Action_Copy" xml:space="preserve">
<value>Copy</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Open in Image Viewer</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} images selected</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Output Folder</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Output Type</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Clear Selection</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Select All</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Send to Inference</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Text to Image</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Image to Image</value>
</data>
<data name="Label_Inpainting" xml:space="preserve">
<value>Inpainting</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Upscale</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Output Browser</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 image selected</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Python Packages</value>
</data>
<data name="Action_Consolidate" xml:space="preserve">
<value>Consolidate</value>
</data>
<data name="Label_AreYouSure" xml:space="preserve">
<value>Are you sure?</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>This will move all generated images from the selected packages to the Consolidated directory of the shared outputs folder. This action cannot be undone.</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>Refresh</value>
</data>
<data name="Action_Upgrade" xml:space="preserve">
<value>Upgrade</value>
</data>
<data name="Action_Downgrade" xml:space="preserve">
<value>Downgrade</value>
</data>
</root> </root>

72
StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StabilityMatrix.Avalonia.Models.Inference;
public record FileNameFormat
{
public string Template { get; }
public string Prefix { get; set; } = "";
public string Postfix { get; set; } = "";
public IReadOnlyList<FileNameFormatPart> Parts { get; }
private FileNameFormat(string template, IReadOnlyList<FileNameFormatPart> parts)
{
Template = template;
Parts = parts;
}
public FileNameFormat WithBatchPostFix(int current, int total)
{
return this with { Postfix = Postfix + $" ({current}-{total})" };
}
public FileNameFormat WithGridPrefix()
{
return this with { Prefix = Prefix + "Grid_" };
}
public string GetFileName()
{
return Prefix
+ string.Join(
"",
Parts.Select(
part => part.Match(constant => constant, substitution => substitution.Invoke())
)
)
+ Postfix;
}
public static FileNameFormat Parse(string template, FileNameFormatProvider provider)
{
var parts = provider.GetParts(template).ToImmutableArray();
return new FileNameFormat(template, parts);
}
public static bool TryParse(
string template,
FileNameFormatProvider provider,
[NotNullWhen(true)] out FileNameFormat? format
)
{
try
{
format = Parse(template, provider);
return true;
}
catch (ArgumentException)
{
format = null;
return false;
}
}
public const string DefaultTemplate = "{date}_{time}-{model_name}-{seed}";
}

7
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs

@ -0,0 +1,7 @@
using System;
using OneOf;
namespace StabilityMatrix.Avalonia.Models.Inference;
[GenerateOneOf]
public partial class FileNameFormatPart : OneOfBase<string, Func<string?>> { }

197
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs

@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Avalonia.Data;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Models.Inference;
public partial class FileNameFormatProvider
{
public GenerationParameters? GenerationParameters { get; init; }
public InferenceProjectType? ProjectType { get; init; }
public string? ProjectName { get; init; }
private Dictionary<string, Func<string?>>? _substitutions;
public Dictionary<string, Func<string?>> Substitutions =>
_substitutions ??= new Dictionary<string, Func<string?>>
{
{ "seed", () => GenerationParameters?.Seed.ToString() },
{ "prompt", () => GenerationParameters?.PositivePrompt },
{ "negative_prompt", () => GenerationParameters?.NegativePrompt },
{
"model_name",
() => Path.GetFileNameWithoutExtension(GenerationParameters?.ModelName)
},
{ "model_hash", () => GenerationParameters?.ModelHash },
{ "width", () => GenerationParameters?.Width.ToString() },
{ "height", () => GenerationParameters?.Height.ToString() },
{ "project_type", () => ProjectType?.GetStringValue() },
{ "project_name", () => ProjectName },
{ "date", () => DateTime.Now.ToString("yyyy-MM-dd") },
{ "time", () => DateTime.Now.ToString("HH-mm-ss") }
};
/// <summary>
/// Validate a format string
/// </summary>
/// <param name="format">Format string</param>
/// <exception cref="DataValidationException">Thrown if the format string contains an unknown variable</exception>
[Pure]
public ValidationResult Validate(string format)
{
var regex = BracketRegex();
var matches = regex.Matches(format);
var variables = matches.Select(m => m.Groups[1].Value);
foreach (var variableText in variables)
{
try
{
var (variable, _) = ExtractVariableAndSlice(variableText);
if (!Substitutions.ContainsKey(variable))
{
return new ValidationResult($"Unknown variable '{variable}'");
}
}
catch (Exception e)
{
return new ValidationResult($"Invalid variable '{variableText}': {e.Message}");
}
}
return ValidationResult.Success!;
}
public IEnumerable<FileNameFormatPart> GetParts(string template)
{
var regex = BracketRegex();
var matches = regex.Matches(template);
var parts = new List<FileNameFormatPart>();
// Loop through all parts of the string, including matches and non-matches
var currentIndex = 0;
foreach (var result in matches.Cast<Match>())
{
// If the match is not at the start of the string, add a constant part
if (result.Index != currentIndex)
{
var constant = template[currentIndex..result.Index];
parts.Add(constant);
currentIndex += constant.Length;
}
// Now we're at start of the current match, add the variable part
var (variable, slice) = ExtractVariableAndSlice(result.Groups[1].Value);
var substitution = Substitutions[variable];
// Slice string if necessary
if (slice is not null)
{
parts.Add(
(FileNameFormatPart)(
() =>
{
var value = substitution();
if (value is null)
return null;
if (slice.End is null)
{
value = value[(slice.Start ?? 0)..];
}
else
{
var length =
Math.Min(value.Length, slice.End.Value) - (slice.Start ?? 0);
value = value.Substring(slice.Start ?? 0, length);
}
return value;
}
)
);
}
else
{
parts.Add(substitution);
}
currentIndex += result.Length;
}
// Add remaining as constant
if (currentIndex != template.Length)
{
var constant = template[currentIndex..];
parts.Add(constant);
}
return parts;
}
/// <summary>
/// Return a sample provider for UI preview
/// </summary>
public static FileNameFormatProvider GetSample()
{
return new FileNameFormatProvider
{
GenerationParameters = GenerationParameters.GetSample(),
ProjectType = InferenceProjectType.TextToImage,
ProjectName = "Sample Project"
};
}
/// <summary>
/// Extract variable and index from a combined string
/// </summary>
private static (string Variable, Slice? Slice) ExtractVariableAndSlice(string combined)
{
if (IndexRegex().Matches(combined).FirstOrDefault() is not { Success: true } match)
{
return (combined, null);
}
// Variable is everything before the match
var variable = combined[..match.Groups[0].Index];
var start = match.Groups["start"].Value;
var end = match.Groups["end"].Value;
var step = match.Groups["step"].Value;
var slice = new Slice(
string.IsNullOrEmpty(start) ? null : int.Parse(start),
string.IsNullOrEmpty(end) ? null : int.Parse(end),
string.IsNullOrEmpty(step) ? null : int.Parse(step)
);
return (variable, slice);
}
/// <summary>
/// Regex for matching contents within a curly brace.
/// </summary>
[GeneratedRegex(@"\{([a-z_:\d\[\]]+)\}")]
private static partial Regex BracketRegex();
/// <summary>
/// Regex for matching a Python-like array index.
/// </summary>
[GeneratedRegex(@"\[(?:(?<start>-?\d+)?)\:(?:(?<end>-?\d+)?)?(?:\:(?<step>-?\d+))?\]")]
private static partial Regex IndexRegex();
private record Slice(int? Start, int? End, int? Step);
}

8
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Avalonia.Models.Inference;
public record FileNameFormatVar
{
public required string Variable { get; init; }
public string? Example { get; init; }
}

1
StabilityMatrix.Avalonia/Models/Inference/Prompt.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;

1
StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs

@ -1,5 +1,4 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StabilityMatrix.Avalonia.ViewModels.Inference;
namespace StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Avalonia.Models.Inference;

3
StabilityMatrix.Avalonia/Models/Inference/ViewState.cs

@ -1,5 +1,4 @@
using System.Text.Json.Nodes; using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Avalonia.Models.Inference;

7
StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Avalonia.Models;
public class PackageOutputCategory
{
public required string Name { get; set; }
public required string Path { get; set; }
}

5
StabilityMatrix.Avalonia/Models/SharedState.cs

@ -1,14 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.Models;
/// <summary> /// <summary>
/// Singleton DI service for observable shared UI state. /// Singleton DI service for observable shared UI state.
/// </summary> /// </summary>
[Singleton]
public partial class SharedState : ObservableObject public partial class SharedState : ObservableObject
{ {
/// <summary> /// <summary>
/// Whether debug mode enabled from settings page version tap. /// Whether debug mode enabled from settings page version tap.
/// </summary> /// </summary>
[ObservableProperty] private bool isDebugMode; [ObservableProperty]
private bool isDebugMode;
} }

3
StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -16,6 +15,7 @@ using NLog;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -26,6 +26,7 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Models.TagCompletion; namespace StabilityMatrix.Avalonia.Models.TagCompletion;
[Singleton(typeof(ICompletionProvider))]
public partial class CompletionProvider : ICompletionProvider public partial class CompletionProvider : ICompletionProvider
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

4
StabilityMatrix.Avalonia/Models/TagCompletion/TextCompletionRequest.cs

@ -1,6 +1,4 @@
using System.Collections.Generic; using StabilityMatrix.Core.Models.Tokens;
using AvaloniaEdit.Document;
using StabilityMatrix.Core.Models.Tokens;
namespace StabilityMatrix.Avalonia.Models.TagCompletion; namespace StabilityMatrix.Avalonia.Models.TagCompletion;

2
StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs

@ -1,10 +1,12 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Core.Attributes;
using TextMateSharp.Grammars; using TextMateSharp.Grammars;
using TextMateSharp.Registry; using TextMateSharp.Registry;
namespace StabilityMatrix.Avalonia.Models.TagCompletion; namespace StabilityMatrix.Avalonia.Models.TagCompletion;
[Singleton(typeof(ITokenizerProvider))]
public class TokenizerProvider : ITokenizerProvider public class TokenizerProvider : ITokenizerProvider
{ {
private readonly Registry registry = new(new RegistryOptions(ThemeName.DarkPlus)); private readonly Registry registry = new(new RegistryOptions(ThemeName.DarkPlus));

3
StabilityMatrix.Avalonia/Models/TextEditorPreset.cs

@ -3,5 +3,6 @@
public enum TextEditorPreset public enum TextEditorPreset
{ {
None, None,
Prompt Prompt,
Console
} }

13
StabilityMatrix.Avalonia/Program.cs

@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader; using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -232,10 +233,16 @@ public class Program
if (e.ExceptionObject is not Exception ex) if (e.ExceptionObject is not Exception ex)
return; return;
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); // Exception automatically logged by Sentry if enabled
if (SentrySdk.IsEnabled) if (SentrySdk.IsEnabled)
{ {
ex.SetSentryMechanism("AppDomain.UnhandledException", handled: false);
SentrySdk.CaptureException(ex); SentrySdk.CaptureException(ex);
SentrySdk.FlushAsync().SafeFireAndForget();
}
else
{
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
} }
if ( if (
@ -290,6 +297,10 @@ public class Program
[DoesNotReturn] [DoesNotReturn]
private static void ExitWithException(Exception exception) private static void ExitWithException(Exception exception)
{ {
if (SentrySdk.IsEnabled)
{
SentrySdk.Flush();
}
App.Shutdown(1); App.Shutdown(1);
Dispatcher.UIThread.InvokeShutdown(); Dispatcher.UIThread.InvokeShutdown();
Environment.Exit(Marshal.GetHRForException(exception)); Environment.Exit(Marshal.GetHRForException(exception));

1
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;

26
StabilityMatrix.Avalonia/Services/INotificationService.cs

@ -2,6 +2,8 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;
@ -11,7 +13,8 @@ public interface INotificationService
public void Initialize( public void Initialize(
Visual? visual, Visual? visual,
NotificationPosition position = NotificationPosition.BottomRight, NotificationPosition position = NotificationPosition.BottomRight,
int maxItems = 3); int maxItems = 3
);
public void Show(INotification notification); public void Show(INotification notification);
@ -26,7 +29,8 @@ public interface INotificationService
Task<T> task, Task<T> task,
string title = "Error", string title = "Error",
string? message = null, string? message = null,
NotificationType appearance = NotificationType.Error); NotificationType appearance = NotificationType.Error
);
/// <summary> /// <summary>
/// Attempt to run the given void task, showing a generic error notification if it fails. /// Attempt to run the given void task, showing a generic error notification if it fails.
@ -40,7 +44,8 @@ public interface INotificationService
Task task, Task task,
string title = "Error", string title = "Error",
string? message = null, string? message = null,
NotificationType appearance = NotificationType.Error); NotificationType appearance = NotificationType.Error
);
/// <summary> /// <summary>
/// Show a notification with the given parameters. /// Show a notification with the given parameters.
@ -49,7 +54,8 @@ public interface INotificationService
string title, string title,
string message, string message,
NotificationType appearance = NotificationType.Information, NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null); TimeSpan? expiration = null
);
/// <summary> /// <summary>
/// Show a notification that will not auto-dismiss. /// Show a notification that will not auto-dismiss.
@ -60,5 +66,15 @@ public interface INotificationService
void ShowPersistent( void ShowPersistent(
string title, string title,
string message, string message,
NotificationType appearance = NotificationType.Information); NotificationType appearance = NotificationType.Information
);
/// <summary>
/// Show a notification for a <see cref="AppException"/> that will not auto-dismiss.
/// </summary>
void ShowPersistent(
AppException exception,
NotificationType appearance = NotificationType.Error,
LogLevel logLevel = LogLevel.Warning
);
} }

46
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -13,6 +13,7 @@ using SkiaSharp;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Inference;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -27,6 +28,7 @@ namespace StabilityMatrix.Avalonia.Services;
/// Manager for the current inference client /// Manager for the current inference client
/// Has observable shared properties for shared info like model names /// Has observable shared properties for shared info like model names
/// </summary> /// </summary>
[Singleton(typeof(IInferenceClientManager))]
public partial class InferenceClientManager : ObservableObject, IInferenceClientManager public partial class InferenceClientManager : ObservableObject, IInferenceClientManager
{ {
private readonly ILogger<InferenceClientManager> logger; private readonly ILogger<InferenceClientManager> logger;
@ -345,6 +347,44 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
return ConnectAsyncImpl(new Uri("http://127.0.0.1:8188"), cancellationToken); return ConnectAsyncImpl(new Uri("http://127.0.0.1:8188"), cancellationToken);
} }
private async Task MigrateLinksIfNeeded(PackagePair packagePair)
{
if (packagePair.InstalledPackage.FullPath is not { } packagePath)
{
throw new ArgumentException("Package path is null", nameof(packagePair));
}
var inferenceDir = settingsManager.ImagesInferenceDirectory;
inferenceDir.Create();
// For locally installed packages only
// Delete ./output/Inference
var legacyInferenceLinkDir = new DirectoryPath(
packagePair.InstalledPackage.FullPath
).JoinDir("output", "Inference");
if (legacyInferenceLinkDir.Exists)
{
logger.LogInformation(
"Deleting legacy inference link at {LegacyDir}",
legacyInferenceLinkDir
);
if (legacyInferenceLinkDir.IsSymbolicLink)
{
await legacyInferenceLinkDir.DeleteAsync(false);
}
else
{
logger.LogWarning(
"Legacy inference link at {LegacyDir} is not a symbolic link, skipping",
legacyInferenceLinkDir
);
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public async Task ConnectAsync( public async Task ConnectAsync(
PackagePair packagePair, PackagePair packagePair,
@ -367,11 +407,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
logger.LogError(ex, "Error setting up completion provider"); logger.LogError(ex, "Error setting up completion provider");
}); });
// Setup image folder links await MigrateLinksIfNeeded(packagePair);
await comfyPackage.SetupInferenceOutputFolderLinks(
packagePair.InstalledPackage.FullPath
?? throw new InvalidOperationException("Package does not have a Path")
);
// Get user defined host and port // Get user defined host and port
var host = packagePair.InstalledPackage.GetLaunchArgsHost(); var host = packagePair.InstalledPackage.GetLaunchArgsHost();

4
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation; using FluentAvalonia.UI.Media.Animation;
@ -8,10 +6,12 @@ using FluentAvalonia.UI.Navigation;
using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(INavigationService))]
public class NavigationService : INavigationService public class NavigationService : INavigationService
{ {
private Frame? _frame; private Frame? _frame;

41
StabilityMatrix.Avalonia/Services/NotificationService.cs

@ -3,20 +3,32 @@ using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(INotificationService))]
public class NotificationService : INotificationService public class NotificationService : INotificationService
{ {
private readonly ILogger<NotificationService> logger;
private WindowNotificationManager? notificationManager; private WindowNotificationManager? notificationManager;
public NotificationService(ILogger<NotificationService> logger)
{
this.logger = logger;
}
public void Initialize( public void Initialize(
Visual? visual, Visual? visual,
NotificationPosition position = NotificationPosition.BottomRight, NotificationPosition position = NotificationPosition.BottomRight,
int maxItems = 4) int maxItems = 4
)
{ {
if (notificationManager is not null) return; if (notificationManager is not null)
return;
notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual)) notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual))
{ {
Position = position, Position = position,
@ -33,7 +45,8 @@ public class NotificationService : INotificationService
string title, string title,
string message, string message,
NotificationType appearance = NotificationType.Information, NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null) TimeSpan? expiration = null
)
{ {
Show(new Notification(title, message, appearance, expiration)); Show(new Notification(title, message, appearance, expiration));
} }
@ -41,17 +54,32 @@ public class NotificationService : INotificationService
public void ShowPersistent( public void ShowPersistent(
string title, string title,
string message, string message,
NotificationType appearance = NotificationType.Information) NotificationType appearance = NotificationType.Information
)
{ {
Show(new Notification(title, message, appearance, TimeSpan.Zero)); Show(new Notification(title, message, appearance, TimeSpan.Zero));
} }
/// <inheritdoc />
public void ShowPersistent(
AppException exception,
NotificationType appearance = NotificationType.Warning,
LogLevel logLevel = LogLevel.Warning
)
{
// Log exception
logger.Log(logLevel, exception, "{Message}", exception.Message);
Show(new Notification(exception.Message, exception.Details, appearance, TimeSpan.Zero));
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<TaskResult<T>> TryAsync<T>( public async Task<TaskResult<T>> TryAsync<T>(
Task<T> task, Task<T> task,
string title = "Error", string title = "Error",
string? message = null, string? message = null,
NotificationType appearance = NotificationType.Error) NotificationType appearance = NotificationType.Error
)
{ {
try try
{ {
@ -69,7 +97,8 @@ public class NotificationService : INotificationService
Task task, Task task,
string title = "Error", string title = "Error",
string? message = null, string? message = null,
NotificationType appearance = NotificationType.Error) NotificationType appearance = NotificationType.Error
)
{ {
try try
{ {

15
StabilityMatrix.Avalonia/Services/ServiceManager.cs

@ -256,4 +256,19 @@ public class ServiceManager<T>
return new BetterContentDialog { Content = view }; return new BetterContentDialog { Content = view };
} }
public void Register(Type type, Func<T> providerFunc)
{
lock (providers)
{
if (instances.ContainsKey(type) || providers.ContainsKey(type))
{
throw new ArgumentException(
$"Service of type {type} is already registered for {typeof(T)}"
);
}
providers[type] = providerFunc;
}
}
} }

38
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -8,7 +8,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon> <ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.5.5-dev.1</Version> <Version>2.6.0-dev.1</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -23,46 +23,48 @@
<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.1" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.1" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" /> <PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" />
<PackageReference Include="Avalonia" Version="11.0.4" /> <PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.4" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
<!--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.4" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" /> <PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.2" /> <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.2" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" /> <PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.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.2" /> <PackageReference Include="Dock.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.2" /> <PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.2" /> <PackageReference Include="Dock.Serializer" Version="11.0.0.2" />
<PackageReference Include="DynamicData" Version="7.14.2" /> <PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" /> <PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.217" /> <PackageReference Include="FluentIcons.Avalonia" Version="1.1.220" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.217" /> <PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.220" />
<PackageReference Include="FuzzySharp" Version="2.0.2" /> <PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.0" /> <PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
<PackageReference Include="MetadataExtractor" Version="2.8.1" /> <PackageReference Include="MetadataExtractor" Version="2.8.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.8" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.12" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="5.2.3" /> <PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.2" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
<PackageReference Include="Polly" Version="7.2.4" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Polly" Version="8.0.0" />
<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="6.6.0" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="8.3.0" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="2.0.0" /> <PackageReference Include="RockLib.Reflection.Optimized" Version="2.0.0" />
<PackageReference Include="Sentry" Version="3.33.1" /> <PackageReference Include="Sentry" Version="3.40.1" />
<PackageReference Include="Sentry.NLog" Version="3.33.1" /> <PackageReference Include="Sentry.NLog" Version="3.40.1" />
<PackageReference Include="SpacedGrid-Avalonia" Version="11.0.0" />
<PackageReference Include="Sylvan.Common" Version="0.4.2" /> <PackageReference Include="Sylvan.Common" Version="0.4.2" />
<PackageReference Include="Sylvan.Data" Version="0.2.12" /> <PackageReference Include="Sylvan.Data" Version="0.2.12" />
<PackageReference Include="Sylvan.Data.Csv" Version="1.3.3" /> <PackageReference Include="Sylvan.Data.Csv" Version="1.3.3" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.55" /> <PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
</ItemGroup> </ItemGroup>

3
StabilityMatrix.Avalonia/Styles/ThemeColors.cs

@ -1,5 +1,4 @@
using Avalonia; using Avalonia.Media;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Styles; namespace StabilityMatrix.Avalonia.Styles;

6
StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs

@ -1,10 +1,14 @@
using System; using System;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
namespace StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogProgressViewModelBase : ConsoleProgressViewModel public partial class ContentDialogProgressViewModelBase : ConsoleProgressViewModel
{ {
[ObservableProperty]
private bool hideCloseButton;
public event EventHandler<ContentDialogResult>? PrimaryButtonClick; public event EventHandler<ContentDialogResult>? PrimaryButtonClick;
public event EventHandler<ContentDialogResult>? SecondaryButtonClick; public event EventHandler<ContentDialogResult>? SecondaryButtonClick;
public event EventHandler<ContentDialogResult>? CloseButtonClick; public event EventHandler<ContentDialogResult>? CloseButtonClick;

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

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
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.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using NLog; using NLog;
@ -27,6 +29,8 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -41,6 +45,7 @@ public abstract partial class InferenceGenerationViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory; private readonly ServiceManager<ViewModelBase> vmFactory;
@ -60,11 +65,13 @@ public abstract partial class InferenceGenerationViewModelBase
protected InferenceGenerationViewModelBase( protected InferenceGenerationViewModelBase(
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
INotificationService notificationService INotificationService notificationService,
ISettingsManager settingsManager
) )
: base(notificationService) : base(notificationService)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.vmFactory = vmFactory; this.vmFactory = vmFactory;
ClientManager = inferenceClientManager; ClientManager = inferenceClientManager;
@ -75,6 +82,101 @@ public abstract partial class InferenceGenerationViewModelBase
GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService);
} }
/// <summary>
/// Write an image to the default output folder
/// </summary>
protected Task<FilePath> WriteOutputImageAsync(
Stream imageStream,
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
)
{
var defaultOutputDir = settingsManager.ImagesInferenceDirectory;
defaultOutputDir.Create();
return WriteOutputImageAsync(
imageStream,
defaultOutputDir,
args,
batchNum,
batchTotal,
isGrid
);
}
/// <summary>
/// Write an image to an output folder
/// </summary>
protected async Task<FilePath> WriteOutputImageAsync(
Stream imageStream,
DirectoryPath outputDir,
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
)
{
var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat;
var formatProvider = new FileNameFormatProvider
{
GenerationParameters = args.Parameters,
ProjectType = args.Project?.ProjectType,
ProjectName = ProjectFile?.NameWithoutExtension
};
// Parse to format
if (
string.IsNullOrEmpty(formatTemplateStr)
|| !FileNameFormat.TryParse(formatTemplateStr, formatProvider, out var format)
)
{
// Fallback to default
Logger.Warn(
"Failed to parse format template: {FormatTemplate}, using default",
formatTemplateStr
);
format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider);
}
if (isGrid)
{
format = format.WithGridPrefix();
}
if (batchNum >= 1 && batchTotal > 1)
{
format = format.WithBatchPostFix(batchNum, batchTotal);
}
var fileName = format.GetFileName();
var file = outputDir.JoinFile($"{fileName}.png");
// Until the file is free, keep adding _{i} to the end
for (var i = 0; i < 100; i++)
{
if (!file.Exists)
break;
file = outputDir.JoinFile($"{fileName}_{i + 1}.png");
}
// If that fails, append an 7-char uuid
if (file.Exists)
{
var uuid = Guid.NewGuid().ToString("N")[..7];
file = outputDir.JoinFile($"{fileName}_{uuid}.png");
}
await using var fileStream = file.Info.OpenWrite();
await imageStream.CopyToAsync(fileStream);
return file;
}
/// <summary> /// <summary>
/// Builds the image generation prompt /// Builds the image generation prompt
/// </summary> /// </summary>
@ -156,7 +258,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Wait for prompt to finish // Wait for prompt to finish
await promptTask.Task.WaitAsync(cancellationToken); await promptTask.Task.WaitAsync(cancellationToken);
Logger.Trace($"Prompt task {promptTask.Id} finished"); Logger.Debug($"Prompt task {promptTask.Id} finished");
// Get output images // Get output images
var imageOutputs = await client.GetImagesForExecutedPromptAsync( var imageOutputs = await client.GetImagesForExecutedPromptAsync(
@ -164,6 +266,20 @@ public abstract partial class InferenceGenerationViewModelBase
cancellationToken cancellationToken
); );
if (
!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images)
|| images is not { Count: > 0 }
)
{
// No images match
notificationService.Show(
"No output",
"Did not receive any output images",
NotificationType.Warning
);
return;
}
// Disable cancellation // Disable cancellation
await promptInterrupt.DisposeAsync(); await promptInterrupt.DisposeAsync();
@ -172,15 +288,6 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGalleryCardViewModel.ImageSources.Clear(); ImageGalleryCardViewModel.ImageSources.Clear();
} }
if (
!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images) || images is null
)
{
// No images match
notificationService.Show("No output", "Did not receive any output images");
return;
}
await ProcessOutputImages(images, args); await ProcessOutputImages(images, args);
} }
finally finally
@ -207,19 +314,22 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGenerationEventArgs args ImageGenerationEventArgs args
) )
{ {
var client = args.Client;
// Write metadata to images // Write metadata to images
var outputImagesBytes = new List<byte[]>();
var outputImages = new List<ImageSource>(); var outputImages = new List<ImageSource>();
foreach (
var (i, filePath) in images foreach (var (i, comfyImage) in images.Enumerate())
.Select(image => image.ToFilePath(args.Client.OutputImagesDir!))
.Enumerate()
)
{
if (!filePath.Exists)
{ {
Logger.Warn($"Image file {filePath} does not exist"); Logger.Debug("Downloading image: {FileName}", comfyImage.FileName);
continue; var imageStream = await client.GetImageStreamAsync(comfyImage);
}
using var ms = new MemoryStream();
await imageStream.CopyToAsync(ms);
var imageArray = ms.ToArray();
outputImagesBytes.Add(imageArray);
var parameters = args.Parameters!; var parameters = args.Parameters!;
var project = args.Project!; var project = args.Project!;
@ -248,17 +358,15 @@ public abstract partial class InferenceGenerationViewModelBase
); );
} }
var bytesWithMetadata = PngDataHelper.AddMetadata( var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
await filePath.ReadAllBytesAsync(),
parameters,
project
);
await using (var outputStream = filePath.Info.OpenWrite()) // Write using generated name
{ var filePath = await WriteOutputImageAsync(
await outputStream.WriteAsync(bytesWithMetadata); new MemoryStream(bytesWithMetadata),
await outputStream.FlushAsync(); args,
} i + 1,
images.Count
);
outputImages.Add(new ImageSource(filePath)); outputImages.Add(new ImageSource(filePath));
@ -268,17 +376,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Download all images to make grid, if multiple // Download all images to make grid, if multiple
if (outputImages.Count > 1) if (outputImages.Count > 1)
{ {
var outputDir = outputImages[0].LocalFile!.Directory; var loadedImages = outputImagesBytes.Select(SKImage.FromEncodedData).ToImmutableArray();
var loadedImages = outputImages
.Select(i => i.LocalFile)
.Where(f => f is { Exists: true })
.Select(f =>
{
using var stream = f!.Info.OpenRead();
return SKImage.FromEncodedData(stream);
})
.ToImmutableArray();
var project = args.Project!; var project = args.Project!;
@ -297,13 +395,11 @@ public abstract partial class InferenceGenerationViewModelBase
); );
// Save to disk // Save to disk
var lastName = outputImages.Last().LocalFile?.Info.Name; var gridPath = await WriteOutputImageAsync(
var gridPath = outputDir!.JoinFile($"grid-{lastName}"); new MemoryStream(gridBytesWithMetadata),
args,
await using (var fileStream = gridPath.Info.OpenWrite()) isGrid: true
{ );
await fileStream.WriteAsync(gridBytesWithMetadata);
}
// Insert to start of images // Insert to start of images
var gridImage = new ImageSource(gridPath); var gridImage = new ImageSource(gridPath);

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

@ -310,7 +310,7 @@ public abstract partial class InferenceTabViewModelBase
if (this is IImageGalleryComponent imageGalleryComponent) if (this is IImageGalleryComponent imageGalleryComponent)
{ {
imageGalleryComponent.LoadImagesToGallery( imageGalleryComponent.LoadImagesToGallery(
new ImageSource(imageFile.GlobalFullPath) new ImageSource(imageFile.AbsolutePath)
); );
} }
} }

1
StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.ViewModels.Base;

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

@ -17,6 +17,7 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
@ -27,6 +28,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[ManagedService]
[Transient]
public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -254,8 +257,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
private static string PruneDescription(CivitModel model) private static string PruneDescription(CivitModel model)
{ {
var prunedDescription = model.Description var prunedDescription =
.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}") model.Description
?.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}") .Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}") .Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}") .Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}")
@ -263,7 +267,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
.Replace("</h3>", $"{Environment.NewLine}{Environment.NewLine}") .Replace("</h3>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h4>", $"{Environment.NewLine}{Environment.NewLine}") .Replace("</h4>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h5>", $"{Environment.NewLine}{Environment.NewLine}") .Replace("</h5>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h6>", $"{Environment.NewLine}{Environment.NewLine}"); .Replace("</h6>", $"{Environment.NewLine}{Environment.NewLine}") ?? string.Empty;
prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty); prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
return prunedDescription; return prunedDescription;
} }

4
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -5,16 +5,13 @@ 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.Reactive.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -42,6 +39,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointBrowserPage))] [View(typeof(CheckpointBrowserPage))]
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase public partial class CheckpointBrowserViewModel : PageViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

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

@ -9,7 +9,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -19,6 +21,8 @@ using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
[ManagedService]
[Transient]
public partial class CheckpointFile : ViewModelBase public partial class CheckpointFile : ViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -257,6 +261,11 @@ public partial class CheckpointFile : ViewModelBase
.Where(File.Exists) .Where(File.Exists)
.FirstOrDefault(); .FirstOrDefault();
if (string.IsNullOrWhiteSpace(checkpointFile.PreviewImagePath))
{
checkpointFile.PreviewImagePath = Assets.NoImage.ToString();
}
yield return checkpointFile; yield return checkpointFile;
} }
} }

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

@ -17,6 +17,7 @@ using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -27,6 +28,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
[ManagedService]
[Transient]
public partial class CheckpointFolder : ViewModelBase public partial class CheckpointFolder : ViewModelBase
{ {
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
@ -99,7 +102,7 @@ public partial class CheckpointFolder : ViewModelBase
public IObservableCollection<CheckpointFile> CheckpointFiles { get; } = public IObservableCollection<CheckpointFile> CheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>(); new ObservableCollectionExtended<CheckpointFile>();
public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; } = public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; set; } =
new ObservableCollectionExtended<CheckpointFile>(); new ObservableCollectionExtended<CheckpointFile>();
public CheckpointFolder( public CheckpointFolder(

1
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -24,6 +24,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointsPage))] [View(typeof(CheckpointsPage))]
[Singleton]
public partial class CheckpointsPageViewModel : PageViewModelBase public partial class CheckpointsPageViewModel : PageViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

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

@ -16,6 +16,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(DownloadResourceDialog))] [View(typeof(DownloadResourceDialog))]
[ManagedService]
[Transient]
public partial class DownloadResourceViewModel : ContentDialogViewModelBase public partial class DownloadResourceViewModel : ContentDialogViewModelBase
{ {
private readonly IDownloadService downloadService; private readonly IDownloadService downloadService;

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

@ -12,6 +12,8 @@ using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(EnvVarsViewModel))] [View(typeof(EnvVarsViewModel))]
[ManagedService]
[Transient]
public partial class EnvVarsViewModel : ContentDialogViewModelBase public partial class EnvVarsViewModel : ContentDialogViewModelBase
{ {
[ObservableProperty] [ObservableProperty]

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

@ -6,6 +6,8 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(ExceptionDialog))] [View(typeof(ExceptionDialog))]
[ManagedService]
[Transient]
public partial class ExceptionViewModel : ViewModelBase public partial class ExceptionViewModel : ViewModelBase
{ {
public Exception? Exception { get; set; } public Exception? Exception { get; set; }

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

@ -21,6 +21,8 @@ using Size = StabilityMatrix.Core.Helper.Size;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(ImageViewerDialog))] [View(typeof(ImageViewerDialog))]
[ManagedService]
[Transient]
public partial class ImageViewerViewModel : ContentDialogViewModelBase public partial class ImageViewerViewModel : ContentDialogViewModelBase
{ {
[ObservableProperty] [ObservableProperty]

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

Loading…
Cancel
Save