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. 8
      StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
  5. 1
      StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs
  6. 4
      StabilityMatrix.Avalonia/App.axaml
  7. 291
      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. 29
      StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs
  21. 2
      StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs
  22. 225
      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. 190
      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. 47
      StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
  58. 95
      StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs
  59. 12
      StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs
  60. 19
      StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs
  61. 38
      StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
  62. 13
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  63. 193
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  64. 200
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  65. 70
      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. 28
      StabilityMatrix.Avalonia/Services/INotificationService.cs
  82. 46
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  83. 4
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  84. 53
      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. 24
      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. 6
      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/),
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
### Fixed
- 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
- 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
## v2.5.6
### Added

5
Jenkinsfile vendored

@ -35,11 +35,6 @@ node("Diligence") {
stage('Publish Linux') {
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 {
stage('Cleanup') {

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

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

8
StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs

@ -1,12 +1,10 @@
using System;
using System.Threading;
using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
using FluentAvalonia.UI.Media.Animation;
namespace StabilityMatrix.Avalonia.Animations;
@ -23,7 +21,7 @@ public class BetterEntranceNavigationTransition : BaseTransitionInfo
/// Gets or sets the Vertical Offset used when animating
/// </summary>
public double FromVerticalOffset { get; set; } = 100;
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken)
{
var animation = new Animation
@ -36,7 +34,7 @@ public class BetterEntranceNavigationTransition : BaseTransitionInfo
Setters =
{
new Setter(Visual.OpacityProperty, 0.0),
new Setter(TranslateTransform.XProperty,FromHorizontalOffset),
new Setter(TranslateTransform.XProperty, FromHorizontalOffset),
new Setter(TranslateTransform.YProperty, FromVerticalOffset)
},
Cue = new Cue(0d)
@ -46,7 +44,7 @@ public class BetterEntranceNavigationTransition : BaseTransitionInfo
Setters =
{
new Setter(Visual.OpacityProperty, 1d),
new Setter(TranslateTransform.XProperty,0.0),
new Setter(TranslateTransform.XProperty, 0.0),
new Setter(TranslateTransform.YProperty, 0.0)
},
Cue = new Cue(1d)

1
StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs

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

4
StabilityMatrix.Avalonia/App.axaml

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

291
StabilityMatrix.Avalonia/App.axaml.cs

@ -39,42 +39,25 @@ using Polly.Extensions.Http;
using Polly.Timeout;
using Refit;
using Sentry;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
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.Settings;
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.Attributes;
using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@ -249,18 +232,6 @@ public sealed class App : Application
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>(
provider =>
new MainWindowViewModel(
@ -278,6 +249,7 @@ public sealed class App : Application
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -287,186 +259,103 @@ public sealed class App : Application
services.AddSingleton<IDisposable>(p => p.GetRequiredService<LaunchPageViewModel>());
}
internal static void ConfigureDialogViewModels(IServiceCollection services)
internal static void ConfigureDialogViewModels(
IServiceCollection services,
Type[] exportedTypes
)
{
// Dialog view models (transient)
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
services.AddSingleton<ServiceManager<ViewModelBase>>(
provider =>
new ServiceManager<ViewModelBase>()
.Register(provider.GetRequiredService<InstallerViewModel>)
.Register(provider.GetRequiredService<OneClickInstallViewModel>)
.Register(provider.GetRequiredService<SelectModelVersionViewModel>)
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<UpdateViewModel>)
.Register(provider.GetRequiredService<CheckpointBrowserCardViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<PackageCardViewModel>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>)
.Register(provider.GetRequiredService<ExceptionViewModel>)
.Register(provider.GetRequiredService<EnvVarsViewModel>)
.Register(provider.GetRequiredService<ProgressManagerViewModel>)
.Register(provider.GetRequiredService<InferenceTextToImageViewModel>)
.Register(provider.GetRequiredService<InferenceImageUpscaleViewModel>)
.Register(provider.GetRequiredService<SeedCardViewModel>)
.Register(provider.GetRequiredService<SamplerCardViewModel>)
.Register(provider.GetRequiredService<ImageGalleryCardViewModel>)
.Register(provider.GetRequiredService<ImageFolderCardViewModel>)
.Register(provider.GetRequiredService<PromptCardViewModel>)
.Register(provider.GetRequiredService<StackCardViewModel>)
.Register(provider.GetRequiredService<StackExpanderViewModel>)
.Register(provider.GetRequiredService<UpscalerCardViewModel>)
.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>)
);
}
services.AddSingleton<ServiceManager<ViewModelBase>>(provider =>
{
var serviceManager = new ServiceManager<ViewModelBase>();
var serviceManagedTypes = exportedTypes
.Select(
t =>
new
{
t,
attributes = t.GetCustomAttributes(
typeof(ManagedServiceAttribute),
true
)
}
)
.Where(t1 => t1.attributes is { Length: > 0 })
.Select(t1 => t1.t)
.ToList();
internal static void ConfigureViews(IServiceCollection services)
{
// 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>();
}
foreach (var type in serviceManagedTypes)
{
ViewModelBase? GetInstance() => provider.GetRequiredService(type) as ViewModelBase;
serviceManager.Register(type, GetInstance);
}
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>();
return serviceManager;
});
}
private static IServiceCollection ConfigureServices()
internal static IServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
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);
ConfigureDialogViewModels(services);
ConfigurePackages(services);
ConfigureDialogViewModels(services, exportedTypes);
// 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<IDisposable>(
provider => (IDisposable)provider.GetRequiredService<ITrackedDownloadService>()
@ -494,13 +383,7 @@ public sealed class App : Application
services.AddSingleton<IPrerequisiteHelper, UnixPrerequisiteHelper>();
}
ConfigureViews(services);
if (Design.IsDesignMode)
{
services.AddSingleton<ILiteDbContext, MockLiteDbContext>();
}
else
if (!Design.IsDesignMode)
{
services.AddSingleton<ILiteDbContext, LiteDbContext>();
services.AddSingleton<IDisposable>(p => p.GetRequiredService<ILiteDbContext>());

40
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

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

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

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

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

@ -1,9 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
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.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Reflection;
using AsyncAwaitBestPractices;
using Avalonia;
@ -11,8 +10,6 @@ using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using Brushes = Avalonia.Media.Brushes;
namespace StabilityMatrix.Avalonia.Controls;

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

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

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

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

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

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

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

@ -91,6 +91,28 @@ public class CompletionListBox : ListBox
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>
/// Selects the item with the specified index and scrolls it into view.
/// </summary>

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

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

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

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

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

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

29
StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs

@ -15,12 +15,14 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")]
public class FASymbolIconSource : PathIconSource
{
public static readonly StyledProperty<string> SymbolProperty =
AvaloniaProperty.Register<FASymbolIconSource, string>(nameof(Symbol));
public static readonly StyledProperty<double> FontSizeProperty = TextElement.FontSizeProperty.AddOwner<FASymbolIconSource>();
public static readonly StyledProperty<string> SymbolProperty = AvaloniaProperty.Register<
FASymbolIconSource,
string
>(nameof(Symbol));
public static readonly StyledProperty<double> FontSizeProperty =
TextElement.FontSizeProperty.AddOwner<FASymbolIconSource>();
public FASymbolIconSource()
{
Stretch = Stretch.None;
@ -52,11 +54,11 @@ public class FASymbolIconSource : PathIconSource
private void InvalidateData()
{
var path = IconProvider.Current.GetIconPath(Symbol);
var path = IconProvider.Current.GetIcon(Symbol).Path;
var geometry = Geometry.Parse(path);
var scale = FontSize / 20;
Data = geometry;
// TODO: Scaling not working
Data.Transform = new ScaleTransform(scale, scale);
@ -74,14 +76,15 @@ public class FASymbolIconSourceConverter : TypeConverter
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
{
string val => new FASymbolIconSource
{
Symbol = val,
},
string val => new FASymbolIconSource { Symbol = val, },
_ => base.ConvertFrom(context, culture, value)
};
}

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

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

225
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -1,17 +1,14 @@
<Styles
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:input="using:FluentAvalonia.UI.Input"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:animations="clr-namespace:StabilityMatrix.Avalonia.Animations"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith>
@ -22,15 +19,11 @@
</Panel>
</Design.PreviewWith>
<!--<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>-->
<Style Selector="controls|ImageFolderCard">
<ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<Setter Property="Background" Value="Transparent" />
<!-- Set Defaults -->
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Focusable" Value="True" />
<!-- Template -->
<Setter Property="Template">
<ControlTemplate>
@ -40,7 +33,7 @@
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalAlignment}">
<!--<controls:Card.Styles>
<Style Selector="ItemsRepeater">
<Setter Property="animations:ItemsRepeaterArrangeAnimation.EnableItemsArrangeAnimation" Value="True"/>
@ -50,155 +43,185 @@
<Grid RowDefinitions="Auto,*">
<TextBox
x:Name="SearchBox"
Text="{Binding SearchQuery}"
Margin="6,4,6,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent>
<ui:SymbolIcon
<ui:SymbolIcon
Margin="0,0,6,0"
FontSize="16"
Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
<ScrollViewer
Grid.Row="1"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory"
<scroll:BetterScrollViewer
Grid.Row="1"
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Disabled">
HorizontalScrollBarVisibility="Disabled"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory">
<ItemsRepeater
Name="ImageRepeater"
x:Name="ImageRepeater"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
VerticalCacheLength="8"
ItemsSource="{Binding LocalImages}">
ItemsSource="{Binding LocalImages}"
VerticalCacheLength="8">
<ItemsRepeater.Resources>
<input:StandardUICommand
x:Key="ImageClickCommand"
Command="{Binding ImageClickCommand}"/>
<input:StandardUICommand
<input:StandardUICommand x:Key="ImageClickCommand" Command="{Binding ImageClickCommand}" />
<input:StandardUICommand
x:Key="ImageDeleteCommand"
Label="{x:Static lang:Resources.Action_Delete}"
Command="{Binding ImageDeleteCommand}"
IconSource="Delete"
Command="{Binding ImageDeleteCommand}"/>
<input:StandardUICommand
Label="{x:Static lang:Resources.Action_Delete}" />
<input:StandardUICommand
x:Key="ImageCopyCommand"
Label="Copy"
Command="{Binding ImageCopyCommand}"
IconSource="Copy"
Command="{Binding ImageCopyCommand}"/>
<input:StandardUICommand
x:Key="ImageOpenCommand"
Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer}, macOS={x:Static lang:Resources.Action_OpenInFinder}}"
Label="Copy" />
<input:StandardUICommand
x:Key="ImageOpenCommand"
Command="{Binding ImageOpenCommand}"
IconSource="Folder"
Command="{Binding ImageOpenCommand}"/>
<input:StandardUICommand
x:Key="ImageExportPngWithMetadataCommand"
Label="Png with Metadata"
Command="{Binding ImageExportPngWithMetadataCommand}"/>
<input:StandardUICommand
x:Key="ImageExportPngCommand"
Label="Png"
Command="{Binding ImageExportPngCommand}"/>
<input:StandardUICommand
x:Key="ImageExportJpegCommand"
Label="Jpeg"
Command="{Binding ImageExportJpegCommand}"/>
<input:StandardUICommand
x:Key="ImageExportWebpCommand"
Label="Webp"
Command="{Binding ImageExportWebpCommand}"/>
Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer},
macOS={x:Static lang:Resources.Action_OpenInFinder}}" />
<input:StandardUICommand
x:Key="ImageExportPngWithMetadataCommand"
Command="{Binding ImageExportPngWithMetadataCommand}"
Label="Png with Metadata" />
<input:StandardUICommand
x:Key="ImageExportPngCommand"
Command="{Binding ImageExportPngCommand}"
Label="Png" />
<input:StandardUICommand
x:Key="ImageExportJpegCommand"
Command="{Binding ImageExportJpegCommand}"
Label="Jpeg" />
<input:StandardUICommand
x:Key="ImageExportWebpCommand"
Command="{Binding ImageExportWebpCommand}"
Label="Webp" />
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<!--<WrapLayout HorizontalSpacing="2" VerticalSpacing="2" />-->
<UniformGridLayout
Orientation="Horizontal"
MinColumnSpacing="2"
MinRowSpacing="2" />
MinRowSpacing="2"
Orientation="Horizontal" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type dbModels:LocalImageFile}">
<Button
ToolTip.Placement="LeftEdgeAlignedTop"
Padding="4"
Classes="transparent-full"
Command="{StaticResource ImageClickCommand}"
CommandParameter="{Binding $self.DataContext}"
CornerRadius="8"
HotKey="{x:Null}"
Padding="4"
Classes="transparent-full"
CornerRadius="8">
ToolTip.Placement="LeftEdgeAlignedTop">
<Interaction.Behaviors>
<BehaviorCollection>
<ContextDragBehavior HorizontalDragThreshold="6" VerticalDragThreshold="6"/>
<ContextDragBehavior HorizontalDragThreshold="6" VerticalDragThreshold="6" />
</BehaviorCollection>
</Interaction.Behaviors>
<Button.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
<ui:MenuFlyoutItem
Command="{StaticResource ImageDeleteCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
Command="{StaticResource ImageCopyCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{StaticResource ImageOpenCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutSubItem Text="Export" IconSource="Share">
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutSubItem IconSource="Share" Text="Export">
<ui:MenuFlyoutItem
Command="{StaticResource ImageExportPngWithMetadataCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportPngCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportJpegCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportWebpCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Button.ContextFlyout>
<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>
<Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage
Width="150"
Height="190"
Source="{Binding GlobalFullPath}"
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding AbsolutePath}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Border
Grid.Row="1"
Classes="theme-dark"
MinHeight="20"
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Padding="4,0,0,0"
VerticalAlignment="Bottom"
Classes="theme-dark"
Opacity="0.9">
<TextBlock
FontSize="12"
<TextBlock
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{Binding FileNameWithoutExtension}"/>
FontSize="12"
Text="{Binding FileNameWithoutExtension}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
</Border>
@ -206,10 +229,10 @@
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</scroll:BetterScrollViewer>
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>
</ControlTheme>
</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;
[Transient]
public class ImageFolderCard : DropTargetTemplatedControlBase
{
private ItemsRepeater? imageRepeater;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
imageRepeater = e.NameScope.Find<ItemsRepeater>("ImageRepeater");
base.OnApplyTemplate(e);
}
/// <inheritdoc />
protected override void DropHandler(object? sender, DragEventArgs e)
{
@ -17,4 +31,30 @@ public class ImageFolderCard : DropTargetTemplatedControlBase
base.DragOverHandler(sender, e);
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>
</Design.PreviewWith>
<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="controls|ImageGalleryCard">
<!-- Set Defaults -->
<Setter Property="Template">
@ -31,6 +26,12 @@
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>
</Grid.Styles>
<!-- Main image view -->
<Border
Classes="theme-dark"

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

@ -1,7 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
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.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;

190
StabilityMatrix.Avalonia/Controls/ModelCard.axaml

@ -1,41 +1,145 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
x:DataType="inference:ModelCardViewModel">
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel VerticalAlignment="Center" Width="300">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/>
<StackPanel Width="300" VerticalAlignment="Center">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|ModelCard">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*,Auto">
<!-- Model -->
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<!-- Model -->
<TextBlock
Grid.Column="0"
TextAlignment="Left"
VerticalAlignment="Center"
MinWidth="60"
Text="{x:Static lang:Resources.Label_Model}" />
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
IsTextSearchEnabled="True"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
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>
<DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
@ -70,67 +174,63 @@
<Button
Grid.Row="0"
Grid.Column="2"
Grid.Column="2"
Margin="8,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem
IsChecked="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"/>
<ui:ToggleMenuFlyoutItem
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"/>
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
<!-- Refiner -->
<!-- Refiner -->
<TextBlock
Grid.Column="0"
Grid.Row="1"
Grid.Column="0"
MinWidth="60"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Refiner}" />
IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsRefinerSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedRefiner}" />
<!-- VAE -->
<!-- VAE -->
<TextBlock
Grid.Column="0"
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
IsVisible="{Binding IsVaeSelectionEnabled}"
Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VAE}" />
IsVisible="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsVaeSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.VaeModels}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedVae}" />
</Grid>
</controls:Card>
</ControlTemplate>

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

@ -1,9 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
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 Avalonia;
using Avalonia.Controls.Primitives;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.Input;
namespace StabilityMatrix.Avalonia.Controls;

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

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

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

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

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

@ -1,7 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
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 StabilityMatrix.Core.Attributes;
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 { }

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;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class SharpenCard : TemplatedControl { }

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

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

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

@ -1,8 +1,10 @@
using Avalonia;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class StackExpander : TemplatedControl
{
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.Primitives;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class UpscalerCard : TemplatedControl
{
/// <inheritdoc />

123
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
@ -20,6 +22,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -29,7 +32,9 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
@ -52,7 +57,7 @@ public static class DesignData
if (isInitialized)
throw new InvalidOperationException("DesignData is already initialized.");
var services = new ServiceCollection();
var services = App.ConfigureServices();
var activePackageId = Guid.NewGuid();
services.AddSingleton<ISettingsManager, MockSettingsManager>(
@ -106,18 +111,18 @@ public static class DesignData
// Mock services
services
.AddSingleton<INotificationService, MockNotificationService>()
.AddSingleton<ISharedFolders, MockSharedFolders>()
.AddSingleton<IDownloadService, MockDownloadService>()
.AddSingleton<IHttpClientFactory, MockHttpClientFactory>()
.AddSingleton<IApiFactory, MockApiFactory>()
.AddSingleton(Substitute.For<INotificationService>())
.AddSingleton(Substitute.For<ISharedFolders>())
.AddSingleton(Substitute.For<IDownloadService>())
.AddSingleton(Substitute.For<IHttpClientFactory>())
.AddSingleton(Substitute.For<IApiFactory>())
.AddSingleton(Substitute.For<IDiscordRichPresenceService>())
.AddSingleton(Substitute.For<ITrackedDownloadService>())
.AddSingleton(Substitute.For<ILiteDbContext>())
.AddSingleton<IInferenceClientManager, MockInferenceClientManager>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>()
.AddSingleton<ICompletionProvider, MockCompletionProvider>()
.AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<IImageIndexService, MockImageIndexService>()
.AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>()
.AddSingleton<IPackageModificationRunner, PackageModificationRunner>();
.AddSingleton<IImageIndexService, MockImageIndexService>();
// Placeholder services that nobody should need during design time
services
@ -128,12 +133,6 @@ public static class DesignData
.AddSingleton<ITokenizerProvider>(_ => 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
services.Remove(ServiceDescriptor.Singleton<LaunchPageViewModel, LaunchPageViewModel>());
services.AddSingleton<LaunchPageViewModel, MockLaunchPageViewModel>();
@ -172,12 +171,61 @@ public static class DesignData
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = packageFactory
.GetAllAvailablePackages()
.ToImmutableArray();
InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
packageFactory.GetAllAvailablePackages()
);
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
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
CheckpointsPageViewModel.CheckpointFolders =
new CheckpointFolder[]
@ -336,6 +384,26 @@ public static class DesignData
public static LaunchPageViewModel 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
{
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"), };
});
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 =>
DialogFactory.Get<InferenceTextToImageViewModel>(vm =>
{
@ -603,8 +680,8 @@ The gallery images are often inpainted, but you will get something very similar
get
{
var list = new CompletionList { IsFiltering = true };
list.CompletionData.AddRange(SampleCompletionData);
list.FilteredCompletionData.AddRange(list.CompletionData);
ExtensionMethods.AddRange(list.CompletionData, SampleCompletionData);
ExtensionMethods.AddRange(list.FilteredCompletionData, list.CompletionData);
list.SelectItem("te", true);
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 DynamicData;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
@ -10,47 +10,50 @@ namespace StabilityMatrix.Avalonia.DesignData;
public class MockImageIndexService : IImageIndexService
{
/// <inheritdoc />
public IndexCollection<LocalImageFile, string> InferenceImages { get; } =
new IndexCollection<LocalImageFile, string>(null!, file => file.RelativePath)
public IndexCollection<LocalImageFile, string> InferenceImages { get; }
public MockImageIndexService()
{
InferenceImages = new IndexCollection<LocalImageFile, string>(
this,
file => file.AbsolutePath
)
{
RelativePath = "inference"
RelativePath = "Inference"
};
}
/// <inheritdoc />
public Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix)
public Task RefreshIndexForAllCollections()
{
return Task.FromResult(
(IReadOnlyList<LocalImageFile>)
new LocalImageFile[]
{
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152",
},
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024",
},
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152",
}
}
);
return RefreshIndex(InferenceImages);
}
/// <inheritdoc />
public Task RefreshIndexForAllCollections()
private static LocalImageFile GetSampleImage(string url)
{
return Task.CompletedTask;
return new LocalImageFile
{
AbsolutePath = url,
GenerationParameters = GenerationParameters.GetSample(),
ImageSize = new System.Drawing.Size(1024, 1024)
};
}
/// <inheritdoc />
public Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
{
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;
}
@ -59,10 +62,4 @@ public class MockImageIndexService : IImageIndexService
{
throw new System.NotImplementedException();
}
/// <inheritdoc />
public Task RemoveImage(LocalImageFile imageFile)
{
throw new System.NotImplementedException();
}
}

12
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
@ -48,6 +49,17 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
/// <inheritdoc />
public bool CanUserDisconnect => IsConnected && !IsConnecting;
public MockInferenceClientManager()
{
Models.AddRange(
new[]
{
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("artshaper1.safetensors"),
}
);
}
/// <inheritdoc />
public Task CopyImageToInputAsync(
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.Controls;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.ComponentModel.__Internals;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Markdown.Avalonia;
using Markdown.Avalonia.SyntaxHigh.Extensions;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Controls;
@ -26,7 +25,6 @@ using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars;
using Process = FuzzySharp.Process;
@ -96,6 +94,18 @@ public static class DialogHelper
Watermark = field.Watermark,
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);
// When IsValid property changes, update invalid count and primary button
@ -427,7 +437,7 @@ public static class DialogHelper
AllowScrollBelowDocument = false
}
};
TextEditorConfigs.ConfigForPrompt(textEditor);
TextEditorConfigs.Configure(textEditor, TextEditorPreset.Prompt);
textEditor.Document.Text = errorLineFormatted;
textEditor.TextArea.Caret.Offset = textEditor.Document.Lines[0].EndOffset;
@ -518,15 +528,6 @@ public static class DialogHelper
XamlRoot = App.VisualRoot
};
}
/// <summary>
/// Creates a connection help dialog.
/// </summary>
public static BetterContentDialog CreateConnectionHelpDialog()
{
// TODO
return new BetterContentDialog();
}
}
// Text fields
@ -541,6 +542,9 @@ public sealed class TextBoxField : INotifyPropertyChanged
// Watermark text
public string Watermark { get; init; } = string.Empty;
// Inner left value
public string? InnerLeftText { get; init; }
/// <summary>
/// Validation action on text changes. Throw exception if invalid.
/// </summary>

16
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

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

47
StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs

@ -13,50 +13,57 @@ public static class ImageProcessor
/// </summary>
public static (int rows, int columns) GetGridDimensionsFromImageCount(int count)
{
if (count <= 1) return (1, 1);
if (count == 2) return (1, 2);
if (count <= 1)
return (1, 1);
if (count == 2)
return (1, 2);
// Prefer one extra row over one extra column,
// the row count will be the floor of the square root
// and the column count will be floor of count / rows
var rows = (int) Math.Floor(Math.Sqrt(count));
var columns = (int) Math.Floor((double) count / rows);
var rows = (int)Math.Floor(Math.Sqrt(count));
var columns = (int)Math.Floor((double)count / rows);
return (rows, columns);
}
public static SKImage CreateImageGrid(
IReadOnlyList<SKImage> images,
int spacing = 0)
public static SKImage CreateImageGrid(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 singleWidth = images[0].Width;
var singleHeight = images[0].Height;
// Make output image
using var output = new SKBitmap(
singleWidth * columns + spacing * (columns - 1),
singleHeight * rows + spacing * (rows - 1));
singleWidth * columns + spacing * (columns - 1),
singleHeight * rows + spacing * (rows - 1)
);
// Draw images
using var canvas = new SKCanvas(output);
foreach (var (row, column) in
Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns)))
foreach (
var (row, column) in Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns))
)
{
// Stop if we have drawn all images
var index = row * columns + column;
if (index >= images.Count) break;
if (index >= images.Count)
break;
// Get image
var image = images[index];
// Draw image
var destination = new SKRect(
singleWidth * column + spacing * column,
singleHeight * row + spacing * row,
singleWidth * column + spacing * column + image.Width,
singleHeight * row + spacing * row + image.Height);
singleHeight * row + spacing * row + image.Height
);
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.Text;
using System.Text.Json;
using Avalonia;
using Force.Crc32;
using StabilityMatrix.Avalonia.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[] 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(
byte[] inputImage,
GenerationParameters generationParameters,

19
StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs

@ -1,24 +1,21 @@
using System.Collections.Generic;
using System.Data.Common;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using Sylvan.Data.Csv;
using Sylvan;
using Sylvan.Data;
namespace StabilityMatrix.Avalonia.Helpers;
public class TagCsvParser
{
private readonly Stream stream;
public TagCsvParser(Stream stream)
{
this.stream = stream;
}
public async IAsyncEnumerable<TagCsvEntry> ParseAsync()
{
var pool = new StringPool();
@ -27,10 +24,10 @@ public class TagCsvParser
StringFactory = pool.GetString,
HasHeaders = false,
};
using var textReader = new StreamReader(stream);
await using var dataReader = await CsvDataReader.CreateAsync(textReader, options);
while (await dataReader.ReadAsync())
{
var entry = new TagCsvEntry
@ -42,7 +39,7 @@ public class TagCsvParser
};
yield return entry;
}
/*var dataBinderOptions = new DataBinderOptions
{
BindingMode = DataBindingMode.Any
@ -54,17 +51,17 @@ public class TagCsvParser
public async Task<Dictionary<string, TagCsvEntry>> GetDictionaryAsync()
{
var dict = new Dictionary<string, TagCsvEntry>();
await foreach (var entry in ParseAsync())
{
if (entry.Name is null || entry.Type is null)
{
continue;
}
dict.Add(entry.Name, entry);
}
return dict;
}
}

38
StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs

@ -1,5 +1,5 @@
using System.IO;
using System.Reflection;
using System;
using System.IO;
using Avalonia.Media;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
@ -18,13 +18,22 @@ public static class TextEditorConfigs
{
public static void Configure(TextEditor editor, TextEditorPreset preset)
{
if (preset == TextEditorPreset.Prompt)
switch (preset)
{
ConfigForPrompt(editor);
case TextEditorPreset.Prompt:
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;
var registryOptions = new RegistryOptions(themeName);
@ -58,6 +67,25 @@ public static class TextEditorConfigs
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)
{
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 =
args.Length == 0 ? "git" : "git " + string.Join(" ", args.Select(ProcessRunner.Quote));
@ -229,6 +233,13 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
throw new NotImplementedException();
}
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)
{
throw new PlatformNotSupportedException();
}
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null)

193
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -17,70 +17,87 @@ namespace StabilityMatrix.Avalonia.Helpers;
public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IGitHubClient gitHubClient;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
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 VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe");
private string AssetsDir => Path.Combine(HomeDir, "Assets");
private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe");
private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip");
private string PythonDir => Path.Combine(AssetsDir, "Python310");
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip");
private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
// Temporary directory to extract venv to during python install
private string VenvTempDir => Path.Combine(PythonDir, "venv");
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit");
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.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 bool IsPythonInstalled => File.Exists(PythonDllPath);
public WindowsPrerequisiteHelper(
IGitHubClient gitHubClient,
IDownloadService downloadService,
ISettingsManager settingsManager)
IDownloadService downloadService,
ISettingsManager settingsManager
)
{
this.gitHubClient = gitHubClient;
this.downloadService = downloadService;
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,
environmentVariables: new Dictionary<string, string>
{
{"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
});
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
},
outputDataReceived: onProcessOutput
);
await ProcessRunner.WaitForExitConditionAsync(process);
}
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{
var process = await ProcessRunner.GetProcessOutputAsync(
GitExePath, string.Join(" ", args),
GitExePath,
string.Join(" ", args),
workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string>
{
{"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
});
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
}
);
return process;
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);
@ -97,16 +114,20 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
(Assets.SevenZipExecutable, 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);
foreach (var (asset, extractDir) in assets)
{
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)
@ -120,7 +141,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Logger.Info("Python not found at {PythonDllPath}, downloading...", PythonDllPath);
Directory.CreateDirectory(AssetsDir);
// Delete existing python zip if it exists
if (File.Exists(PythonLibraryZipPath))
{
@ -130,44 +151,45 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
var remote = Assets.PythonDownloadUrl;
var url = remote.Url.ToString();
Logger.Info($"Downloading Python from {url} to {PythonLibraryZipPath}");
// Cleanup to remove zip if download fails
try
{
// Download python zip
await downloadService.DownloadToFileAsync(url, PythonDownloadPath, progress: progress);
// Verify python hash
var downloadHash = await FileHash.GetSha256Async(PythonDownloadPath, progress);
if (downloadHash != remote.HashSha256)
{
var fileExists = File.Exists(PythonDownloadPath);
var fileSize = new FileInfo(PythonDownloadPath).Length;
var msg = $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " +
$"(file exists: {fileExists}, size: {fileSize})";
var msg =
$"Python download hash mismatch: {downloadHash} != {remote.HashSha256} "
+ $"(file exists: {fileExists}, size: {fileSize})";
throw new Exception(msg);
}
progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete"));
progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true));
// We also need 7z if it's not already unpacked
if (!File.Exists(SevenZipPath))
{
await Assets.SevenZipExecutable.ExtractToDir(AssetsDir);
await Assets.SevenZipLicense.ExtractToDir(AssetsDir);
}
// Delete existing python dir
if (Directory.Exists(PythonDir))
{
Directory.Delete(PythonDir, true);
}
// Unzip python
await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir);
try
{
// Extract embedded venv folder
@ -185,7 +207,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
await resource.ExtractTo(path);
}
// Add venv to python's library zip
await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir);
}
finally
@ -196,16 +218,19 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Directory.Delete(VenvTempDir, true);
}
}
// Extract get-pip.pyc
await Assets.PyScriptGetPip.ExtractToDir(PythonDir);
// We need to uncomment the #import site line in python310._pth for pip to work
var pythonPthPath = Path.Combine(PythonDir, "python310._pth");
var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath);
pythonPthContent = pythonPthContent.Replace("#import site", "import site");
await File.WriteAllTextAsync(pythonPthPath, pythonPthContent);
// Install TKinter
await InstallTkinterIfNecessary(progress);
progress?.Report(new ProgressReport(1f, "Python install complete"));
}
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)
{
if (File.Exists(GitExePath))
@ -225,7 +283,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Logger.Debug("Git already installed at {GitExePath}", GitExePath);
return;
}
Logger.Info("Git not found at {GitExePath}, downloading...", GitExePath);
var portableGitUrl =
@ -233,7 +291,11 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
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"));
}
@ -245,7 +307,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{
var registry = Registry.LocalMachine;
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)
{
var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -254,20 +318,44 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
return;
}
}
Logger.Info("Downloading VC Redist");
await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress);
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete",
type: ProgressType.Download));
await downloadService.DownloadToFileAsync(
VcRedistDownloadUrl,
VcRedistDownloadPath,
progress: progress
);
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ download complete",
type: ProgressType.Download
)
);
Logger.Info("Installing VC Redist");
progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites..."));
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
var process = ProcessRunner.StartAnsiProcess(
VcRedistDownloadPath,
"/install /quiet /norestart"
);
await process.WaitForExitAsync();
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete",
type: ProgressType.Generic));
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ install complete",
type: ProgressType.Generic
)
);
File.Delete(VcRedistDownloadPath);
}
@ -286,5 +374,4 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
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>
/// Looks up a localized string similar to Close.
/// </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>
/// Looks up a localized string similar to Continue.
/// </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>
/// Looks up a localized string similar to Delete.
/// </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>
/// Looks up a localized string similar to Edit.
/// </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>
/// Looks up a localized string similar to Open on CivitAI.
/// </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>
/// Looks up a localized string similar to Relaunch.
/// </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>
/// Looks up a localized string similar to Select Directory.
/// </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>
/// Looks up a localized string similar to Show in Explorer.
/// </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>
/// Looks up a localized string similar to Yes.
/// </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>
/// Looks up a localized string similar to Automatically scroll to end of console output.
/// </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>
/// Looks up a localized string similar to Current directory:.
/// </summary>
@ -888,7 +987,16 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <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>
public static string Label_ImportAsConnected {
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>
/// Looks up a localized string similar to Input.
/// </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>
/// Looks up a localized string similar to Only available on Windows.
/// </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>
/// Looks up a localized string similar to Package Environment.
/// </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>
/// Looks up a localized string similar to Python Version Info.
/// </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>
/// Looks up a localized string similar to Theme.
/// </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>
/// Looks up a localized string similar to VAE.
/// </summary>

70
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -379,7 +379,7 @@
<value>Drop file here to import</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Import as Connected</value>
<value>Import with Metadata</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Search for connected metadata on new local imports</value>
@ -678,7 +678,73 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restore Default Layout</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Output Sharing</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Batch Index</value>
</data>
</root>
<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>

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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;

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

@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
using StabilityMatrix.Avalonia.ViewModels.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;

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 StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Models;
/// <summary>
/// Singleton DI service for observable shared UI state.
/// </summary>
[Singleton]
public partial class SharedState : ObservableObject
{
/// <summary>
/// Whether debug mode enabled from settings page version tap.
/// </summary>
[ObservableProperty] private bool isDebugMode;
[ObservableProperty]
private bool isDebugMode;
}

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

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

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

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

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

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

3
StabilityMatrix.Avalonia/Models/TextEditorPreset.cs

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

13
StabilityMatrix.Avalonia/Program.cs

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

1
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

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

28
StabilityMatrix.Avalonia/Services/INotificationService.cs

@ -2,6 +2,8 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services;
@ -11,7 +13,8 @@ public interface INotificationService
public void Initialize(
Visual? visual,
NotificationPosition position = NotificationPosition.BottomRight,
int maxItems = 3);
int maxItems = 3
);
public void Show(INotification notification);
@ -26,7 +29,8 @@ public interface INotificationService
Task<T> task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error);
NotificationType appearance = NotificationType.Error
);
/// <summary>
/// Attempt to run the given void task, showing a generic error notification if it fails.
@ -40,16 +44,18 @@ public interface INotificationService
Task task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error);
NotificationType appearance = NotificationType.Error
);
/// <summary>
/// Show a notification with the given parameters.
/// </summary>
void Show(
string title,
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null);
TimeSpan? expiration = null
);
/// <summary>
/// Show a notification that will not auto-dismiss.
@ -60,5 +66,15 @@ public interface INotificationService
void ShowPersistent(
string title,
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.TagCompletion;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Inference;
using StabilityMatrix.Core.Models;
@ -27,6 +28,7 @@ namespace StabilityMatrix.Avalonia.Services;
/// Manager for the current inference client
/// Has observable shared properties for shared info like model names
/// </summary>
[Singleton(typeof(IInferenceClientManager))]
public partial class InferenceClientManager : ObservableObject, IInferenceClientManager
{
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);
}
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 />
public async Task ConnectAsync(
PackagePair packagePair,
@ -367,11 +407,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
logger.LogError(ex, "Error setting up completion provider");
});
// Setup image folder links
await comfyPackage.SetupInferenceOutputFolderLinks(
packagePair.InstalledPackage.FullPath
?? throw new InvalidOperationException("Package does not have a Path")
);
await MigrateLinksIfNeeded(packagePair);
// Get user defined host and port
var host = packagePair.InstalledPackage.GetLaunchArgsHost();

4
StabilityMatrix.Avalonia/Services/NavigationService.cs

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

53
StabilityMatrix.Avalonia/Services/NotificationService.cs

@ -3,20 +3,32 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(INotificationService))]
public class NotificationService : INotificationService
{
private readonly ILogger<NotificationService> logger;
private WindowNotificationManager? notificationManager;
public NotificationService(ILogger<NotificationService> logger)
{
this.logger = logger;
}
public void Initialize(
Visual? visual,
Visual? visual,
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))
{
Position = position,
@ -30,28 +42,44 @@ public class NotificationService : INotificationService
}
public void Show(
string title,
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null)
TimeSpan? expiration = null
)
{
Show(new Notification(title, message, appearance, expiration));
}
public void ShowPersistent(
string title,
string title,
string message,
NotificationType appearance = NotificationType.Information)
NotificationType appearance = NotificationType.Information
)
{
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 />
public async Task<TaskResult<T>> TryAsync<T>(
Task<T> task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error)
NotificationType appearance = NotificationType.Error
)
{
try
{
@ -63,13 +91,14 @@ public class NotificationService : INotificationService
return TaskResult<T>.FromException(e);
}
}
/// <inheritdoc />
public async Task<TaskResult<bool>> TryAsync(
Task task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error)
NotificationType appearance = NotificationType.Error
)
{
try
{

15
StabilityMatrix.Avalonia/Services/ServiceManager.cs

@ -256,4 +256,19 @@ public class ServiceManager<T>
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>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.5.5-dev.1</Version>
<Version>2.6.0-dev.1</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -23,46 +23,48 @@
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.1" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" />
<PackageReference Include="Avalonia" Version="11.0.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.4" />
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
<!--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.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="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Dock.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="DynamicData" Version="7.14.2" />
<PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.217" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.217" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.220" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.220" />
<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="Microsoft.Extensions.Configuration" 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.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="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="5.2.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.2" />
<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
<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.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="Sentry" Version="3.33.1" />
<PackageReference Include="Sentry.NLog" Version="3.33.1" />
<PackageReference Include="Sentry" Version="3.40.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.Data" Version="0.2.12" />
<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>

3
StabilityMatrix.Avalonia/Styles/ThemeColors.cs

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

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

@ -1,10 +1,14 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
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>? SecondaryButtonClick;
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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using NLog;
@ -27,6 +29,8 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -41,6 +45,7 @@ public abstract partial class InferenceGenerationViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
@ -60,11 +65,13 @@ public abstract partial class InferenceGenerationViewModelBase
protected InferenceGenerationViewModelBase(
ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager,
INotificationService notificationService
INotificationService notificationService,
ISettingsManager settingsManager
)
: base(notificationService)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.vmFactory = vmFactory;
ClientManager = inferenceClientManager;
@ -75,6 +82,101 @@ public abstract partial class InferenceGenerationViewModelBase
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>
/// Builds the image generation prompt
/// </summary>
@ -156,7 +258,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Wait for prompt to finish
await promptTask.Task.WaitAsync(cancellationToken);
Logger.Trace($"Prompt task {promptTask.Id} finished");
Logger.Debug($"Prompt task {promptTask.Id} finished");
// Get output images
var imageOutputs = await client.GetImagesForExecutedPromptAsync(
@ -164,6 +266,20 @@ public abstract partial class InferenceGenerationViewModelBase
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
await promptInterrupt.DisposeAsync();
@ -172,15 +288,6 @@ public abstract partial class InferenceGenerationViewModelBase
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);
}
finally
@ -207,19 +314,22 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGenerationEventArgs args
)
{
var client = args.Client;
// Write metadata to images
var outputImagesBytes = new List<byte[]>();
var outputImages = new List<ImageSource>();
foreach (
var (i, filePath) in images
.Select(image => image.ToFilePath(args.Client.OutputImagesDir!))
.Enumerate()
)
foreach (var (i, comfyImage) in images.Enumerate())
{
if (!filePath.Exists)
{
Logger.Warn($"Image file {filePath} does not exist");
continue;
}
Logger.Debug("Downloading image: {FileName}", comfyImage.FileName);
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 project = args.Project!;
@ -248,17 +358,15 @@ public abstract partial class InferenceGenerationViewModelBase
);
}
var bytesWithMetadata = PngDataHelper.AddMetadata(
await filePath.ReadAllBytesAsync(),
parameters,
project
);
var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
await using (var outputStream = filePath.Info.OpenWrite())
{
await outputStream.WriteAsync(bytesWithMetadata);
await outputStream.FlushAsync();
}
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(bytesWithMetadata),
args,
i + 1,
images.Count
);
outputImages.Add(new ImageSource(filePath));
@ -268,17 +376,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Download all images to make grid, if multiple
if (outputImages.Count > 1)
{
var outputDir = outputImages[0].LocalFile!.Directory;
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 loadedImages = outputImagesBytes.Select(SKImage.FromEncodedData).ToImmutableArray();
var project = args.Project!;
@ -297,13 +395,11 @@ public abstract partial class InferenceGenerationViewModelBase
);
// Save to disk
var lastName = outputImages.Last().LocalFile?.Info.Name;
var gridPath = outputDir!.JoinFile($"grid-{lastName}");
await using (var fileStream = gridPath.Info.OpenWrite())
{
await fileStream.WriteAsync(gridBytesWithMetadata);
}
var gridPath = await WriteOutputImageAsync(
new MemoryStream(gridBytesWithMetadata),
args,
isGrid: true
);
// Insert to start of images
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)
{
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.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.Avalonia.ViewModels.Base;

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

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

4
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -5,16 +5,13 @@ using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
@ -42,6 +39,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointBrowserPage))]
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase
{
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 FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
@ -19,6 +21,8 @@ using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
[ManagedService]
[Transient]
public partial class CheckpointFile : ViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -257,6 +261,11 @@ public partial class CheckpointFile : ViewModelBase
.Where(File.Exists)
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(checkpointFile.PreviewImagePath))
{
checkpointFile.PreviewImagePath = Assets.NoImage.ToString();
}
yield return checkpointFile;
}
}

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

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

1
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -24,6 +24,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointsPage))]
[Singleton]
public partial class CheckpointsPageViewModel : PageViewModelBase
{
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;
[View(typeof(DownloadResourceDialog))]
[ManagedService]
[Transient]
public partial class DownloadResourceViewModel : ContentDialogViewModelBase
{
private readonly IDownloadService downloadService;

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

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

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

@ -6,11 +6,13 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(ExceptionDialog))]
[ManagedService]
[Transient]
public partial class ExceptionViewModel : ViewModelBase
{
public Exception? Exception { get; set; }
public string? Message => Exception?.Message;
public string? ExceptionType => Exception?.GetType().Name ?? "";
}

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

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

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

Loading…
Cancel
Save