diff --git a/.github/workflows/test-ui.yml b/.github/workflows/test-ui.yml
new file mode 100644
index 00000000..244472bb
--- /dev/null
+++ b/.github/workflows/test-ui.yml
@@ -0,0 +1,27 @@
+name: UI Tests
+
+on:
+ workflow_dispatch:
+
+concurrency:
+ group: build-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ if: github.repository == 'LykosAI/StabilityMatrix' || github.event_name == 'workflow_dispatch'
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '7.0.x'
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: Test
+ run: dotnet test StabilityMatrix.UITests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f69ee68d..07cb51e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,43 @@ 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
+- Added "Open on GitHub" option to the three-dots menu on the Packages page
+### 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
diff --git a/Jenkinsfile b/Jenkinsfile
index 7b493b1a..51dddff3 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -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') {
diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
index cfbcd729..75091167 100644
--- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
+++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
@@ -19,12 +19,12 @@
-
-
+
+
-
-
+
+
diff --git a/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs b/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
index fef4f8db..66a66152 100644
--- a/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
+++ b/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
///
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)
diff --git a/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs b/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs
index 507d9872..7014e63f 100644
--- a/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index 0258b36b..f33a4d5c 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -20,6 +20,8 @@
+
+
@@ -56,10 +58,10 @@
-
+
-->
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+ FontSize="12"
+ Text="{Binding FileNameWithoutExtension}"
+ TextAlignment="Center"
+ TextTrimming="CharacterEllipsis" />
@@ -206,10 +229,10 @@
-
+
-
-
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs
index b36e1fe1..08a9cca5 100644
--- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs
+++ b/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("ImageRepeater");
+ base.OnApplyTemplate(e);
+ }
+
///
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;
+ }
}
diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml
index 3fe4071e..6fb6e844 100644
--- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml
@@ -13,11 +13,6 @@
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ IsVisible="{Binding IsRefinerSelectionEnabled}"
+ Text="{x:Static lang:Resources.Label_Refiner}"
+ TextAlignment="Left" />
+
-
-
+
+
-
+ IsVisible="{Binding IsVaeSelectionEnabled}"
+ Text="{x:Static lang:Resources.Label_VAE}"
+ TextAlignment="Left" />
+
-
+
diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs
index e997a874..622f1cbe 100644
--- a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs
+++ b/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
-{
-}
\ No newline at end of file
+[Transient]
+public class ModelCard : TemplatedControl { }
diff --git a/StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs b/StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs
index 9342ec12..ef1f1114 100644
--- a/StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs
index 47a02af1..f5a79dcb 100644
--- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs
+++ b/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
{
///
@@ -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)
diff --git a/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs b/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs
index f5620f12..1943c3ae 100644
--- a/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs
+++ b/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()
diff --git a/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml.cs
index 7e732e0f..ace972b1 100644
--- a/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml.cs
+++ b/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 { }
diff --git a/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs b/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs
new file mode 100644
index 00000000..a5f30ff1
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml b/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml
new file mode 100644
index 00000000..48f59f05
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ Item 7
+ Item 8
+ Item 9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs b/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs
new file mode 100644
index 00000000..05adc971
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs
@@ -0,0 +1,5 @@
+using Avalonia.Controls;
+
+namespace StabilityMatrix.Avalonia.Controls.Scroll;
+
+public class BetterScrollViewer : ScrollViewer { }
diff --git a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SeedCard.axaml.cs
index f08853dd..06d164e9 100644
--- a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml.cs
+++ b/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 { }
diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs
index dd748696..726cf414 100644
--- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs
+++ b/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 { }
diff --git a/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
new file mode 100644
index 00000000..c94276b1
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
new file mode 100644
index 00000000..7878bf8b
--- /dev/null
+++ b/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 IsSelectedProperty =
+ ToggleButton.IsCheckedProperty.AddOwner();
+
+ public static readonly StyledProperty SourceProperty =
+ AdvancedImage.SourceProperty.AddOwner();
+
+ public static readonly StyledProperty ImageWidthProperty = AvaloniaProperty.Register<
+ SelectableImageButton,
+ double
+ >("ImageWidth", 300);
+
+ public static readonly StyledProperty ImageHeightProperty = AvaloniaProperty.Register<
+ SelectableImageButton,
+ double
+ >("ImageHeight", 300);
+
+ static SelectableImageButton()
+ {
+ AffectsRender(ImageWidthProperty, ImageHeightProperty);
+ AffectsArrange(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);
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs
index 6fd22370..4c2a250d 100644
--- a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs
+++ b/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 { }
diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs
index c8374349..2341a96d 100644
--- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs
+++ b/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 SpacingProperty = AvaloniaProperty.Register(
- "Spacing", 8);
+ public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register<
+ StackCard,
+ int
+ >("Spacing", 8);
public int Spacing
{
diff --git a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs b/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
index 3fb2304b..38208891 100644
--- a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
+++ b/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 SpacingProperty = AvaloniaProperty.Register<
diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs
index 083d09fa..7e2e2327 100644
--- a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs
+++ b/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
{
///
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 43d8e751..4d785605 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/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(
@@ -106,18 +111,18 @@ public static class DesignData
// Mock services
services
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
+ .AddSingleton(Substitute.For())
.AddSingleton()
- .AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton();
+ .AddSingleton();
// Placeholder services that nobody should need during design time
services
@@ -128,12 +133,6 @@ public static class DesignData
.AddSingleton(_ => null!)
.AddSingleton(_ => 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());
services.AddSingleton();
@@ -172,12 +171,61 @@ public static class DesignData
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService();
- InstallerViewModel.AvailablePackages = packageFactory
- .GetAllAvailablePackages()
- .ToImmutableArray();
+ InstallerViewModel.AvailablePackages = new ObservableCollectionExtended(
+ 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()
+ {
+ 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
+ {
+ 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();
+ public static OutputsPageViewModel OutputsPageViewModel
+ {
+ get
+ {
+ var vm = Services.GetRequiredService();
+ vm.Outputs = new ObservableCollectionExtended
+ {
+ 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 { new("UWU", "TRUE"), };
});
+ public static PythonPackagesViewModel PythonPackagesViewModel =>
+ DialogFactory.Get(vm =>
+ {
+ vm.AddPackages(
+ new PipPackageInfo("pip", "1.0.0"),
+ new PipPackageInfo("torch", "2.1.0+cu121")
+ );
+ });
+
public static InferenceTextToImageViewModel InferenceTextToImageViewModel =>
DialogFactory.Get(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;
}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockApiFactory.cs b/StabilityMatrix.Avalonia/DesignData/MockApiFactory.cs
deleted file mode 100644
index 6bffe945..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockApiFactory.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using StabilityMatrix.Core.Api;
-
-namespace StabilityMatrix.Avalonia.DesignData;
-
-public class MockApiFactory : IApiFactory
-{
- public T CreateRefitClient(Uri baseAddress)
- {
- throw new NotImplementedException();
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockDiscordRichPresenceService.cs b/StabilityMatrix.Avalonia/DesignData/MockDiscordRichPresenceService.cs
deleted file mode 100644
index 79851cd7..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockDiscordRichPresenceService.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using StabilityMatrix.Avalonia.Services;
-
-namespace StabilityMatrix.Avalonia.DesignData;
-
-public class MockDiscordRichPresenceService : IDiscordRichPresenceService
-{
- ///
- public void Dispose()
- {
- GC.SuppressFinalize(this);
- }
-
- ///
- public void UpdateState()
- {
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs
deleted file mode 100644
index b1e08d8b..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs
+++ /dev/null
@@ -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? progress = null,
- string? httpClientName = null,
- CancellationToken cancellationToken = default
- )
- {
- return Task.CompletedTask;
- }
-
- ///
- public Task ResumeDownloadToFileAsync(
- string downloadUrl,
- string downloadPath,
- long existingFileSize,
- IProgress? progress = null,
- string? httpClientName = null,
- CancellationToken cancellationToken = default
- )
- {
- return Task.CompletedTask;
- }
-
- ///
- public Task GetFileSizeAsync(
- string downloadUrl,
- string? httpClientName = null,
- CancellationToken cancellationToken = default
- )
- {
- return Task.FromResult(0L);
- }
-
- public Task GetImageStreamFromUrl(string url)
- {
- return Task.FromResult(new MemoryStream(new byte[24]) as Stream)!;
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs b/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs
deleted file mode 100644
index 452af8c4..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs
+++ /dev/null
@@ -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();
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs
index d5ee1650..39474975 100644
--- a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs
+++ b/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
{
///
- public IndexCollection InferenceImages { get; } =
- new IndexCollection(null!, file => file.RelativePath)
+ public IndexCollection InferenceImages { get; }
+
+ public MockImageIndexService()
+ {
+ InferenceImages = new IndexCollection(
+ this,
+ file => file.AbsolutePath
+ )
{
- RelativePath = "inference"
+ RelativePath = "Inference"
};
+ }
///
- public Task> GetLocalImagesByPrefix(string pathPrefix)
+ public Task RefreshIndexForAllCollections()
{
- return Task.FromResult(
- (IReadOnlyList)
- 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);
}
- ///
- 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)
+ };
}
///
public Task RefreshIndex(IndexCollection 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();
}
-
- ///
- public Task RemoveImage(LocalImageFile imageFile)
- {
- throw new System.NotImplementedException();
- }
}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs b/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
index dffaee55..70b928f4 100644
--- a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
+++ b/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
///
public bool CanUserDisconnect => IsConnected && !IsConnecting;
+ public MockInferenceClientManager()
+ {
+ Models.AddRange(
+ new[]
+ {
+ HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
+ HybridModelFile.FromRemote("artshaper1.safetensors"),
+ }
+ );
+ }
+
///
public Task CopyImageToInputAsync(
FilePath imageFile,
diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
deleted file mode 100644
index a8f2996e..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
+++ /dev/null
@@ -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 CivitModels => throw new NotImplementedException();
- public ILiteCollectionAsync CivitModelVersions =>
- throw new NotImplementedException();
- public ILiteCollectionAsync CivitModelQueryCache =>
- throw new NotImplementedException();
- public ILiteCollectionAsync LocalModelFiles =>
- throw new NotImplementedException();
- public ILiteCollectionAsync InferenceProjects =>
- throw new NotImplementedException();
- public ILiteCollectionAsync LocalImageFiles =>
- throw new NotImplementedException();
-
- public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
- string hashBlake3
- )
- {
- return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null));
- }
-
- public Task UpsertCivitModelAsync(CivitModel civitModel)
- {
- return Task.FromResult(true);
- }
-
- public Task UpsertCivitModelAsync(IEnumerable civitModels)
- {
- return Task.FromResult(true);
- }
-
- public Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry)
- {
- return Task.FromResult(true);
- }
-
- public Task GetGithubCacheEntry(string cacheKey)
- {
- return Task.FromResult(null);
- }
-
- public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry)
- {
- return Task.FromResult(true);
- }
-
- public void Dispose()
- {
- GC.SuppressFinalize(this);
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs b/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs
deleted file mode 100644
index 4fdde7ad..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs
+++ /dev/null
@@ -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> TryAsync(Task task, string title = "Error", string? message = null,
- NotificationType appearance = NotificationType.Error)
- {
- return Task.FromResult(new TaskResult(default!));
- }
-
- public Task> TryAsync(Task task, string title = "Error", string? message = null,
- NotificationType appearance = NotificationType.Error)
- {
- return Task.FromResult(new TaskResult(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)
- {
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs b/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs
deleted file mode 100644
index 35c21160..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs
+++ /dev/null
@@ -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()
- {
- }
-}
diff --git a/StabilityMatrix.Avalonia/DesignData/MockTrackedDownloadService.cs b/StabilityMatrix.Avalonia/DesignData/MockTrackedDownloadService.cs
deleted file mode 100644
index 4522bd2d..00000000
--- a/StabilityMatrix.Avalonia/DesignData/MockTrackedDownloadService.cs
+++ /dev/null
@@ -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
-{
- ///
- public IEnumerable Downloads => Array.Empty();
-
- ///
- public event EventHandler? DownloadAdded;
-
- ///
- public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath)
- {
- throw new NotImplementedException();
- }
-}
diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs
index 15397e48..d99bc8dc 100644
--- a/StabilityMatrix.Avalonia/DialogHelper.cs
+++ b/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
};
}
-
- ///
- /// Creates a connection help dialog.
- ///
- 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; }
+
///
/// Validation action on text changes. Throw exception if invalid.
///
diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
index a6303f21..81691e93 100644
--- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
+++ b/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
- {
- ["filename_prefix"] = "Inference/TextToImage",
- ["images"] = builder.Connections.Image
- }
+ ClassType = "PreviewImage",
+ Inputs = new Dictionary { ["images"] = builder.Connections.Image }
}
);
- builder.Connections.OutputNodes.Add(saveImage);
+ builder.Connections.OutputNodes.Add(previewImage);
- return saveImage.Name;
+ return previewImage.Name;
}
}
diff --git a/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs b/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
index 28c215d6..a090c6a2 100644
--- a/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
+++ b/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
@@ -13,50 +13,57 @@ public static class ImageProcessor
///
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 images,
- int spacing = 0)
+
+ public static SKImage CreateImageGrid(IReadOnlyList 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);
}
diff --git a/StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs b/StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs
new file mode 100644
index 00000000..b5d280fe
--- /dev/null
+++ b/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 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
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs
index 086d0785..1d729cfe 100644
--- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs
+++ b/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,
diff --git a/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs b/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs
index 1afa66e9..414fc1a5 100644
--- a/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs
+++ b/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 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> GetDictionaryAsync()
{
var dict = new Dictionary();
-
+
await foreach (var entry in ParseAsync())
{
if (entry.Name is null || entry.Type is null)
{
continue;
}
-
+
dict.Add(entry.Name, entry);
}
-
+
return dict;
}
}
diff --git a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs b/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
index 21139027..83a744eb 100644
--- a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
+++ b/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);
diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
index 07a38fb9..04cb8ffd 100644
--- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
+++ b/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? 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? progress = null)
+ {
+ throw new PlatformNotSupportedException();
+ }
+
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallVcRedistIfNecessary(IProgress? progress = null)
diff --git a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
index e5ee642f..8df65b94 100644
--- a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
+++ b/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? onProcessOutput = null,
+ params string[] args
+ )
{
- var process = ProcessRunner.StartAnsiProcess(GitExePath, args,
+ var process = ProcessRunner.StartAnsiProcess(
+ GitExePath,
+ args,
workingDirectory: workingDirectory,
environmentVariables: new Dictionary
{
- {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
- });
-
+ { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
+ },
+ outputDataReceived: onProcessOutput
+ );
+
await ProcessRunner.WaitForExitConditionAsync(process);
}
public async Task 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
{
- {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
- });
-
+ { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
+ }
+ );
+
return process;
}
-
+
public async Task InstallAllIfNecessary(IProgress? 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? 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? 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? 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);
}
-
}
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
index 73fc62e0..569ea29d 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
+++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
@@ -113,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Clear Selection.
+ ///
+ public static string Action_ClearSelection {
+ get {
+ return ResourceManager.GetString("Action_ClearSelection", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Close.
///
@@ -131,6 +140,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Consolidate.
+ ///
+ public static string Action_Consolidate {
+ get {
+ return ResourceManager.GetString("Action_Consolidate", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Continue.
///
@@ -140,6 +158,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Copy.
+ ///
+ public static string Action_Copy {
+ get {
+ return ResourceManager.GetString("Action_Copy", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Delete.
///
@@ -149,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Downgrade.
+ ///
+ public static string Action_Downgrade {
+ get {
+ return ResourceManager.GetString("Action_Downgrade", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Edit.
///
@@ -221,6 +257,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Open on GitHub.
+ ///
+ public static string Action_OpenGithub {
+ get {
+ return ResourceManager.GetString("Action_OpenGithub", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Open in Browser.
///
@@ -248,6 +293,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Open in Image Viewer.
+ ///
+ public static string Action_OpenInViewer {
+ get {
+ return ResourceManager.GetString("Action_OpenInViewer", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Open on CivitAI.
///
@@ -284,6 +338,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Refresh.
+ ///
+ public static string Action_Refresh {
+ get {
+ return ResourceManager.GetString("Action_Refresh", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Relaunch.
///
@@ -383,6 +446,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Select All.
+ ///
+ public static string Action_SelectAll {
+ get {
+ return ResourceManager.GetString("Action_SelectAll", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Select Directory.
///
@@ -410,6 +482,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Send to Inference.
+ ///
+ public static string Action_SendToInference {
+ get {
+ return ResourceManager.GetString("Action_SendToInference", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Show in Explorer.
///
@@ -446,6 +527,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Upgrade.
+ ///
+ public static string Action_Upgrade {
+ get {
+ return ResourceManager.GetString("Action_Upgrade", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Yes.
///
@@ -509,6 +599,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Are you sure?.
+ ///
+ public static string Label_AreYouSure {
+ get {
+ return ResourceManager.GetString("Label_AreYouSure", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Automatically scroll to end of console output.
///
@@ -662,6 +761,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// 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..
+ ///
+ public static string Label_ConsolidateExplanation {
+ get {
+ return ResourceManager.GetString("Label_ConsolidateExplanation", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Current directory:.
///
@@ -888,7 +996,16 @@ namespace StabilityMatrix.Avalonia.Languages {
}
///
- /// Looks up a localized string similar to Import as Connected.
+ /// Looks up a localized string similar to Image to Image.
+ ///
+ public static string Label_ImageToImage {
+ get {
+ return ResourceManager.GetString("Label_ImageToImage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Import with Metadata.
///
public static string Label_ImportAsConnected {
get {
@@ -932,6 +1049,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Inpainting.
+ ///
+ public static string Label_Inpainting {
+ get {
+ return ResourceManager.GetString("Label_Inpainting", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Input.
///
@@ -1148,6 +1274,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to {0} images selected.
+ ///
+ public static string Label_NumImagesSelected {
+ get {
+ return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to 1 image selected.
+ ///
+ public static string Label_OneImageSelected {
+ get {
+ return ResourceManager.GetString("Label_OneImageSelected", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Only available on Windows.
///
@@ -1157,6 +1301,33 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Output Folder.
+ ///
+ public static string Label_OutputFolder {
+ get {
+ return ResourceManager.GetString("Label_OutputFolder", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Output Browser.
+ ///
+ public static string Label_OutputsPageTitle {
+ get {
+ return ResourceManager.GetString("Label_OutputsPageTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Output Type.
+ ///
+ public static string Label_OutputType {
+ get {
+ return ResourceManager.GetString("Label_OutputType", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Package Environment.
///
@@ -1247,6 +1418,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Python Packages.
+ ///
+ public static string Label_PythonPackages {
+ get {
+ return ResourceManager.GetString("Label_PythonPackages", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Python Version Info.
///
@@ -1481,6 +1661,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Text to Image.
+ ///
+ public static string Label_TextToImage {
+ get {
+ return ResourceManager.GetString("Label_TextToImage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Theme.
///
@@ -1526,6 +1715,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Upscale.
+ ///
+ public static string Label_Upscale {
+ get {
+ return ResourceManager.GetString("Label_Upscale", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Output Sharing.
+ ///
+ public static string Label_UseSharedOutputFolder {
+ get {
+ return ResourceManager.GetString("Label_UseSharedOutputFolder", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to VAE.
///
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx
index 7575d1d8..791c361b 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.resx
+++ b/StabilityMatrix.Avalonia/Languages/Resources.resx
@@ -379,7 +379,7 @@
Drop file here to import
- Import as Connected
+ Import with Metadata
Search for connected metadata on new local imports
@@ -678,7 +678,76 @@
Restore Default Layout
+
+ Output Sharing
+
Batch Index
-
\ No newline at end of file
+
+ Copy
+
+
+ Open in Image Viewer
+
+
+ {0} images selected
+
+
+ Output Folder
+
+
+ Output Type
+
+
+ Clear Selection
+
+
+ Select All
+
+
+ Send to Inference
+
+
+ Text to Image
+
+
+ Image to Image
+
+
+ Inpainting
+
+
+ Upscale
+
+
+ Output Browser
+
+
+ 1 image selected
+
+
+ Python Packages
+
+
+ Consolidate
+
+
+ Are you sure?
+
+
+ This will move all generated images from the selected packages to the Consolidated directory of the shared outputs folder. This action cannot be undone.
+
+
+ Refresh
+
+
+ Upgrade
+
+
+ Downgrade
+
+
+ Open on GitHub
+
+
diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs
new file mode 100644
index 00000000..5017ab38
--- /dev/null
+++ b/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 Parts { get; }
+
+ private FileNameFormat(string template, IReadOnlyList 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}";
+}
diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs
new file mode 100644
index 00000000..3b17284b
--- /dev/null
+++ b/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> { }
diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs
new file mode 100644
index 00000000..ff6905fd
--- /dev/null
+++ b/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>? _substitutions;
+
+ public Dictionary> Substitutions =>
+ _substitutions ??= new Dictionary>
+ {
+ { "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") }
+ };
+
+ ///
+ /// Validate a format string
+ ///
+ /// Format string
+ /// Thrown if the format string contains an unknown variable
+ [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 GetParts(string template)
+ {
+ var regex = BracketRegex();
+ var matches = regex.Matches(template);
+
+ var parts = new List();
+
+ // Loop through all parts of the string, including matches and non-matches
+ var currentIndex = 0;
+
+ foreach (var result in matches.Cast())
+ {
+ // 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;
+ }
+
+ ///
+ /// Return a sample provider for UI preview
+ ///
+ public static FileNameFormatProvider GetSample()
+ {
+ return new FileNameFormatProvider
+ {
+ GenerationParameters = GenerationParameters.GetSample(),
+ ProjectType = InferenceProjectType.TextToImage,
+ ProjectName = "Sample Project"
+ };
+ }
+
+ ///
+ /// Extract variable and index from a combined string
+ ///
+ 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);
+ }
+
+ ///
+ /// Regex for matching contents within a curly brace.
+ ///
+ [GeneratedRegex(@"\{([a-z_:\d\[\]]+)\}")]
+ private static partial Regex BracketRegex();
+
+ ///
+ /// Regex for matching a Python-like array index.
+ ///
+ [GeneratedRegex(@"\[(?:(?-?\d+)?)\:(?:(?-?\d+)?)?(?:\:(?-?\d+))?\]")]
+ private static partial Regex IndexRegex();
+
+ private record Slice(int? Start, int? End, int? Step);
+}
diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs
new file mode 100644
index 00000000..a453b3bc
--- /dev/null
+++ b/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; }
+}
diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs
index c4c76fa4..4c18bd81 100644
--- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs b/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs
index 5c782de4..e361207b 100644
--- a/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs b/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs
index f736fdd8..689fe144 100644
--- a/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs b/StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
new file mode 100644
index 00000000..2318d0f8
--- /dev/null
+++ b/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; }
+}
diff --git a/StabilityMatrix.Avalonia/Models/SharedState.cs b/StabilityMatrix.Avalonia/Models/SharedState.cs
index d2dd8fe1..17fd3288 100644
--- a/StabilityMatrix.Avalonia/Models/SharedState.cs
+++ b/StabilityMatrix.Avalonia/Models/SharedState.cs
@@ -1,14 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Models;
///
/// Singleton DI service for observable shared UI state.
///
+[Singleton]
public partial class SharedState : ObservableObject
{
///
/// Whether debug mode enabled from settings page version tap.
///
- [ObservableProperty] private bool isDebugMode;
+ [ObservableProperty]
+ private bool isDebugMode;
}
diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs
index 1b4395a4..5b03207d 100644
--- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs
+++ b/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();
diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TextCompletionRequest.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TextCompletionRequest.cs
index c93df5fa..fea2490c 100644
--- a/StabilityMatrix.Avalonia/Models/TagCompletion/TextCompletionRequest.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs
index 74b22d44..2181e5df 100644
--- a/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs
+++ b/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));
diff --git a/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs b/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs
index 3caf0b86..50416643 100644
--- a/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs
+++ b/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs
@@ -3,5 +3,6 @@
public enum TextEditorPreset
{
None,
- Prompt
+ Prompt,
+ Console
}
diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs
index 40c95d08..8f1b4e9e 100644
--- a/StabilityMatrix.Avalonia/Program.cs
+++ b/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;
@@ -82,13 +83,21 @@ public class Program
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
- // Avalonia configuration, don't remove; also used by visual designer.
- public static AppBuilder BuildAvaloniaApp()
+ ///
+ /// Called in and UI tests to setup static configurations
+ ///
+ internal static void SetupAvaloniaApp()
{
IconProvider.Current.Register();
// Use our custom image loader for custom local load error handling
ImageLoader.AsyncImageLoader.Dispose();
ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader();
+ }
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ {
+ SetupAvaloniaApp();
var app = AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace();
@@ -232,10 +241,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 +305,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));
diff --git a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
index bc6afaff..f40990fa 100644
--- a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Services/INotificationService.cs b/StabilityMatrix.Avalonia/Services/INotificationService.cs
index 77a4d9d2..d3cc7e77 100644
--- a/StabilityMatrix.Avalonia/Services/INotificationService.cs
+++ b/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 task,
string title = "Error",
string? message = null,
- NotificationType appearance = NotificationType.Error);
+ NotificationType appearance = NotificationType.Error
+ );
///
/// 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
+ );
///
/// Show a notification with the given parameters.
///
void Show(
- string title,
+ string title,
string message,
NotificationType appearance = NotificationType.Information,
- TimeSpan? expiration = null);
+ TimeSpan? expiration = null
+ );
///
/// 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
+ );
+
+ ///
+ /// Show a notification for a that will not auto-dismiss.
+ ///
+ void ShowPersistent(
+ AppException exception,
+ NotificationType appearance = NotificationType.Error,
+ LogLevel logLevel = LogLevel.Warning
+ );
}
diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
index 3e71a877..8254e467 100644
--- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
+++ b/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
///
+[Singleton(typeof(IInferenceClientManager))]
public partial class InferenceClientManager : ObservableObject, IInferenceClientManager
{
private readonly ILogger 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
+ );
+ }
+ }
+ }
+
///
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();
diff --git a/StabilityMatrix.Avalonia/Services/NavigationService.cs b/StabilityMatrix.Avalonia/Services/NavigationService.cs
index 4cc47881..b9fb1952 100644
--- a/StabilityMatrix.Avalonia/Services/NavigationService.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/Services/NotificationService.cs b/StabilityMatrix.Avalonia/Services/NotificationService.cs
index 1bb664b9..98611f8d 100644
--- a/StabilityMatrix.Avalonia/Services/NotificationService.cs
+++ b/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 logger;
private WindowNotificationManager? notificationManager;
-
+
+ public NotificationService(ILogger 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));
}
-
+
+ ///
+ 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));
+ }
+
///
public async Task> TryAsync(
Task 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.FromException(e);
}
}
-
+
///
public async Task> TryAsync(
Task task,
string title = "Error",
string? message = null,
- NotificationType appearance = NotificationType.Error)
+ NotificationType appearance = NotificationType.Error
+ )
{
try
{
diff --git a/StabilityMatrix.Avalonia/Services/ServiceManager.cs b/StabilityMatrix.Avalonia/Services/ServiceManager.cs
index 26d604f4..4b21c294 100644
--- a/StabilityMatrix.Avalonia/Services/ServiceManager.cs
+++ b/StabilityMatrix.Avalonia/Services/ServiceManager.cs
@@ -256,4 +256,19 @@ public class ServiceManager
return new BetterContentDialog { Content = view };
}
+
+ public void Register(Type type, Func 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;
+ }
+ }
}
diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
index 648330dd..a0e6c8cc 100644
--- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
+++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
@@ -8,7 +8,7 @@
app.manifest
true
./Assets/Icon.ico
- 2.5.5-dev.1
+ 2.6.0-dev.1
$(Version)
true
true
@@ -16,6 +16,7 @@
+
@@ -23,46 +24,48 @@
-
-
-
+
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
+
-
+
-
-
+
+
+
-
+
diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs
index 5df260c2..c59b9d53 100644
--- a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs
+++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs
@@ -1,5 +1,4 @@
-using Avalonia;
-using Avalonia.Media;
+using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Styles;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
index b905bba6..a4ed0309 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
+++ b/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? PrimaryButtonClick;
public event EventHandler? SecondaryButtonClick;
public event EventHandler? CloseButtonClick;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index 3bd7e614..f1d0d11c 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/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 vmFactory;
@@ -60,11 +65,13 @@ public abstract partial class InferenceGenerationViewModelBase
protected InferenceGenerationViewModelBase(
ServiceManager 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);
}
+ ///
+ /// Write an image to the default output folder
+ ///
+ protected Task 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
+ );
+ }
+
+ ///
+ /// Write an image to an output folder
+ ///
+ protected async Task 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;
+ }
+
///
/// Builds the image generation prompt
///
@@ -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();
var outputImages = new List();
- 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);
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
index b31b5501..bc1d60ff 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
+++ b/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)
);
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
index e4e458d8..0d06e87f 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
index a4785ae6..43288d83 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
+++ b/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("
", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
- .Replace("", $"{Environment.NewLine}{Environment.NewLine}");
+ var prunedDescription =
+ model.Description
+ ?.Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}")
+ .Replace("", $"{Environment.NewLine}{Environment.NewLine}") ?? string.Empty;
prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
return prunedDescription;
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
index 2ecfc90e..e7659258 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
+++ b/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();
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
index f1260f93..a978d874 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
+++ b/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;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
index 398aaf64..ecff1c02 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
+++ b/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 CheckpointFiles { get; } =
new ObservableCollectionExtended();
- public IObservableCollection DisplayedCheckpointFiles { get; } =
+ public IObservableCollection DisplayedCheckpointFiles { get; set; } =
new ObservableCollectionExtended();
public CheckpointFolder(
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
index c77b92d2..86b7fdd3 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
+++ b/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();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs
index aaf2aaa0..47625eaf 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs
+++ b/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;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
index 55fa73b5..cab1a11a 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
+++ b/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]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs
index 5c0b9427..42e8a6d6 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs
+++ b/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 ?? "";
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs
index 821d8f5b..c07824e7 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs
+++ b/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]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
index ac0d1dc1..351f9a64 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
@@ -6,7 +6,6 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
-using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@@ -23,6 +22,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(InferenceConnectionHelpDialog))]
+[ManagedService]
+[Transient]
public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
index 33f92b9d..55b40f30 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
@@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
@@ -31,11 +32,14 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
+[ManagedService]
+[Transient]
public partial class InstallerViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
+ private readonly IPackageFactory packageFactory;
private readonly IPyRunner pyRunner;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
@@ -47,15 +51,15 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[ObservableProperty]
private PackageVersion? selectedVersion;
- [ObservableProperty]
- private IReadOnlyList? availablePackages;
-
[ObservableProperty]
private ObservableCollection? availableCommits;
[ObservableProperty]
private ObservableCollection? availableVersions;
+ [ObservableProperty]
+ private ObservableCollection availablePackages;
+
[ObservableProperty]
private GitCommit? selectedCommit;
@@ -69,6 +73,9 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[NotifyPropertyChangedFor(nameof(CanInstall))]
private bool showDuplicateWarning;
+ [ObservableProperty]
+ private bool showIncompatiblePackages;
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanInstall))]
private string? installName;
@@ -115,7 +122,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
public bool CanInstall =>
!string.IsNullOrWhiteSpace(InstallName) && !ShowDuplicateWarning && !IsLoading;
- public ProgressViewModel InstallProgress { get; } = new();
public IEnumerable Steps { get; set; }
public InstallerViewModel(
@@ -128,22 +134,25 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
)
{
this.settingsManager = settingsManager;
+ this.packageFactory = packageFactory;
this.pyRunner = pyRunner;
this.downloadService = downloadService;
this.notificationService = notificationService;
this.prerequisiteHelper = prerequisiteHelper;
- // AvailablePackages and SelectedPackage
+ var filtered = packageFactory.GetAllAvailablePackages().Where(p => p.IsCompatible).ToList();
+
AvailablePackages = new ObservableCollection(
- packageFactory.GetAllAvailablePackages()
+ filtered.Any() ? filtered : packageFactory.GetAllAvailablePackages()
);
- SelectedPackage = AvailablePackages[0];
+ ShowIncompatiblePackages = !filtered.Any();
}
public override void OnLoaded()
{
if (AvailablePackages == null)
return;
+
IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
}
@@ -238,13 +247,22 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
downloadOptions.VersionTag =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
+ downloadOptions.IsLatest =
+ AvailableVersions?.First().TagName == downloadOptions.VersionTag;
+ downloadOptions.IsPrerelease = SelectedVersion.IsPrerelease;
installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag;
+ installedVersion.IsPrerelease = SelectedVersion.IsPrerelease;
}
else
{
downloadOptions.CommitHash =
SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null");
+ downloadOptions.BranchName =
+ SelectedVersion?.TagName
+ ?? throw new NullReferenceException("Selected version is null");
+ downloadOptions.IsLatest = AvailableCommits?.First().Sha == SelectedCommit.Sha;
+
installedVersion.InstalledBranch =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
@@ -259,6 +277,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
var installStep = new InstallPackageStep(
SelectedPackage,
SelectedTorchVersion,
+ SelectedSharedFolderMethod,
+ downloadOptions,
installLocation
);
var setupModelFoldersStep = new SetupModelFoldersStep(
@@ -301,6 +321,19 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
OnCloseButtonClick();
}
+ partial void OnShowIncompatiblePackagesChanged(bool value)
+ {
+ var filtered = packageFactory
+ .GetAllAvailablePackages()
+ .Where(p => ShowIncompatiblePackages || p.IsCompatible)
+ .ToList();
+
+ AvailablePackages = new ObservableCollection(
+ filtered.Any() ? filtered : packageFactory.GetAllAvailablePackages()
+ );
+ SelectedPackage = AvailablePackages[0];
+ }
+
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
@@ -309,8 +342,10 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
}
else
{
- // First try to find master
- var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master");
+ // First try to find the package-defined main branch
+ var version = AvailableVersions.FirstOrDefault(
+ x => x.TagName == SelectedPackage.MainBranch
+ );
// If not found, try main
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main");
@@ -358,41 +393,33 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
- partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
- OnSelectedPackageChanged(SelectedPackage);
-
- partial void OnSelectedPackageChanged(BasePackage value)
+ partial void OnSelectedVersionTypeChanged(PackageVersionType value)
{
- IsLoading = true;
- ReleaseNotes = string.Empty;
- AvailableVersions?.Clear();
- AvailableCommits?.Clear();
-
- AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases
- ? PackageVersionType.Commit
- : PackageVersionType.GithubRelease | PackageVersionType.Commit;
- SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
- SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
- if (Design.IsDesignMode)
+ if (SelectedPackage is null || Design.IsDesignMode)
return;
Dispatcher.UIThread
.InvokeAsync(async () =>
{
Logger.Debug($"Release mode: {IsReleaseMode}");
- var versionOptions = await value.GetAllVersionOptions();
+ var versionOptions = await SelectedPackage.GetAllVersionOptions();
AvailableVersions = IsReleaseMode
? new ObservableCollection(versionOptions.AvailableVersions)
: new ObservableCollection(versionOptions.AvailableBranches);
- SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
+ SelectedVersion = AvailableVersions?.FirstOrDefault(x => !x.IsPrerelease);
+ if (SelectedVersion is null)
+ return;
+
ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown;
Logger.Debug($"Loaded release notes for {ReleaseNotes}");
if (!IsReleaseMode)
{
- var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
+ var commits = (
+ await SelectedPackage.GetAllCommits(SelectedVersion.TagName)
+ )?.ToList();
if (commits is null || commits.Count == 0)
return;
@@ -408,6 +435,29 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
.SafeFireAndForget();
}
+ partial void OnSelectedPackageChanged(BasePackage? value)
+ {
+ IsLoading = true;
+ ReleaseNotes = string.Empty;
+ AvailableVersions?.Clear();
+ AvailableCommits?.Clear();
+
+ if (value == null)
+ return;
+
+ AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases
+ ? PackageVersionType.Commit
+ : PackageVersionType.GithubRelease | PackageVersionType.Commit;
+ IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
+ SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
+ SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
+ SelectedVersionType = SelectedPackage.ShouldIgnoreReleases
+ ? PackageVersionType.Commit
+ : PackageVersionType.GithubRelease;
+
+ OnSelectedVersionTypeChanged(SelectedVersionType);
+ }
+
partial void OnInstallNameChanged(string? value)
{
ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(
@@ -418,7 +468,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
partial void OnSelectedVersionChanged(PackageVersion? value)
{
ReleaseNotes = value?.ReleaseNotesMarkdown ?? string.Empty;
- if (value == null)
+ if (value == null || Design.IsDesignMode)
return;
SelectedCommit = null;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
index a4d2e4a6..303d4462 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
@@ -17,6 +17,8 @@ using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(LaunchOptionsDialog))]
+[ManagedService]
+[Transient]
public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
{
private readonly ILogger logger;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
index b4d4a823..a7d1d26d 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@@ -7,25 +8,29 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Languages;
+using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
-using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
-public partial class OneClickInstallViewModel : ViewModelBase
+[ManagedService]
+[Transient]
+public partial class OneClickInstallViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly ILogger logger;
private readonly IPyRunner pyRunner;
- private readonly ISharedFolders sharedFolders;
+ private readonly INavigationService navigationService;
private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty]
@@ -43,6 +48,9 @@ public partial class OneClickInstallViewModel : ViewModelBase
[ObservableProperty]
private bool isIndeterminate;
+ [ObservableProperty]
+ private bool showIncompatiblePackages;
+
[ObservableProperty]
private ObservableCollection allPackages;
@@ -53,6 +61,8 @@ public partial class OneClickInstallViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsProgressBarVisible))]
private int oneClickInstallProgress;
+ private bool isInferenceInstall;
+
public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate;
public OneClickInstallViewModel(
@@ -61,7 +71,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
IPrerequisiteHelper prerequisiteHelper,
ILogger logger,
IPyRunner pyRunner,
- ISharedFolders sharedFolders
+ INavigationService navigationService
)
{
this.settingsManager = settingsManager;
@@ -69,13 +79,21 @@ public partial class OneClickInstallViewModel : ViewModelBase
this.prerequisiteHelper = prerequisiteHelper;
this.logger = logger;
this.pyRunner = pyRunner;
- this.sharedFolders = sharedFolders;
+ this.navigationService = navigationService;
HeaderText = Resources.Text_WelcomeToStabilityMatrix;
SubHeaderText = Resources.Text_OneClickInstaller_SubHeader;
ShowInstallButton = true;
+
+ var filteredPackages = this.packageFactory
+ .GetAllAvailablePackages()
+ .Where(p => p is { OfferInOneClickInstaller: true, IsCompatible: true })
+ .ToList();
+
AllPackages = new ObservableCollection(
- this.packageFactory.GetAllAvailablePackages().Where(p => p.OfferInOneClickInstaller)
+ filteredPackages.Any()
+ ? filteredPackages
+ : this.packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AllPackages[0];
}
@@ -95,40 +113,39 @@ public partial class OneClickInstallViewModel : ViewModelBase
return Task.CompletedTask;
}
- private async Task DoInstall()
+ [RelayCommand]
+ private async Task InstallComfyForInference()
{
- HeaderText = $"{Resources.Label_Installing} {SelectedPackage.DisplayName}";
-
- var progressHandler = new Progress(progress =>
- {
- SubHeaderText = $"{progress.Title} {progress.Percentage:N0}%";
-
- IsIndeterminate = progress.IsIndeterminate;
- OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
- });
-
- await prerequisiteHelper.InstallAllIfNecessary(progressHandler);
-
- SubHeaderText = Resources.Progress_InstallingPrerequisites;
- IsIndeterminate = true;
- if (!PyRunner.PipInstalled)
+ var comfyPackage = AllPackages.FirstOrDefault(x => x is ComfyUI);
+ if (comfyPackage != null)
{
- await pyRunner.SetupPip();
+ SelectedPackage = comfyPackage;
+ isInferenceInstall = true;
+ await InstallCommand.ExecuteAsync(null);
}
+ }
- if (!PyRunner.VenvInstalled)
+ private async Task DoInstall()
+ {
+ var steps = new List
{
- await pyRunner.InstallPackage("virtualenv");
- }
- IsIndeterminate = false;
-
- var libraryDir = settingsManager.LibraryDir;
+ new SetPackageInstallingStep(settingsManager, SelectedPackage.Name),
+ new SetupPrerequisitesStep(prerequisiteHelper, pyRunner)
+ };
// get latest version & download & install
- var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
+ var installLocation = Path.Combine(
+ settingsManager.LibraryDir,
+ "Packages",
+ SelectedPackage.Name
+ );
- var downloadVersion = new DownloadPackageVersionOptions();
- var installedVersion = new InstalledPackageVersion();
+ var downloadVersion = new DownloadPackageVersionOptions
+ {
+ IsLatest = true,
+ IsPrerelease = false
+ };
+ var installedVersion = new InstalledPackageVersion { IsPrerelease = false };
var versionOptions = await SelectedPackage.GetAllVersionOptions();
if (versionOptions.AvailableVersions != null && versionOptions.AvailableVersions.Any())
@@ -138,17 +155,42 @@ public partial class OneClickInstallViewModel : ViewModelBase
}
else
{
- downloadVersion.BranchName = await SelectedPackage.GetLatestVersion();
+ var latestVersion = await SelectedPackage.GetLatestVersion();
+ downloadVersion.BranchName = latestVersion.BranchName;
+ downloadVersion.CommitHash =
+ (await SelectedPackage.GetAllCommits(downloadVersion.BranchName))
+ ?.FirstOrDefault()
+ ?.Sha ?? string.Empty;
+
installedVersion.InstalledBranch = downloadVersion.BranchName;
+ installedVersion.InstalledCommitSha = downloadVersion.CommitHash;
}
var torchVersion = SelectedPackage.GetRecommendedTorchVersion();
+ var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
- await DownloadPackage(installLocation, downloadVersion);
- await InstallPackage(installLocation, torchVersion);
+ var downloadStep = new DownloadPackageVersionStep(
+ SelectedPackage,
+ installLocation,
+ downloadVersion
+ );
+ steps.Add(downloadStep);
- var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
- await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
+ var installStep = new InstallPackageStep(
+ SelectedPackage,
+ torchVersion,
+ recommendedSharedFolderMethod,
+ downloadVersion,
+ installLocation
+ );
+ steps.Add(installStep);
+
+ var setupModelFoldersStep = new SetupModelFoldersStep(
+ SelectedPackage,
+ recommendedSharedFolderMethod,
+ installLocation
+ );
+ steps.Add(setupModelFoldersStep);
var installedPackage = new InstalledPackage
{
@@ -162,59 +204,47 @@ public partial class OneClickInstallViewModel : ViewModelBase
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = recommendedSharedFolderMethod
};
- await using var st = settingsManager.BeginTransaction();
- st.Settings.InstalledPackages.Add(installedPackage);
- st.Settings.ActiveInstalledPackageId = installedPackage.Id;
- EventManager.Instance.OnInstalledPackagesChanged();
- HeaderText = Resources.Progress_InstallationComplete;
- SubSubHeaderText = string.Empty;
- OneClickInstallProgress = 100;
+ var addInstalledPackageStep = new AddInstalledPackageStep(
+ settingsManager,
+ installedPackage
+ );
+ steps.Add(addInstalledPackageStep);
+
+ var runner = new PackageModificationRunner
+ {
+ ShowDialogOnStart = true,
+ HideCloseButton = true,
+ };
+ EventManager.Instance.OnPackageInstallProgressAdded(runner);
+ await runner.ExecuteSteps(steps);
- for (var i = 0; i < 3; i++)
+ EventManager.Instance.OnInstalledPackagesChanged();
+ HeaderText = $"{SelectedPackage.DisplayName} installed successfully";
+ for (var i = 3; i > 0; i--)
{
- SubHeaderText = $"{Resources.Text_ProceedingToLaunchPage} ({i + 1}s)";
+ SubHeaderText = $"{Resources.Text_ProceedingToLaunchPage} ({i}s)";
await Task.Delay(1000);
}
// should close dialog
EventManager.Instance.OnOneClickInstallFinished(false);
- }
-
- private async Task DownloadPackage(
- string installLocation,
- DownloadPackageVersionOptions versionOptions
- )
- {
- SubHeaderText = Resources.Progress_DownloadingPackage;
-
- var progress = new Progress(progress =>
+ if (isInferenceInstall)
{
- IsIndeterminate = progress.IsIndeterminate;
- OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
- EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
- });
-
- await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress);
- SubHeaderText = Resources.Progress_DownloadComplete;
- OneClickInstallProgress = 100;
+ navigationService.NavigateTo();
+ }
}
- private async Task InstallPackage(string installLocation, TorchVersion torchVersion)
+ partial void OnShowIncompatiblePackagesChanged(bool value)
{
- var progress = new Progress(progress =>
- {
- SubHeaderText = Resources.Progress_InstallingPackageRequirements;
- IsIndeterminate = progress.IsIndeterminate;
- OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
- EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
- });
+ var filteredPackages = packageFactory
+ .GetAllAvailablePackages()
+ .Where(p => p.OfferInOneClickInstaller && (ShowIncompatiblePackages || p.IsCompatible))
+ .ToList();
- await SelectedPackage.InstallPackage(
- installLocation,
- torchVersion,
- progress,
- (output) => SubSubHeaderText = output.Text
+ AllPackages = new ObservableCollection(
+ filteredPackages.Any() ? filteredPackages : packageFactory.GetAllAvailablePackages()
);
+ SelectedPackage = AllPackages[0];
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
index 915c2a2f..06ff94de 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
@@ -23,6 +23,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(PackageImportDialog))]
+[ManagedService]
+[Transient]
public partial class PackageImportViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs
new file mode 100644
index 00000000..7259df54
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs
@@ -0,0 +1,129 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Semver;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Models.FileInterfaces;
+using StabilityMatrix.Core.Python;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
+
+public partial class PythonPackagesItemViewModel : ViewModelBase
+{
+ [ObservableProperty]
+ private PipPackageInfo package;
+
+ [ObservableProperty]
+ private string? selectedVersion;
+
+ [ObservableProperty]
+ private IReadOnlyList? availableVersions;
+
+ [ObservableProperty]
+ private PipShowResult? pipShowResult;
+
+ [ObservableProperty]
+ private bool isLoading;
+
+ ///
+ /// True if selected version is newer than the installed version
+ ///
+ [ObservableProperty]
+ private bool canUpgrade;
+
+ ///
+ /// True if selected version is older than the installed version
+ ///
+ [ObservableProperty]
+ private bool canDowngrade;
+
+ partial void OnSelectedVersionChanged(string? value)
+ {
+ if (
+ value is null
+ || Package.Version == value
+ || !SemVersion.TryParse(Package.Version, out var currentSemver)
+ || !SemVersion.TryParse(value, out var selectedSemver)
+ )
+ {
+ CanUpgrade = false;
+ CanDowngrade = false;
+ return;
+ }
+
+ var precedence = selectedSemver.ComparePrecedenceTo(currentSemver);
+
+ CanUpgrade = precedence > 0;
+ CanDowngrade = precedence < 0;
+ }
+
+ ///
+ /// Return the known index URL for a package, currently this is torch, torchvision and torchaudio
+ ///
+ public static string? GetKnownIndexUrl(string packageName, string version)
+ {
+ var torchPackages = new[] { "torch", "torchvision", "torchaudio" };
+ if (torchPackages.Contains(packageName) && version.Contains('+'))
+ {
+ // Get the metadata for the current version (everything after the +)
+ var indexName = version.Split('+', 2).Last();
+
+ var indexUrl = $"https://download.pytorch.org/whl/{indexName}";
+ return indexUrl;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Loads the pip show result if not already loaded
+ ///
+ public async Task LoadExtraInfo(DirectoryPath venvPath)
+ {
+ if (PipShowResult is not null)
+ {
+ return;
+ }
+
+ IsLoading = true;
+
+ try
+ {
+ if (Design.IsDesignMode)
+ {
+ await LoadExtraInfoDesignMode();
+ }
+ else
+ {
+ await using var venvRunner = new PyVenvRunner(venvPath);
+
+ PipShowResult = await venvRunner.PipShow(Package.Name);
+
+ // Attempt to get known index url
+ var indexUrl = GetKnownIndexUrl(Package.Name, Package.Version);
+
+ if (await venvRunner.PipIndex(Package.Name, indexUrl) is { } pipIndexResult)
+ {
+ AvailableVersions = pipIndexResult.AvailableVersions;
+ SelectedVersion = Package.Version;
+ }
+ }
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private async Task LoadExtraInfoDesignMode()
+ {
+ await using var _ = new MinimumDelay(200, 300);
+
+ PipShowResult = new PipShowResult { Name = Package.Name, Version = Package.Version };
+ AvailableVersions = new[] { Package.Version, "1.2.0", "1.1.0", "1.0.0" };
+ SelectedVersion = Package.Version;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs
new file mode 100644
index 00000000..b7d00f8e
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs
@@ -0,0 +1,313 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncAwaitBestPractices;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using DynamicData;
+using DynamicData.Binding;
+using FluentAvalonia.UI.Controls;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Languages;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.Views.Dialogs;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Extensions;
+using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.FileInterfaces;
+using StabilityMatrix.Core.Models.PackageModification;
+using StabilityMatrix.Core.Processes;
+using StabilityMatrix.Core.Python;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
+
+[View(typeof(PythonPackagesDialog))]
+[ManagedService]
+[Transient]
+public partial class PythonPackagesViewModel : ContentDialogViewModelBase
+{
+ public DirectoryPath? VenvPath { get; set; }
+
+ [ObservableProperty]
+ private bool isLoading;
+
+ private readonly SourceCache packageSource = new(p => p.Name);
+
+ public IObservableCollection Packages { get; } =
+ new ObservableCollectionExtended();
+
+ [ObservableProperty]
+ private PythonPackagesItemViewModel? selectedPackage;
+
+ public PythonPackagesViewModel()
+ {
+ packageSource
+ .Connect()
+ .DeferUntilLoaded()
+ .Transform(p => new PythonPackagesItemViewModel { Package = p })
+ .SortBy(vm => vm.Package.Name)
+ .Bind(Packages)
+ .Subscribe();
+ }
+
+ private async Task Refresh()
+ {
+ if (VenvPath is null)
+ return;
+
+ IsLoading = true;
+
+ try
+ {
+ if (Design.IsDesignMode)
+ {
+ await Task.Delay(250);
+ }
+ else
+ {
+ await using var venvRunner = new PyVenvRunner(VenvPath);
+
+ var packages = await venvRunner.PipList();
+ packageSource.EditDiff(packages);
+ }
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ [RelayCommand]
+ private async Task RefreshBackground()
+ {
+ if (VenvPath is null)
+ return;
+
+ await using var venvRunner = new PyVenvRunner(VenvPath);
+
+ var packages = await venvRunner.PipList();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ // Backup selected package
+ var currentPackageName = SelectedPackage?.Package.Name;
+
+ packageSource.EditDiff(packages);
+
+ // Restore selected package
+ SelectedPackage = Packages.FirstOrDefault(p => p.Package.Name == currentPackageName);
+ });
+ }
+
+ ///
+ /// Load the selected package's show info if not already loaded
+ ///
+ partial void OnSelectedPackageChanged(PythonPackagesItemViewModel? value)
+ {
+ if (value is null)
+ {
+ return;
+ }
+
+ if (value.PipShowResult is null)
+ {
+ value.LoadExtraInfo(VenvPath!).SafeFireAndForget();
+ }
+ }
+
+ ///
+ public override Task OnLoadedAsync()
+ {
+ return Refresh();
+ }
+
+ public void AddPackages(params PipPackageInfo[] packages)
+ {
+ packageSource.AddOrUpdate(packages);
+ }
+
+ [RelayCommand]
+ private Task ModifySelectedPackage(PythonPackagesItemViewModel? item)
+ {
+ return item?.SelectedVersion != null
+ ? UpgradePackageVersion(
+ item.Package.Name,
+ item.SelectedVersion,
+ PythonPackagesItemViewModel.GetKnownIndexUrl(
+ item.Package.Name,
+ item.SelectedVersion
+ ),
+ isDowngrade: item.CanDowngrade
+ )
+ : Task.CompletedTask;
+ }
+
+ private async Task UpgradePackageVersion(
+ string packageName,
+ string version,
+ string? extraIndexUrl = null,
+ bool isDowngrade = false
+ )
+ {
+ if (VenvPath is null || SelectedPackage?.Package is not { } package)
+ return;
+
+ // Confirmation dialog
+ var dialog = DialogHelper.CreateMarkdownDialog(
+ isDowngrade
+ ? $"Downgrade **{package.Name}** to **{version}**?"
+ : $"Upgrade **{package.Name}** to **{version}**?",
+ Resources.Label_ConfirmQuestion
+ );
+
+ dialog.PrimaryButtonText = isDowngrade
+ ? Resources.Action_Downgrade
+ : Resources.Action_Upgrade;
+ dialog.IsPrimaryButtonEnabled = true;
+ dialog.DefaultButton = ContentDialogButton.Primary;
+ dialog.CloseButtonText = Resources.Action_Cancel;
+
+ if (await dialog.ShowAsync() is not ContentDialogResult.Primary)
+ {
+ return;
+ }
+
+ var args = new ProcessArgsBuilder("install", $"{packageName}=={version}");
+
+ if (extraIndexUrl != null)
+ {
+ args = args.AddArg(("--extra-index-url", extraIndexUrl));
+ }
+
+ var steps = new List
+ {
+ new PipStep
+ {
+ VenvDirectory = VenvPath,
+ WorkingDirectory = VenvPath.Parent,
+ Args = args
+ }
+ };
+
+ var runner = new PackageModificationRunner
+ {
+ ShowDialogOnStart = true,
+ ModificationCompleteMessage = isDowngrade
+ ? $"Downgraded Python Package '{packageName}' to {version}"
+ : $"Upgraded Python Package '{packageName}' to {version}"
+ };
+ EventManager.Instance.OnPackageInstallProgressAdded(runner);
+ await runner.ExecuteSteps(steps);
+
+ // Refresh
+ RefreshBackground().SafeFireAndForget();
+ }
+
+ [RelayCommand]
+ private async Task InstallPackage()
+ {
+ if (VenvPath is null)
+ return;
+
+ // Dialog
+ var fields = new TextBoxField[]
+ {
+ new() { Label = "Package Name", InnerLeftText = "pip install" }
+ };
+
+ var dialog = DialogHelper.CreateTextEntryDialog("Install Package", "", fields);
+ var result = await dialog.ShowAsync();
+
+ if (result != ContentDialogResult.Primary || fields[0].Text is not { } packageName)
+ {
+ return;
+ }
+
+ var steps = new List
+ {
+ new PipStep
+ {
+ VenvDirectory = VenvPath,
+ WorkingDirectory = VenvPath.Parent,
+ Args = new[] { "install", packageName }
+ }
+ };
+
+ var runner = new PackageModificationRunner
+ {
+ ShowDialogOnStart = true,
+ ModificationCompleteMessage = $"Installed Python Package '{packageName}'"
+ };
+ EventManager.Instance.OnPackageInstallProgressAdded(runner);
+ await runner.ExecuteSteps(steps);
+
+ // Refresh
+ RefreshBackground().SafeFireAndForget();
+ }
+
+ [RelayCommand]
+ private async Task UninstallSelectedPackage()
+ {
+ if (VenvPath is null || SelectedPackage?.Package is not { } package)
+ return;
+
+ // Confirmation dialog
+ var dialog = DialogHelper.CreateMarkdownDialog(
+ $"This will uninstall the package '{package.Name}'",
+ Resources.Label_ConfirmQuestion
+ );
+ dialog.PrimaryButtonText = Resources.Action_Uninstall;
+ dialog.IsPrimaryButtonEnabled = true;
+ dialog.DefaultButton = ContentDialogButton.Primary;
+ dialog.CloseButtonText = Resources.Action_Cancel;
+
+ if (await dialog.ShowAsync() is not ContentDialogResult.Primary)
+ {
+ return;
+ }
+
+ var steps = new List
+ {
+ new PipStep
+ {
+ VenvDirectory = VenvPath,
+ WorkingDirectory = VenvPath.Parent,
+ Args = new[] { "uninstall", "--yes", package.Name }
+ }
+ };
+
+ var runner = new PackageModificationRunner
+ {
+ ShowDialogOnStart = true,
+ ModificationCompleteMessage = $"Uninstalled Python Package '{package.Name}'"
+ };
+ EventManager.Instance.OnPackageInstallProgressAdded(runner);
+ await runner.ExecuteSteps(steps);
+
+ // Refresh
+ RefreshBackground().SafeFireAndForget();
+ }
+
+ public BetterContentDialog GetDialog()
+ {
+ return new BetterContentDialog
+ {
+ CloseOnClickOutside = true,
+ MinDialogWidth = 800,
+ MaxDialogWidth = 1500,
+ FullSizeDesired = true,
+ ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
+ Title = Resources.Label_PythonPackages,
+ Content = new PythonPackagesDialog { DataContext = this },
+ CloseButtonText = Resources.Action_Close,
+ DefaultButton = ContentDialogButton.Close
+ };
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
index 15fa1e48..9c20bdf0 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
@@ -19,6 +19,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(SelectDataDirectoryDialog))]
+[ManagedService]
+[Transient]
public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
index 77eac510..c3f10931 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
@@ -1,19 +1,19 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
-using System.Threading.Tasks;
using Avalonia.Media.Imaging;
-using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
+[ManagedService]
+[Transient]
public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
index 8710295a..95fe270b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
@@ -22,6 +22,8 @@ using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(UpdateDialog))]
+[ManagedService]
+[Singleton]
public partial class UpdateViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
@@ -122,24 +124,30 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
if (UpdateInfo is null)
return;
+ ReleaseNotes = await GetReleaseNotes(UpdateInfo.ChangelogUrl);
+ }
+
+ internal async Task GetReleaseNotes(string changelogUrl)
+ {
using var client = httpClientFactory.CreateClient();
- var response = await client.GetAsync(UpdateInfo.ChangelogUrl);
+ var response = await client.GetAsync(changelogUrl);
if (response.IsSuccessStatusCode)
{
var changelog = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
- if (UpdateInfo.ChangelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
+ if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
- ReleaseNotes =
- FormatChangelog(changelog, Compat.AppVersion)
+ return FormatChangelog(changelog, Compat.AppVersion)
?? "## Unable to format release notes";
}
+
+ return changelog;
}
else
{
- ReleaseNotes = "## Unable to load release notes";
+ return "## Unable to load release notes";
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
index 25a6faed..54463c21 100644
--- a/StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
@@ -11,6 +11,8 @@ using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(FirstLaunchSetupWindow))]
+[ManagedService]
+[Singleton]
public partial class FirstLaunchSetupViewModel : ViewModelBase
{
[ObservableProperty]
@@ -20,14 +22,17 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private string gpuInfoText = string.Empty;
[ObservableProperty]
- private RefreshBadgeViewModel checkHardwareBadge = new()
- {
- WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
- SuccessToolTipText = "Everything looks good!",
- FailToolTipText = "We recommend a GPU with CUDA support for the best experience. " +
- "You can continue without one, but some packages may not work, and inference may be slower.",
- FailColorBrush = ThemeColors.ThemeYellow,
- };
+ private RefreshBadgeViewModel checkHardwareBadge =
+ new()
+ {
+ WorkingToolTipText =
+ "We're checking some hardware specifications to determine compatibility.",
+ SuccessToolTipText = "Everything looks good!",
+ FailToolTipText =
+ "We recommend a GPU with CUDA support for the best experience. "
+ + "You can continue without one, but some packages may not work, and inference may be slower.",
+ FailColorBrush = ThemeColors.ThemeYellow,
+ };
public FirstLaunchSetupViewModel()
{
@@ -43,14 +48,16 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
gpuInfo = await Task.Run(() => HardwareHelper.IterGpuInfo().ToArray());
}
// First Nvidia GPU
- var activeGpu = gpuInfo.FirstOrDefault(gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false);
+ var activeGpu = gpuInfo.FirstOrDefault(
+ gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false
+ );
var isNvidia = activeGpu is not null;
// Otherwise first GPU
activeGpu ??= gpuInfo.FirstOrDefault();
GpuInfoText = activeGpu is null
? "No GPU detected"
: $"{activeGpu.Name} ({Size.FormatBytes(activeGpu.MemoryBytes)})";
-
+
return isNvidia;
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs
index d18d22bd..12a1d0e9 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs
@@ -6,6 +6,8 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(BatchSizeCard))]
+[ManagedService]
+[Transient]
public partial class BatchSizeCardViewModel : LoadableViewModelBase
{
[ObservableProperty]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs
index 059975d3..91b09866 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs
@@ -7,6 +7,8 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(FreeUCard))]
+[ManagedService]
+[Transient]
public partial class FreeUCardViewModel : LoadableViewModelBase
{
[ObservableProperty]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs
index eab2a345..65f9fa0a 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs
@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
+using System.Linq;
using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
index e8097834..7502f496 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
@@ -4,7 +4,6 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
-using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
@@ -26,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
+using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using SortDirection = DynamicData.Binding.SortDirection;
@@ -33,6 +33,8 @@ using SortDirection = DynamicData.Binding.SortDirection;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ImageFolderCard))]
+[ManagedService]
+[Transient]
public partial class ImageFolderCardViewModel : ViewModelBase
{
private readonly ILogger logger;
@@ -43,6 +45,9 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[ObservableProperty]
private string? searchQuery;
+ [ObservableProperty]
+ private Size imageSize = new(150, 190);
+
///
/// Collection of local image files
///
@@ -61,20 +66,28 @@ public partial class ImageFolderCardViewModel : ViewModelBase
this.settingsManager = settingsManager;
this.notificationService = notificationService;
- var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
+ var searcher = new ImageSearcher();
+
+ // Observable predicate from SearchQuery changes
+ var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))!
- .Select, Func>(
- p => file => SearchPredicate(file, p.Value)
- )
+ .Select(property => searcher.GetPredicate(property.Value))
.AsObservable();
imageIndexService.InferenceImages.ItemsSource
.Connect()
.DeferUntilLoaded()
- .Filter(predicate)
+ .Filter(searchPredicate)
.SortBy(file => file.LastModifiedAt, SortDirection.Descending)
.Bind(LocalImages)
.Subscribe();
+
+ settingsManager.RelayPropertyFor(
+ this,
+ vm => vm.ImageSize,
+ settings => settings.InferenceImageSize,
+ delay: TimeSpan.FromMilliseconds(250)
+ );
}
private static bool SearchPredicate(LocalImageFile file, string? query)
@@ -116,24 +129,49 @@ public partial class ImageFolderCardViewModel : ViewModelBase
public override async Task OnLoadedAsync()
{
await base.OnLoadedAsync();
-
+ ImageSize = settingsManager.Settings.InferenceImageSize;
imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget();
}
+ ///
+ /// Gets the image path if it exists, returns null.
+ /// If the image path is resolved but the file doesn't exist, it will be removed from the index.
+ ///
+ private FilePath? GetImagePathIfExists(LocalImageFile item)
+ {
+ var imageFile = new FilePath(item.AbsolutePath);
+
+ if (!imageFile.Exists)
+ {
+ // Remove from index
+ imageIndexService.InferenceImages.Remove(item);
+
+ // Invalidate cache
+ if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
+ {
+ loader.RemoveAllNamesFromCache(imageFile.Name);
+ }
+
+ return null;
+ }
+
+ return imageFile;
+ }
+
///
/// Handles image clicks to show preview
///
[RelayCommand]
private async Task OnImageClick(LocalImageFile item)
{
- if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
+ if (GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
var currentIndex = LocalImages.IndexOf(item);
- var image = new ImageSource(new FilePath(imagePath));
+ var image = new ImageSource(imageFile);
// Preload
await image.GetBitmapAsync();
@@ -156,14 +194,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
if (newIndex >= 0 && newIndex < LocalImages.Count)
{
var newImage = LocalImages[newIndex];
- var newImageSource = new ImageSource(
- new FilePath(newImage.GetFullPath(settingsManager.ImagesDirectory))
- );
+ var newImageSource = new ImageSource(newImage.AbsolutePath);
// Preload
await newImageSource.GetBitmapAsync();
- var oldImageSource = sender.ImageSource;
+ // var oldImageSource = sender.ImageSource;
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
@@ -185,13 +221,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageDelete(LocalImageFile? item)
{
- if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
+ if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
// Delete the file
- var imageFile = new FilePath(imagePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
@@ -215,14 +250,14 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageCopy(LocalImageFile? item)
{
- if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
+ if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
var clipboard = App.Clipboard;
- await clipboard.SetFileDataObjectAsync(imagePath);
+ await clipboard.SetFileDataObjectAsync(imageFile.FullPath);
}
///
@@ -231,12 +266,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageOpen(LocalImageFile? item)
{
- if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
+ if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
- await ProcessRunner.OpenFileBrowser(imagePath);
+ await ProcessRunner.OpenFileBrowser(imageFile);
}
///
@@ -248,13 +283,11 @@ public partial class ImageFolderCardViewModel : ViewModelBase
bool includeMetadata = false
)
{
- if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } sourcePath)
+ if (item is null || GetImagePathIfExists(item) is not { } sourceFile)
{
return;
}
- var sourceFile = new FilePath(sourcePath);
-
var formatName = format.ToString();
var storageFile = await App.StorageProvider.SaveFilePickerAsync(
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs
index a8f5920c..93497d43 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs
@@ -23,6 +23,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ImageGalleryCard))]
+[ManagedService]
+[Transient]
public partial class ImageGalleryCardViewModel : ViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
index 9e56a16d..d2a196e6 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
@@ -1,13 +1,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
-using System.Drawing;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
-using Avalonia.Controls.Shapes;
-using Avalonia.Threading;
using DynamicData.Binding;
using NLog;
using StabilityMatrix.Avalonia.Extensions;
@@ -19,6 +16,7 @@ using StabilityMatrix.Avalonia.Views.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+using StabilityMatrix.Core.Services;
using Path = System.IO.Path;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
@@ -27,6 +25,8 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceImageUpscaleView), persistent: true)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+[ManagedService]
+[Transient]
public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -60,9 +60,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
public InferenceImageUpscaleViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
+ ISettingsManager settingsManager,
ServiceManager vmFactory
)
- : base(vmFactory, inferenceClientManager, notificationService)
+ : base(vmFactory, inferenceClientManager, notificationService, settingsManager)
{
this.notificationService = notificationService;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
index 07124ac0..c06c438c 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
@@ -26,6 +26,8 @@ using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.Infere
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceTextToImageView), persistent: true)]
+[ManagedService]
+[Transient]
public class InferenceTextToImageViewModel
: InferenceGenerationViewModelBase,
IParametersLoadableState
@@ -86,10 +88,11 @@ public class InferenceTextToImageViewModel
public InferenceTextToImageViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
+ ISettingsManager settingsManager,
ServiceManager vmFactory,
IModelIndexService modelIndexService
)
- : base(vmFactory, inferenceClientManager, notificationService)
+ : base(vmFactory, inferenceClientManager, notificationService, settingsManager)
{
this.notificationService = notificationService;
this.modelIndexService = modelIndexService;
@@ -248,7 +251,7 @@ public class InferenceTextToImageViewModel
if (ModelCardViewModel is { IsVaeSelectionEnabled: true, SelectedVae.IsDefault: false })
{
var customVaeLoader = nodes.AddNamedNode(
- ComfyNodeBuilder.VAELoader("VAELoader", ModelCardViewModel.SelectedVae.FileName)
+ ComfyNodeBuilder.VAELoader("VAELoader", ModelCardViewModel.SelectedVae.RelativePath)
);
builder.Connections.BaseVAE = customVaeLoader.Output;
@@ -381,22 +384,7 @@ public class InferenceTextToImageViewModel
Client = ClientManager.Client,
Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
- Parameters = new GenerationParameters
- {
- Seed = (ulong)seed,
- Steps = SamplerCardViewModel.Steps,
- CfgScale = SamplerCardViewModel.CfgScale,
- Sampler = SamplerCardViewModel.SelectedSampler?.Name,
- ModelName = ModelCardViewModel.SelectedModelName,
- ModelHash = ModelCardViewModel
- .SelectedModel
- ?.Local
- ?.ConnectedModelInfo
- ?.Hashes
- .SHA256,
- PositivePrompt = PromptCardViewModel.PromptDocument.Text,
- NegativePrompt = PromptCardViewModel.NegativePromptDocument.Text
- },
+ Parameters = SaveStateToParameters(new GenerationParameters()),
Project = InferenceProjectDocument.FromLoadable(this),
// Only clear output images on the first batch
ClearOutputImages = i == 0
@@ -417,10 +405,9 @@ public class InferenceTextToImageViewModel
{
PromptCardViewModel.LoadStateFromParameters(parameters);
SamplerCardViewModel.LoadStateFromParameters(parameters);
+ ModelCardViewModel.LoadStateFromParameters(parameters);
SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed);
-
- ModelCardViewModel.LoadStateFromParameters(parameters);
}
///
@@ -428,11 +415,10 @@ public class InferenceTextToImageViewModel
{
parameters = PromptCardViewModel.SaveStateToParameters(parameters);
parameters = SamplerCardViewModel.SaveStateToParameters(parameters);
+ parameters = ModelCardViewModel.SaveStateToParameters(parameters);
parameters.Seed = (ulong)SeedCardViewModel.Seed;
- parameters = ModelCardViewModel.SaveStateToParameters(parameters);
-
return parameters;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index 2c28da8f..d69f5b80 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -12,6 +12,8 @@ using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ModelCard))]
+[ManagedService]
+[Transient]
public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
[ObservableProperty]
@@ -29,9 +31,9 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad
[ObservableProperty]
private bool isVaeSelectionEnabled;
- public string? SelectedModelName => SelectedModel?.FileName;
+ public string? SelectedModelName => SelectedModel?.RelativePath;
- public string? SelectedVaeName => SelectedVae?.FileName;
+ public string? SelectedVaeName => SelectedVae?.RelativePath;
public IInferenceClientManager ClientManager { get; }
@@ -60,11 +62,11 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad
SelectedModel = model.SelectedModelName is null
? null
- : ClientManager.Models.FirstOrDefault(x => x.FileName == model.SelectedModelName);
+ : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedModelName);
SelectedVae = model.SelectedVaeName is null
? HybridModelFile.Default
- : ClientManager.VaeModels.FirstOrDefault(x => x.FileName == model.SelectedVaeName);
+ : ClientManager.VaeModels.FirstOrDefault(x => x.RelativePath == model.SelectedVaeName);
}
internal class ModelCardModel
@@ -99,7 +101,7 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad
else
{
// Name matches
- model = currentModels.FirstOrDefault(m => m.FileName.EndsWith(paramsModelName));
+ model = currentModels.FirstOrDefault(m => m.RelativePath.EndsWith(paramsModelName));
model ??= currentModels.FirstOrDefault(
m => m.ShortDisplayName.StartsWith(paramsModelName)
);
@@ -114,6 +116,10 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad
///
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
- return parameters with { ModelName = SelectedModel?.FileName };
+ return parameters with
+ {
+ ModelName = SelectedModel?.FileName,
+ ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256
+ };
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
index a7b76ad0..26e8acf8 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
@@ -1,15 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
+using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using AvaloniaEdit;
using AvaloniaEdit.Document;
-using AvaloniaEdit.Editing;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Controls;
@@ -20,7 +14,6 @@ using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
-using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
@@ -28,6 +21,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(PromptCard))]
+[ManagedService]
+[Transient]
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
private readonly IModelIndexService modelIndexService;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
index 1d70fa42..8d52ebe9 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
@@ -13,6 +13,8 @@ using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SamplerCard))]
+[ManagedService]
+[Transient]
public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
[ObservableProperty]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SeedCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SeedCardViewModel.cs
index 6c52c4db..1f0a9fa4 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/SeedCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SeedCardViewModel.cs
@@ -10,6 +10,8 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SeedCard))]
+[ManagedService]
+[Transient]
public partial class SeedCardViewModel : LoadableViewModelBase
{
[ObservableProperty, NotifyPropertyChangedFor(nameof(RandomizeButtonToolTip))]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
index ff175ccd..7639bc14 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
-using System.Text.Json;
using System.Linq;
using Avalonia.Input;
using Avalonia.Media;
@@ -19,6 +18,8 @@ using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SelectImageCard))]
+[ManagedService]
+[Transient]
public partial class SelectImageCardViewModel : ViewModelBase, IDropTarget
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -88,7 +89,7 @@ public partial class SelectImageCardViewModel : ViewModelBase, IDropTarget
{
var current = ImageSource;
- ImageSource = new ImageSource(imageFile.GlobalFullPath);
+ ImageSource = new ImageSource(imageFile.AbsolutePath);
current?.Dispose();
});
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SharpenCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SharpenCardViewModel.cs
index 98a1f933..90a67a12 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/SharpenCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SharpenCardViewModel.cs
@@ -7,6 +7,8 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SharpenCard))]
+[ManagedService]
+[Transient]
public partial class SharpenCardViewModel : LoadableViewModelBase
{
[Range(1, 31)]
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/StackCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/StackCardViewModel.cs
index 763054b2..49e7b810 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/StackCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/StackCardViewModel.cs
@@ -8,20 +8,24 @@ using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(StackCard))]
+[ManagedService]
+[Transient]
public class StackCardViewModel : StackViewModelBase
{
///
public override void LoadStateFromJsonObject(JsonObject state)
{
var model = DeserializeModel(state);
-
- if (model.Cards is null) return;
-
+
+ if (model.Cards is null)
+ return;
+
foreach (var (i, card) in model.Cards.Enumerate())
{
// Ignore if more than cards than we have
- if (i > Cards.Count - 1) break;
-
+ if (i > Cards.Count - 1)
+ break;
+
Cards[i].LoadStateFromJsonObject(card);
}
}
@@ -29,9 +33,8 @@ public class StackCardViewModel : StackViewModelBase
///
public override JsonObject SaveStateToJsonObject()
{
- return SerializeModel(new StackCardModel
- {
- Cards = Cards.Select(x => x.SaveStateToJsonObject()).ToList()
- });
+ return SerializeModel(
+ new StackCardModel { Cards = Cards.Select(x => x.SaveStateToJsonObject()).ToList() }
+ );
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs
index 2b4e3c2e..9eef7ad6 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs
@@ -11,28 +11,32 @@ using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(StackExpander))]
+[ManagedService]
+[Transient]
public partial class StackExpanderViewModel : StackViewModelBase
{
[ObservableProperty]
[property: JsonIgnore]
private string? title;
-
- [ObservableProperty]
+
+ [ObservableProperty]
private bool isEnabled;
-
+
///
public override void LoadStateFromJsonObject(JsonObject state)
{
var model = DeserializeModel(state);
IsEnabled = model.IsEnabled;
-
- if (model.Cards is null) return;
-
+
+ if (model.Cards is null)
+ return;
+
foreach (var (i, card) in model.Cards.Enumerate())
{
// Ignore if more than cards than we have
- if (i > Cards.Count - 1) break;
-
+ if (i > Cards.Count - 1)
+ break;
+
Cards[i].LoadStateFromJsonObject(card);
}
}
@@ -40,10 +44,12 @@ public partial class StackExpanderViewModel : StackViewModelBase
///
public override JsonObject SaveStateToJsonObject()
{
- return SerializeModel(new StackExpanderModel
- {
- IsEnabled = IsEnabled,
- Cards = Cards.Select(x => x.SaveStateToJsonObject()).ToList()
- });
+ return SerializeModel(
+ new StackExpanderModel
+ {
+ IsEnabled = IsEnabled,
+ Cards = Cards.Select(x => x.SaveStateToJsonObject()).ToList()
+ }
+ );
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs
index ceef75c6..02ef8a83 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs
@@ -20,6 +20,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(UpscalerCard))]
+[ManagedService]
+[Transient]
public partial class UpscalerCardViewModel : LoadableViewModelBase
{
private readonly INotificationService notificationService;
diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
index 4b2ff874..33320aab 100644
--- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
@@ -9,7 +9,6 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
-using Avalonia.Controls.Shapes;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -23,7 +22,6 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.Views;
-using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
@@ -43,6 +41,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Preload]
[View(typeof(InferencePage))]
+[Singleton]
public partial class InferenceViewModel : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -111,6 +110,10 @@ public partial class InferenceViewModel : PageViewModelBase
// Keep RunningPackage updated with the current package pair
EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged;
+ // "Send to Inference"
+ EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
+ EventManager.Instance.InferenceUpscaleRequested += OnInferenceUpscaleRequested;
+
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
}
@@ -232,6 +235,16 @@ public partial class InferenceViewModel : PageViewModelBase
}
}
+ private void OnInferenceTextToImageRequested(object? sender, LocalImageFile e)
+ {
+ Dispatcher.UIThread.Post(() => AddTabFromImage(e).SafeFireAndForget());
+ }
+
+ private void OnInferenceUpscaleRequested(object? sender, LocalImageFile e)
+ {
+ Dispatcher.UIThread.Post(() => AddUpscalerTabFromImage(e).SafeFireAndForget());
+ }
+
///
/// Update the database with current tabs
///
@@ -593,6 +606,70 @@ public partial class InferenceViewModel : PageViewModelBase
await SyncTabStatesWithDatabase();
}
+ private async Task AddTabFromImage(LocalImageFile imageFile)
+ {
+ var metadata = imageFile.ReadMetadata();
+ InferenceTabViewModelBase? vm = null;
+
+ if (!string.IsNullOrWhiteSpace(metadata.SMProject))
+ {
+ var document = JsonSerializer.Deserialize(metadata.SMProject);
+ if (document is null)
+ {
+ throw new ApplicationException(
+ "MenuOpenProject: Deserialize project file returned null"
+ );
+ }
+
+ if (document.State is null)
+ {
+ throw new ApplicationException("Project file does not have 'State' key");
+ }
+
+ document.VerifyVersion();
+ var textToImage = vmFactory.Get();
+ textToImage.LoadStateFromJsonObject(document.State);
+ vm = textToImage;
+ }
+ else if (!string.IsNullOrWhiteSpace(metadata.Parameters))
+ {
+ if (GenerationParameters.TryParse(metadata.Parameters, out var generationParameters))
+ {
+ var textToImageViewModel = vmFactory.Get();
+ textToImageViewModel.LoadStateFromParameters(generationParameters);
+ vm = textToImageViewModel;
+ }
+ }
+
+ if (vm == null)
+ {
+ notificationService.Show(
+ "Unable to load project from image",
+ "No image metadata found",
+ NotificationType.Error
+ );
+ return;
+ }
+
+ Tabs.Add(vm);
+
+ SelectedTab = vm;
+
+ await SyncTabStatesWithDatabase();
+ }
+
+ private async Task AddUpscalerTabFromImage(LocalImageFile imageFile)
+ {
+ var upscaleVm = vmFactory.Get();
+ upscaleVm.IsUpscaleEnabled = true;
+ upscaleVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath);
+
+ Tabs.Add(upscaleVm);
+ SelectedTab = upscaleVm;
+
+ await SyncTabStatesWithDatabase();
+ }
+
///
/// Menu "Open Project" command.
///
diff --git a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
index 87c7ea3f..b9ed79ab 100644
--- a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
@@ -40,6 +40,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(LaunchPageView))]
+[Singleton]
public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyncDisposable
{
private readonly ILogger logger;
diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
index 1abe5f17..6e99b718 100644
--- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
@@ -102,7 +102,10 @@ public partial class MainWindowViewModel : ViewModelBase
// Index checkpoints if we dont have
Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget();
- PreloadPages();
+ if (!App.IsHeadlessMode)
+ {
+ PreloadPages();
+ }
Program.StartupTimer.Stop();
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
@@ -128,7 +131,7 @@ public partial class MainWindowViewModel : ViewModelBase
EventManager.Instance.OnTeachingTooltipNeeded();
};
- await dialog.ShowAsync();
+ await dialog.ShowAsync(App.TopLevel);
}
}
@@ -239,7 +242,7 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
- var result = await dialog.ShowAsync();
+ var result = await dialog.ShowAsync(App.TopLevel);
if (result == ContentDialogResult.Primary)
{
// 1. For portable mode, call settings.SetPortableMode()
diff --git a/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
index b165aef0..87ebb0f5 100644
--- a/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
@@ -2,14 +2,12 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
-using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
@@ -35,6 +33,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(NewCheckpointsPage))]
+[Singleton]
public partial class NewCheckpointsPageViewModel : PageViewModelBase
{
private readonly ILogger logger;
@@ -44,12 +43,17 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
private readonly ServiceManager dialogFactory;
private readonly INotificationService notificationService;
public override string Title => "Checkpoint Manager";
- public override IconSource IconSource => new SymbolIconSource
- {Symbol = Symbol.Cellular5g, IsFilled = true};
+ public override IconSource IconSource =>
+ new SymbolIconSource { Symbol = Symbol.Cellular5g, IsFilled = true };
- public NewCheckpointsPageViewModel(ILogger logger,
- ISettingsManager settingsManager, ILiteDbContext liteDbContext, ICivitApi civitApi,
- ServiceManager dialogFactory, INotificationService notificationService)
+ public NewCheckpointsPageViewModel(
+ ILogger logger,
+ ISettingsManager settingsManager,
+ ILiteDbContext liteDbContext,
+ ICivitApi civitApi,
+ ServiceManager dialogFactory,
+ INotificationService notificationService
+ )
{
this.logger = logger;
this.settingsManager = settingsManager;
@@ -63,23 +67,27 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
[NotifyPropertyChangedFor(nameof(ConnectedCheckpoints))]
[NotifyPropertyChangedFor(nameof(NonConnectedCheckpoints))]
private ObservableCollection allCheckpoints = new();
-
+
[ObservableProperty]
private ObservableCollection civitModels = new();
- public ObservableCollection ConnectedCheckpoints => new(
- AllCheckpoints.Where(x => x.IsConnectedModel)
- .OrderBy(x => x.ConnectedModel!.ModelName)
- .ThenBy(x => x.ModelType)
- .GroupBy(x => x.ConnectedModel!.ModelId)
- .Select(x => x.First()));
+ public ObservableCollection ConnectedCheckpoints =>
+ new(
+ AllCheckpoints
+ .Where(x => x.IsConnectedModel)
+ .OrderBy(x => x.ConnectedModel!.ModelName)
+ .ThenBy(x => x.ModelType)
+ .GroupBy(x => x.ConnectedModel!.ModelId)
+ .Select(x => x.First())
+ );
- public ObservableCollection NonConnectedCheckpoints => new(
- AllCheckpoints.Where(x => !x.IsConnectedModel).OrderBy(x => x.ModelType));
+ public ObservableCollection NonConnectedCheckpoints =>
+ new(AllCheckpoints.Where(x => !x.IsConnectedModel).OrderBy(x => x.ModelType));
public override async Task OnLoadedAsync()
{
- if (Design.IsDesignMode) return;
+ if (Design.IsDesignMode)
+ return;
var files = CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory);
AllCheckpoints = new ObservableCollection(files);
@@ -89,17 +97,17 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
{
CommaSeparatedModelIds = string.Join(',', connectedModelIds)
};
-
+
// See if query is cached
var cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
-
+
// If cached, update model cards
if (cachedQuery is not null)
{
CivitModels = new ObservableCollection(cachedQuery.Items);
-
+
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
@@ -113,24 +121,34 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
await CivitQuery(modelRequest);
}
}
-
+
public async Task ShowVersionDialog(int modelId)
{
var model = CivitModels.FirstOrDefault(m => m.Id == modelId);
if (model == null)
{
- notificationService.Show(new Notification("Model has no versions available",
- "This model has no versions available for download", NotificationType.Warning));
+ notificationService.Show(
+ new Notification(
+ "Model has no versions available",
+ "This model has no versions available for download",
+ NotificationType.Warning
+ )
+ );
return;
}
var versions = model.ModelVersions;
if (versions is null || versions.Count == 0)
{
- notificationService.Show(new Notification("Model has no versions available",
- "This model has no versions available for download", NotificationType.Warning));
+ notificationService.Show(
+ new Notification(
+ "Model has no versions available",
+ "This model has no versions available for download",
+ NotificationType.Warning
+ )
+ );
return;
}
-
+
var dialog = new BetterContentDialog
{
Title = model.Name,
@@ -139,19 +157,21 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
IsFooterVisible = false,
MaxDialogWidth = 750,
};
-
+
var viewModel = dialogFactory.Get();
viewModel.Dialog = dialog;
- viewModel.Versions = versions.Select(version =>
- new ModelVersionViewModel(
- settingsManager.Settings.InstalledModelHashes ?? new HashSet(), version))
+ viewModel.Versions = versions
+ .Select(
+ version =>
+ new ModelVersionViewModel(
+ settingsManager.Settings.InstalledModelHashes ?? new HashSet(),
+ version
+ )
+ )
.ToImmutableArray();
viewModel.SelectedVersionViewModel = viewModel.Versions[0];
-
- dialog.Content = new SelectModelVersionDialog
- {
- DataContext = viewModel
- };
+
+ dialog.Content = new SelectModelVersionDialog { DataContext = viewModel };
var result = await dialog.ShowAsync();
@@ -171,8 +191,10 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
var modelResponse = await civitApi.GetModels(request);
var models = modelResponse.Items;
// Filter out unknown model types and archived/taken-down models
- models = models.Where(m => m.Type.ConvertTo() > 0)
- .Where(m => m.Mode == null).ToList();
+ models = models
+ .Where(m => m.Type.ConvertTo() > 0)
+ .Where(m => m.Mode == null)
+ .ToList();
// Database update calls will invoke `OnModelsUpdated`
// Add to database
@@ -186,7 +208,8 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
Request = request,
Items = models,
Metadata = modelResponse.Metadata
- });
+ }
+ );
if (cacheNew)
{
@@ -195,26 +218,42 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase
}
catch (OperationCanceledException)
{
- notificationService.Show(new Notification("Request to CivitAI timed out",
- "Could not check for checkpoint updates. Please try again later."));
+ notificationService.Show(
+ new Notification(
+ "Request to CivitAI timed out",
+ "Could not check for checkpoint updates. Please try again later."
+ )
+ );
logger.LogWarning($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
- notificationService.Show(new Notification("CivitAI can't be reached right now",
- "Could not check for checkpoint updates. Please try again later."));
+ notificationService.Show(
+ new Notification(
+ "CivitAI can't be reached right now",
+ "Could not check for checkpoint updates. Please try again later."
+ )
+ );
logger.LogWarning(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
- notificationService.Show(new Notification("CivitAI can't be reached right now",
- "Could not check for checkpoint updates. Please try again later."));
+ notificationService.Show(
+ new Notification(
+ "CivitAI can't be reached right now",
+ "Could not check for checkpoint updates. Please try again later."
+ )
+ );
logger.LogWarning(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
- notificationService.Show(new Notification("CivitAI can't be reached right now",
- $"Unknown exception during CivitAI query: {e.GetType().Name}"));
+ notificationService.Show(
+ new Notification(
+ "CivitAI can't be reached right now",
+ $"Unknown exception during CivitAI query: {e.GetType().Name}"
+ )
+ );
logger.LogError(e, $"CivitAI query unknown exception ({request})");
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
new file mode 100644
index 00000000..30ce380e
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
@@ -0,0 +1,18 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Models.Database;
+
+namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage;
+
+public partial class OutputImageViewModel : ViewModelBase
+{
+ public LocalImageFile ImageFile { get; }
+
+ [ObservableProperty]
+ private bool isSelected;
+
+ public OutputImageViewModel(LocalImageFile imageFile)
+ {
+ ImageFile = imageFile;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
new file mode 100644
index 00000000..8352f6d5
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
@@ -0,0 +1,531 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using AsyncAwaitBestPractices;
+using AsyncImageLoader;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using DynamicData;
+using DynamicData.Binding;
+using FluentAvalonia.UI.Controls;
+using Microsoft.Extensions.Logging;
+using Nito.Disposables.Internals;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Extensions;
+using StabilityMatrix.Avalonia.Helpers;
+using StabilityMatrix.Avalonia.Languages;
+using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Services;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.ViewModels.Dialogs;
+using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Extensions;
+using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Helper.Factory;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Database;
+using StabilityMatrix.Core.Models.FileInterfaces;
+using StabilityMatrix.Core.Processes;
+using StabilityMatrix.Core.Services;
+using Size = StabilityMatrix.Core.Models.Settings.Size;
+using Symbol = FluentIcons.Common.Symbol;
+using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
+
+namespace StabilityMatrix.Avalonia.ViewModels;
+
+[View(typeof(Views.OutputsPage))]
+[Singleton]
+public partial class OutputsPageViewModel : PageViewModelBase
+{
+ private readonly ISettingsManager settingsManager;
+ private readonly IPackageFactory packageFactory;
+ private readonly INotificationService notificationService;
+ private readonly INavigationService navigationService;
+ private readonly ILogger logger;
+ public override string Title => Resources.Label_OutputsPageTitle;
+
+ public override IconSource IconSource =>
+ new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
+
+ public SourceCache OutputsCache { get; } =
+ new(file => file.AbsolutePath);
+
+ public IObservableCollection Outputs { get; set; } =
+ new ObservableCollectionExtended();
+
+ public IEnumerable OutputTypes { get; } = Enum.GetValues();
+
+ [ObservableProperty]
+ private ObservableCollection categories;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
+ private PackageOutputCategory selectedCategory;
+
+ [ObservableProperty]
+ private SharedOutputType selectedOutputType;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(NumImagesSelected))]
+ private int numItemsSelected;
+
+ [ObservableProperty]
+ private string searchQuery;
+
+ [ObservableProperty]
+ private Size imageSize = new(300, 300);
+
+ [ObservableProperty]
+ private bool isConsolidating;
+
+ public bool CanShowOutputTypes =>
+ SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
+
+ public string NumImagesSelected =>
+ NumItemsSelected == 1
+ ? Resources.Label_OneImageSelected
+ : string.Format(Resources.Label_NumImagesSelected, NumItemsSelected);
+
+ public OutputsPageViewModel(
+ ISettingsManager settingsManager,
+ IPackageFactory packageFactory,
+ INotificationService notificationService,
+ INavigationService navigationService,
+ ILogger logger
+ )
+ {
+ this.settingsManager = settingsManager;
+ this.packageFactory = packageFactory;
+ this.notificationService = notificationService;
+ this.navigationService = navigationService;
+ this.logger = logger;
+
+ var searcher = new ImageSearcher();
+
+ // Observable predicate from SearchQuery changes
+ var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
+ .Throttle(TimeSpan.FromMilliseconds(50))!
+ .Select(property => searcher.GetPredicate(property.Value))
+ .AsObservable();
+
+ OutputsCache
+ .Connect()
+ .DeferUntilLoaded()
+ .Filter(searchPredicate)
+ .Transform(file => new OutputImageViewModel(file))
+ .SortBy(vm => vm.ImageFile.CreatedAt, SortDirection.Descending)
+ .Bind(Outputs)
+ .WhenPropertyChanged(p => p.IsSelected)
+ .Subscribe(_ =>
+ {
+ NumItemsSelected = Outputs.Count(o => o.IsSelected);
+ });
+
+ settingsManager.RelayPropertyFor(
+ this,
+ vm => vm.ImageSize,
+ settings => settings.OutputsImageSize,
+ delay: TimeSpan.FromMilliseconds(250)
+ );
+ }
+
+ public override void OnLoaded()
+ {
+ if (Design.IsDesignMode)
+ return;
+
+ if (!settingsManager.IsLibraryDirSet)
+ return;
+
+ Directory.CreateDirectory(settingsManager.ImagesDirectory);
+
+ var packageCategories = settingsManager.Settings.InstalledPackages
+ .Where(x => !x.UseSharedOutputFolder)
+ .Select(packageFactory.GetPackagePair)
+ .WhereNotNull()
+ .Where(
+ p =>
+ p.BasePackage.SharedOutputFolders != null
+ && p.BasePackage.SharedOutputFolders.Any()
+ )
+ .Select(
+ pair =>
+ new PackageOutputCategory
+ {
+ Path = Path.Combine(
+ pair.InstalledPackage.FullPath!,
+ pair.BasePackage.OutputFolderName
+ ),
+ Name = pair.InstalledPackage.DisplayName ?? ""
+ }
+ )
+ .ToList();
+
+ packageCategories.Insert(
+ 0,
+ new PackageOutputCategory
+ {
+ Path = settingsManager.ImagesDirectory,
+ Name = "Shared Output Folder"
+ }
+ );
+
+ packageCategories.Insert(
+ 1,
+ new PackageOutputCategory
+ {
+ Path = settingsManager.ImagesInferenceDirectory,
+ Name = "Inference"
+ }
+ );
+
+ Categories = new ObservableCollection(packageCategories);
+ SelectedCategory = Categories.First();
+ SelectedOutputType = SharedOutputType.All;
+ SearchQuery = string.Empty;
+ ImageSize = settingsManager.Settings.OutputsImageSize;
+
+ var path =
+ CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
+ ? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
+ : SelectedCategory.Path;
+ GetOutputs(path);
+ }
+
+ partial void OnSelectedCategoryChanged(
+ PackageOutputCategory? oldValue,
+ PackageOutputCategory? newValue
+ )
+ {
+ if (oldValue == newValue || newValue == null)
+ return;
+
+ var path =
+ CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
+ ? Path.Combine(newValue.Path, SelectedOutputType.ToString())
+ : SelectedCategory.Path;
+ GetOutputs(path);
+ }
+
+ partial void OnSelectedOutputTypeChanged(SharedOutputType oldValue, SharedOutputType newValue)
+ {
+ if (oldValue == newValue)
+ return;
+
+ var path =
+ newValue == SharedOutputType.All
+ ? SelectedCategory.Path
+ : Path.Combine(SelectedCategory.Path, newValue.ToString());
+ GetOutputs(path);
+ }
+
+ public Task OnImageClick(OutputImageViewModel item)
+ {
+ // Select image if we're in "select mode"
+ if (NumItemsSelected > 0)
+ {
+ item.IsSelected = !item.IsSelected;
+ }
+ else
+ {
+ return ShowImageDialog(item);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public async Task ShowImageDialog(OutputImageViewModel item)
+ {
+ var currentIndex = Outputs.IndexOf(item);
+
+ var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath));
+
+ // Preload
+ await image.GetBitmapAsync();
+
+ var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
+
+ using var onNext = Observable
+ .FromEventPattern(
+ vm,
+ nameof(ImageViewerViewModel.NavigationRequested)
+ )
+ .Subscribe(ctx =>
+ {
+ Dispatcher.UIThread
+ .InvokeAsync(async () =>
+ {
+ var sender = (ImageViewerViewModel)ctx.Sender!;
+ var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1);
+
+ if (newIndex >= 0 && newIndex < Outputs.Count)
+ {
+ var newImage = Outputs[newIndex];
+ var newImageSource = new ImageSource(
+ new FilePath(newImage.ImageFile.AbsolutePath)
+ );
+
+ // Preload
+ await newImageSource.GetBitmapAsync();
+
+ sender.ImageSource = newImageSource;
+ sender.LocalImageFile = newImage.ImageFile;
+
+ currentIndex = newIndex;
+ }
+ })
+ .SafeFireAndForget();
+ });
+
+ await vm.GetDialog().ShowAsync();
+ }
+
+ public Task CopyImage(string imagePath)
+ {
+ var clipboard = App.Clipboard;
+ return clipboard.SetFileDataObjectAsync(imagePath);
+ }
+
+ public Task OpenImage(string imagePath) => ProcessRunner.OpenFileBrowser(imagePath);
+
+ public async Task DeleteImage(OutputImageViewModel? item)
+ {
+ if (item is null)
+ return;
+
+ var confirmationDialog = new BetterContentDialog
+ {
+ Title = "Are you sure you want to delete this image?",
+ Content = "This action cannot be undone.",
+ PrimaryButtonText = Resources.Action_Delete,
+ SecondaryButtonText = Resources.Action_Cancel,
+ DefaultButton = ContentDialogButton.Primary,
+ IsSecondaryButtonEnabled = true,
+ };
+ var dialogResult = await confirmationDialog.ShowAsync();
+ if (dialogResult != ContentDialogResult.Primary)
+ return;
+
+ // Delete the file
+ var imageFile = new FilePath(item.ImageFile.AbsolutePath);
+ var result = await notificationService.TryAsync(imageFile.DeleteAsync());
+
+ if (!result.IsSuccessful)
+ {
+ return;
+ }
+
+ OutputsCache.Remove(item.ImageFile);
+
+ // Invalidate cache
+ if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
+ {
+ loader.RemoveAllNamesFromCache(imageFile.Name);
+ }
+ }
+
+ public void SendToTextToImage(OutputImageViewModel vm)
+ {
+ navigationService.NavigateTo();
+ EventManager.Instance.OnInferenceTextToImageRequested(vm.ImageFile);
+ }
+
+ public void SendToUpscale(OutputImageViewModel vm)
+ {
+ navigationService.NavigateTo();
+ EventManager.Instance.OnInferenceUpscaleRequested(vm.ImageFile);
+ }
+
+ public void ClearSelection()
+ {
+ foreach (var output in Outputs)
+ {
+ output.IsSelected = false;
+ }
+ }
+
+ public void SelectAll()
+ {
+ foreach (var output in Outputs)
+ {
+ output.IsSelected = true;
+ }
+ }
+
+ public async Task DeleteAllSelected()
+ {
+ var confirmationDialog = new BetterContentDialog
+ {
+ Title = $"Are you sure you want to delete {NumItemsSelected} images?",
+ Content = "This action cannot be undone.",
+ PrimaryButtonText = Resources.Action_Delete,
+ SecondaryButtonText = Resources.Action_Cancel,
+ DefaultButton = ContentDialogButton.Primary,
+ IsSecondaryButtonEnabled = true,
+ };
+ var dialogResult = await confirmationDialog.ShowAsync();
+ if (dialogResult != ContentDialogResult.Primary)
+ return;
+
+ var selected = Outputs.Where(o => o.IsSelected).ToList();
+ Debug.Assert(selected.Count == NumItemsSelected);
+ foreach (var output in selected)
+ {
+ // Delete the file
+ var imageFile = new FilePath(output.ImageFile.AbsolutePath);
+ var result = await notificationService.TryAsync(imageFile.DeleteAsync());
+
+ if (!result.IsSuccessful)
+ {
+ continue;
+ }
+ OutputsCache.Remove(output.ImageFile);
+
+ // Invalidate cache
+ if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
+ {
+ loader.RemoveAllNamesFromCache(imageFile.Name);
+ }
+ }
+
+ NumItemsSelected = 0;
+ ClearSelection();
+ }
+
+ public async Task ConsolidateImages()
+ {
+ var stackPanel = new StackPanel();
+ stackPanel.Children.Add(
+ new TextBlock
+ {
+ Text = Resources.Label_ConsolidateExplanation,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 8, 0, 16)
+ }
+ );
+ foreach (var category in Categories)
+ {
+ if (category.Name == "Shared Output Folder")
+ {
+ continue;
+ }
+
+ stackPanel.Children.Add(
+ new CheckBox
+ {
+ Content = $"{category.Name} ({category.Path})",
+ IsChecked = true,
+ Margin = new Thickness(0, 8, 0, 0),
+ Tag = category.Path
+ }
+ );
+ }
+
+ var confirmationDialog = new BetterContentDialog
+ {
+ Title = Resources.Label_AreYouSure,
+ Content = stackPanel,
+ PrimaryButtonText = Resources.Action_Yes,
+ SecondaryButtonText = Resources.Action_Cancel,
+ DefaultButton = ContentDialogButton.Primary,
+ IsSecondaryButtonEnabled = true,
+ };
+
+ var dialogResult = await confirmationDialog.ShowAsync();
+ if (dialogResult != ContentDialogResult.Primary)
+ return;
+
+ IsConsolidating = true;
+
+ Directory.CreateDirectory(settingsManager.ConsolidatedImagesDirectory);
+
+ foreach (
+ var category in stackPanel.Children.OfType().Where(c => c.IsChecked == true)
+ )
+ {
+ if (
+ string.IsNullOrWhiteSpace(category.Tag?.ToString())
+ || !Directory.Exists(category.Tag?.ToString())
+ )
+ continue;
+
+ var directory = category.Tag.ToString();
+
+ foreach (
+ var path in Directory.EnumerateFiles(
+ directory,
+ "*.png",
+ SearchOption.AllDirectories
+ )
+ )
+ {
+ try
+ {
+ var file = new FilePath(path);
+ var newPath = settingsManager.ConsolidatedImagesDirectory + file.Name;
+ if (file.FullPath == newPath)
+ continue;
+
+ // ignore inference if not in inference directory
+ if (
+ file.FullPath.Contains(settingsManager.ImagesInferenceDirectory)
+ && directory != settingsManager.ImagesInferenceDirectory
+ )
+ {
+ continue;
+ }
+
+ await file.MoveToAsync(newPath);
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error when consolidating: ");
+ }
+ }
+ }
+
+ OnLoaded();
+ IsConsolidating = false;
+ }
+
+ private void GetOutputs(string directory)
+ {
+ if (!settingsManager.IsLibraryDirSet)
+ return;
+
+ if (
+ !Directory.Exists(directory)
+ && (
+ SelectedCategory.Path != settingsManager.ImagesDirectory
+ || SelectedOutputType != SharedOutputType.All
+ )
+ )
+ {
+ Directory.CreateDirectory(directory);
+ return;
+ }
+
+ var files = Directory
+ .EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
+ .Select(file => LocalImageFile.FromPath(file))
+ .ToList();
+
+ if (files.Count == 0)
+ {
+ OutputsCache.Clear();
+ }
+ else
+ {
+ OutputsCache.EditDiff(files);
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
index d921aa08..c9893634 100644
--- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Animations;
@@ -14,6 +15,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.Helper;
using StabilityMatrix.Core.Helper.Factory;
@@ -26,6 +28,8 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
+[ManagedService]
+[Transient]
public partial class PackageCardViewModel : ProgressViewModel
{
private readonly ILogger logger;
@@ -62,6 +66,15 @@ public partial class PackageCardViewModel : ProgressViewModel
[ObservableProperty]
private bool canUseConfigMethod;
+ [ObservableProperty]
+ private bool canUseSymlinkMethod;
+
+ [ObservableProperty]
+ private bool useSharedOutput;
+
+ [ObservableProperty]
+ private bool canUseSharedOutput;
+
public PackageCardViewModel(
ILogger logger,
IPackageFactory packageFactory,
@@ -103,6 +116,11 @@ public partial class PackageCardViewModel : ProgressViewModel
CanUseConfigMethod =
basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration)
?? false;
+ CanUseSymlinkMethod =
+ basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Symlink)
+ ?? false;
+ UseSharedOutput = Package?.UseSharedOutputFolder ?? false;
+ CanUseSharedOutput = basePackage?.SharedOutputFolders != null;
}
}
@@ -243,7 +261,29 @@ public partial class PackageCardViewModel : ProgressViewModel
{
ModificationCompleteMessage = $"{packageName} Update Complete"
};
- var updatePackageStep = new UpdatePackageStep(settingsManager, Package, basePackage);
+
+ var versionOptions = new DownloadPackageVersionOptions { IsLatest = true };
+ if (Package.Version.IsReleaseMode)
+ {
+ versionOptions = await basePackage.GetLatestVersion(Package.Version.IsPrerelease);
+ }
+ else
+ {
+ var commits = await basePackage.GetAllCommits(Package.Version.InstalledBranch);
+ var latest = commits?.FirstOrDefault();
+ if (latest == null)
+ throw new Exception("Could not find latest commit");
+
+ versionOptions.BranchName = Package.Version.InstalledBranch;
+ versionOptions.CommitHash = latest.Sha;
+ }
+
+ var updatePackageStep = new UpdatePackageStep(
+ settingsManager,
+ Package,
+ versionOptions,
+ basePackage
+ );
var steps = new List { updatePackageStep };
EventManager.Instance.OnPackageInstallProgressAdded(runner);
@@ -335,6 +375,39 @@ public partial class PackageCardViewModel : ProgressViewModel
await ProcessRunner.OpenFolderBrowser(Package.FullPath);
}
+ [RelayCommand]
+ public async Task OpenPythonPackagesDialog()
+ {
+ if (Package is not { FullPath: not null })
+ return;
+
+ var vm = vmFactory.Get(vm =>
+ {
+ vm.VenvPath = new DirectoryPath(Package.FullPath, "venv");
+ });
+
+ await vm.GetDialog().ShowAsync();
+ }
+
+ [RelayCommand]
+ private void OpenOnGitHub()
+ {
+ if (Package is null)
+ return;
+
+ var basePackage = packageFactory[Package.PackageName!];
+ if (basePackage == null)
+ {
+ logger.LogWarning(
+ "Could not find package {SelectedPackagePackageName}",
+ Package.PackageName
+ );
+ return;
+ }
+
+ ProcessRunner.OpenUrl(basePackage.GithubUrl);
+ }
+
private async Task HasUpdate()
{
if (Package == null || IsUnknownPackage || Design.IsDesignMode)
@@ -374,6 +447,33 @@ public partial class PackageCardViewModel : ProgressViewModel
public void ToggleSharedModelNone() => IsSharedModelDisabled = !IsSharedModelDisabled;
+ public void ToggleSharedOutput() => UseSharedOutput = !UseSharedOutput;
+
+ partial void OnUseSharedOutputChanged(bool value)
+ {
+ if (Package == null)
+ return;
+
+ if (value == Package.UseSharedOutputFolder)
+ return;
+
+ using var st = settingsManager.BeginTransaction();
+ Package.UseSharedOutputFolder = value;
+
+ var basePackage = packageFactory[Package.PackageName!];
+ if (basePackage == null)
+ return;
+
+ if (value)
+ {
+ basePackage.SetupOutputFolderLinks(Package.FullPath!);
+ }
+ else
+ {
+ basePackage.RemoveOutputFolderLinks(Package.FullPath!);
+ }
+ }
+
// fake radio button stuff
partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue)
{
diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
index 22c4dd10..5578a5f7 100644
--- a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
@@ -7,9 +7,12 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
+using Avalonia.Controls.Primitives;
+using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
+using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@@ -19,7 +22,6 @@ using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
-using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
@@ -35,12 +37,13 @@ namespace StabilityMatrix.Avalonia.ViewModels;
///
[View(typeof(PackageManagerPage))]
+[Singleton]
public partial class PackageManagerViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
- private readonly IPackageFactory packageFactory;
private readonly ServiceManager dialogFactory;
private readonly INotificationService notificationService;
+ private readonly ILogger logger;
public override string Title => "Packages";
public override IconSource IconSource =>
@@ -62,17 +65,19 @@ public partial class PackageManagerViewModel : PageViewModelBase
public IObservableCollection PackageCards { get; } =
new ObservableCollectionExtended();
+ private DispatcherTimer timer;
+
public PackageManagerViewModel(
ISettingsManager settingsManager,
- IPackageFactory packageFactory,
ServiceManager dialogFactory,
- INotificationService notificationService
+ INotificationService notificationService,
+ ILogger logger
)
{
this.settingsManager = settingsManager;
- this.packageFactory = packageFactory;
this.dialogFactory = dialogFactory;
this.notificationService = notificationService;
+ this.logger = logger;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
@@ -93,6 +98,9 @@ public partial class PackageManagerViewModel : PageViewModelBase
)
.Bind(PackageCards)
.Subscribe();
+
+ timer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(15), IsEnabled = true };
+ timer.Tick += async (_, _) => await CheckPackagesForUpdates();
}
public void SetPackages(IEnumerable packages)
@@ -117,22 +125,31 @@ public partial class PackageManagerViewModel : PageViewModelBase
var currentUnknown = await Task.Run(IndexUnknownPackages);
unknownInstalledPackages.Edit(s => s.Load(currentUnknown));
+
+ timer.Start();
+ }
+
+ public override void OnUnloaded()
+ {
+ timer.Stop();
+ base.OnUnloaded();
}
public async Task ShowInstallDialog(BasePackage? selectedPackage = null)
{
var viewModel = dialogFactory.Get();
- viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
viewModel.SelectedPackage = selectedPackage ?? viewModel.AvailablePackages[0];
var dialog = new BetterContentDialog
{
MaxDialogWidth = 900,
MinDialogWidth = 900,
+ FullSizeDesired = true,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
+ ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = new InstallerDialog { DataContext = viewModel }
};
@@ -157,6 +174,25 @@ public partial class PackageManagerViewModel : PageViewModelBase
}
}
+ private async Task CheckPackagesForUpdates()
+ {
+ foreach (var package in PackageCards)
+ {
+ try
+ {
+ await package.OnLoadedAsync();
+ }
+ catch (Exception e)
+ {
+ logger.LogError(
+ e,
+ "Failed to check for updates for {Package}",
+ package?.Package?.PackageName
+ );
+ }
+ }
+ }
+
private IEnumerable IndexUnknownPackages()
{
var packageDir = new DirectoryPath(settingsManager.LibraryDir).JoinDir("Packages");
diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
index dc410872..9cb0eb3d 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
@@ -17,7 +17,10 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
private readonly IPackageModificationRunner packageModificationRunner;
private BetterContentDialog? dialog;
- public PackageInstallProgressItemViewModel(IPackageModificationRunner packageModificationRunner)
+ public PackageInstallProgressItemViewModel(
+ IPackageModificationRunner packageModificationRunner,
+ bool hideCloseButton = false
+ )
{
this.packageModificationRunner = packageModificationRunner;
Id = packageModificationRunner.Id;
@@ -25,6 +28,7 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = packageModificationRunner.CurrentProgress.Percentage;
Progress.Text = packageModificationRunner.ConsoleOutput.LastOrDefault();
Progress.IsIndeterminate = packageModificationRunner.CurrentProgress.IsIndeterminate;
+ Progress.HideCloseButton = hideCloseButton;
Progress.Console.StartUpdates();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
index 51b88020..445f0e0c 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
@@ -22,6 +22,8 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels.Progress;
[View(typeof(ProgressManagerPage))]
+[ManagedService]
+[Singleton]
public partial class ProgressManagerViewModel : PageViewModelBase
{
private readonly INotificationService notificationService;
@@ -120,19 +122,22 @@ public partial class ProgressManagerViewModel : PageViewModelBase
}
}
- private async Task AddPackageInstall(IPackageModificationRunner packageModificationRunner)
+ private Task AddPackageInstall(IPackageModificationRunner packageModificationRunner)
{
if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id))
{
- return;
+ return Task.CompletedTask;
}
- var vm = new PackageInstallProgressItemViewModel(packageModificationRunner);
+ var vm = new PackageInstallProgressItemViewModel(
+ packageModificationRunner,
+ packageModificationRunner.HideCloseButton
+ );
ProgressItems.Add(vm);
- if (packageModificationRunner.ShowDialogOnStart)
- {
- await vm.ShowProgressDialog();
- }
+
+ return packageModificationRunner.ShowDialogOnStart
+ ? vm.ShowProgressDialog()
+ : Task.CompletedTask;
}
private void ShowFailedNotification(string title, string message)
diff --git a/StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs
index 10ee1194..b624f1e6 100644
--- a/StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs
@@ -16,10 +16,12 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(RefreshBadge))]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
+[ManagedService]
+[Transient]
public partial class RefreshBadgeViewModel : ViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
-
+
public string WorkingToolTipText { get; set; } = "Loading...";
public string SuccessToolTipText { get; set; } = "Success";
public string InactiveToolTipText { get; set; } = "";
@@ -32,7 +34,7 @@ public partial class RefreshBadgeViewModel : ViewModelBase
public IBrush SuccessColorBrush { get; set; } = ThemeColors.ThemeGreen;
public IBrush InactiveColorBrush { get; set; } = ThemeColors.ThemeYellow;
public IBrush FailColorBrush { get; set; } = ThemeColors.ThemeYellow;
-
+
public Func>? RefreshFunc { get; set; }
[ObservableProperty]
@@ -41,7 +43,7 @@ public partial class RefreshBadgeViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(CurrentToolTip))]
[NotifyPropertyChangedFor(nameof(Icon))]
private ProgressState state;
-
+
public bool IsWorking => State == ProgressState.Working;
/*public ControlAppearance Appearance => State switch
@@ -51,36 +53,40 @@ public partial class RefreshBadgeViewModel : ViewModelBase
ProgressState.Failed => ControlAppearance.Danger,
_ => ControlAppearance.Secondary
};*/
-
- public IBrush ColorBrush => State switch
- {
- ProgressState.Success => SuccessColorBrush,
- ProgressState.Inactive => InactiveColorBrush,
- ProgressState.Failed => FailColorBrush,
- _ => Brushes.Gray
- };
- public string CurrentToolTip => State switch
- {
- ProgressState.Working => WorkingToolTipText,
- ProgressState.Success => SuccessToolTipText,
- ProgressState.Inactive => InactiveToolTipText,
- ProgressState.Failed => FailToolTipText,
- _ => ""
- };
-
- public Symbol Icon => State switch
- {
- ProgressState.Success => SuccessIcon,
- ProgressState.Failed => FailIcon,
- _ => InactiveIcon
- };
+ public IBrush ColorBrush =>
+ State switch
+ {
+ ProgressState.Success => SuccessColorBrush,
+ ProgressState.Inactive => InactiveColorBrush,
+ ProgressState.Failed => FailColorBrush,
+ _ => Brushes.Gray
+ };
+
+ public string CurrentToolTip =>
+ State switch
+ {
+ ProgressState.Working => WorkingToolTipText,
+ ProgressState.Success => SuccessToolTipText,
+ ProgressState.Inactive => InactiveToolTipText,
+ ProgressState.Failed => FailToolTipText,
+ _ => ""
+ };
+
+ public Symbol Icon =>
+ State switch
+ {
+ ProgressState.Success => SuccessIcon,
+ ProgressState.Failed => FailIcon,
+ _ => InactiveIcon
+ };
[RelayCommand]
private async Task Refresh()
{
Logger.Info("Running refresh command...");
- if (RefreshFunc == null) return;
+ if (RefreshFunc == null)
+ return;
State = ProgressState.Working;
try
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
index cec925fb..80d5c7ba 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
@@ -1,12 +1,9 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(InferenceSettingsPage))]
-public partial class InferenceSettingsViewModel : ViewModelBase
-{
-
-}
+[Singleton]
+public partial class InferenceSettingsViewModel : ViewModelBase { }
diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
index eee148fa..74dc145b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
@@ -3,15 +3,16 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
-using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
@@ -21,6 +22,7 @@ using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using SkiaSharp;
@@ -29,6 +31,7 @@ using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@@ -49,6 +52,7 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(SettingsPage))]
+[Singleton]
public partial class SettingsViewModel : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -107,6 +111,25 @@ public partial class SettingsViewModel : PageViewModelBase
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
+ [ObservableProperty]
+ [CustomValidation(typeof(SettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
+ private string? outputImageFileNameFormat;
+
+ [ObservableProperty]
+ private string? outputImageFileNameFormatSample;
+
+ public IEnumerable OutputImageFileNameFormatVars =>
+ FileNameFormatProvider
+ .GetSample()
+ .Substitutions.Select(
+ kv =>
+ new FileNameFormatVar
+ {
+ Variable = $"{{{kv.Key}}}",
+ Example = kv.Value.Invoke()
+ }
+ );
+
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
@@ -201,6 +224,39 @@ public partial class SettingsViewModel : PageViewModelBase
true
);
+ this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
+ .Throttle(TimeSpan.FromMilliseconds(50))
+ .Subscribe(formatProperty =>
+ {
+ var provider = FileNameFormatProvider.GetSample();
+ var template = formatProperty.Value ?? string.Empty;
+
+ if (
+ !string.IsNullOrEmpty(template)
+ && provider.Validate(template) == ValidationResult.Success
+ )
+ {
+ var format = FileNameFormat.Parse(template, provider);
+ OutputImageFileNameFormatSample = format.GetFileName() + ".png";
+ }
+ else
+ {
+ // Use default format if empty
+ var defaultFormat = FileNameFormat.Parse(
+ FileNameFormat.DefaultTemplate,
+ provider
+ );
+ OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
+ }
+ });
+
+ settingsManager.RelayPropertyFor(
+ this,
+ vm => vm.OutputImageFileNameFormat,
+ settings => settings.InferenceOutputImageFileNameFormat,
+ true
+ );
+
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
@@ -225,6 +281,14 @@ public partial class SettingsViewModel : PageViewModelBase
UpdateAvailableTagCompletionCsvs();
}
+ public static ValidationResult ValidateOutputImageFileNameFormat(
+ string? format,
+ ValidationContext context
+ )
+ {
+ return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
+ }
+
partial void OnSelectedThemeChanged(string? value)
{
// In case design / tests
diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
index 7ad45f0c..e14edf4a 100644
--- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
@@ -21,7 +21,7 @@
@@ -66,6 +66,7 @@
-
+
-
+
-
+
diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
index f61dae4c..34e0887a 100644
--- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
+[Singleton]
public partial class CheckpointBrowserPage : UserControlBase
{
public CheckpointBrowserPage()
diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
index 55f759ec..1c8d8d0b 100644
--- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
@@ -30,7 +30,7 @@
Height="18"
Margin="4,0,0,0"
Padding="3"
- Width="40">
+ Width="48">
+ Margin="4">
-
+
@@ -65,9 +66,9 @@
-
+
@@ -121,15 +122,17 @@
-
+
-
+
@@ -213,8 +215,9 @@
@@ -268,7 +271,8 @@
ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding DisplayedCheckpointFiles}">
-
+
@@ -280,7 +284,7 @@
IsVisible="{Binding !CheckpointFiles.Count}"/>
@@ -445,6 +449,8 @@
diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
index 3cccca86..ee5c2946 100644
--- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
@@ -1,17 +1,29 @@
-using Avalonia.Controls;
+using System;
+using System.Linq;
+using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using DynamicData.Binding;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.ViewModels;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Helper;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.Views;
+[Singleton]
public partial class CheckpointsPage : UserControlBase
{
+ private ItemsControl? repeater;
+ private IDisposable? subscription;
+
public CheckpointsPage()
{
InitializeComponent();
-
+
AddHandler(DragDrop.DragEnterEvent, OnDragEnter);
AddHandler(DragDrop.DragLeaveEvent, OnDragExit);
AddHandler(DragDrop.DropEvent, OnDrop);
@@ -21,6 +33,34 @@ public partial class CheckpointsPage : UserControlBase
{
AvaloniaXamlLoader.Load(this);
}
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ subscription?.Dispose();
+ subscription = null;
+
+ if (DataContext is CheckpointsPageViewModel vm)
+ {
+ subscription = vm.WhenPropertyChanged(m => m.ShowConnectedModelImages)
+ .Subscribe(_ => InvalidateRepeater());
+ }
+ }
+
+ private void InvalidateRepeater()
+ {
+ repeater ??= this.FindControl("FilesRepeater");
+ repeater?.InvalidateArrange();
+ repeater?.InvalidateMeasure();
+
+ foreach (var child in this.GetVisualDescendants().OfType())
+ {
+ child?.InvalidateArrange();
+ child?.InvalidateMeasure();
+ }
+ }
+
private static async void OnDrop(object? sender, DragEventArgs e)
{
var sourceDataContext = (e.Source as Control)?.DataContext;
@@ -29,7 +69,7 @@ public partial class CheckpointsPage : UserControlBase
await folder.OnDrop(e);
}
}
-
+
private static void OnDragExit(object? sender, DragEventArgs e)
{
var sourceDataContext = (e.Source as Control)?.DataContext;
@@ -38,7 +78,7 @@ public partial class CheckpointsPage : UserControlBase
folder.IsCurrentDragTarget = false;
}
}
-
+
private static void OnDragEnter(object? sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
@@ -47,9 +87,9 @@ public partial class CheckpointsPage : UserControlBase
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.Files))
{
- e.DragEffects = DragDropEffects.None;
+ e.DragEffects = DragDropEffects.None;
}
-
+
// Forward to view model
var sourceDataContext = (e.Source as Control)?.DataContext;
if (sourceDataContext is CheckpointFolder folder)
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml.cs
index 22d34516..3928f022 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml.cs
@@ -1,12 +1,13 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
+using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class DownloadResourceDialog : UserControl
{
public DownloadResourceDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml.cs
index 63068706..3c54d2c2 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class EnvVarsDialog : UserControlBase
{
public EnvVarsDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml.cs
index bbd91996..b2bf44ad 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml.cs
@@ -3,15 +3,17 @@ using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Windowing;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class ExceptionDialog : AppWindowBase
{
public ExceptionDialog()
{
InitializeComponent();
-
+
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
}
@@ -20,7 +22,7 @@ public partial class ExceptionDialog : AppWindowBase
{
AvaloniaXamlLoader.Load(this);
}
-
+
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
private void ExitButton_OnClick(object? sender, RoutedEventArgs e)
{
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
index 1ab9fde9..39152677 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
@@ -106,6 +106,20 @@
Grid.Row="1"
Text="{Binding NegativePrompt}" />
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs
index 7af815c6..fc921fee 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs
@@ -1,10 +1,11 @@
using Avalonia;
using Avalonia.Input;
-using Avalonia.Interactivity;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class ImageViewerDialog : UserControlBase
{
public static readonly StyledProperty IsFooterEnabledProperty = AvaloniaProperty.Register<
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml.cs
index fc0e45d5..b2568572 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml.cs
@@ -1,9 +1,10 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class InferenceConnectionHelpDialog : UserControl
{
public InferenceConnectionHelpDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
index d3792f60..513144cd 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
@@ -16,34 +16,140 @@
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
+ RowDefinitions="Auto, Auto, Auto, Auto, *, Auto">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -60,7 +166,7 @@
-
+
-
+
@@ -185,7 +291,7 @@
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs
index 84d4d3a6..5dd11fc4 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class LaunchOptionsDialog : UserControl
{
public LaunchOptionsDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
index 1f1e6e62..8df43ca8 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
@@ -8,13 +8,14 @@
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="700"
x:DataType="dialogs:OneClickInstallViewModel"
d:DataContext="{x:Static designData:DesignData.OneClickInstallViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.OneClickInstallDialog">
@@ -37,8 +38,10 @@
Title="Use ComfyUI with Inference"
Subtitle="A new built-in native Stable Diffusion experience, powered by ComfyUI"
ActionButtonContent="{x:Static lang:Resources.Action_Install}"
+ ActionButtonCommand="{Binding InstallComfyForInferenceCommand}"
CloseButtonContent="{x:Static lang:Resources.Action_Close}"
PreferredPlacement="RightTop"
+ Margin="8,0,0,0"
PlacementMargin="0,0,0,0"
TailVisibility="Auto">
@@ -68,16 +71,123 @@
FontSize="24"
Margin="16, 16, 0, 4"/>
+ TextWrapping="Wrap"
+ Margin="16, 0, 0, 4"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -114,7 +224,7 @@
FontSize="32"
HorizontalAlignment="Center"
Classes="success"
- Margin="16"
+ Margin="8"
Padding="16, 8, 16, 8" />
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs
index 3e6ff511..d615eddc 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs
@@ -3,7 +3,6 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Core.Models.Packages;
@@ -30,7 +29,12 @@ public partial class OneClickInstallDialog : UserControl
var teachingTip =
this.FindControl("InferenceTeachingTip")
?? throw new InvalidOperationException("TeachingTip not found");
- ;
+
+ teachingTip.ActionButtonClick += (_, _) =>
+ {
+ teachingTip.IsOpen = false;
+ };
+
// Find ComfyUI listbox item
var listBox = this.FindControl("PackagesListBox");
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs
index a5f8cf50..7123dfa9 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class PackageImportDialog : UserControlBase
{
public PackageImportDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
index 7b9d65fb..cb4de0be 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
@@ -12,7 +12,8 @@
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageModificationDialog">
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml
new file mode 100644
index 00000000..bb7dddda
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml.cs
new file mode 100644
index 00000000..197a94ad
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml.cs
@@ -0,0 +1,13 @@
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
+
+namespace StabilityMatrix.Avalonia.Views.Dialogs;
+
+[Transient]
+public partial class PythonPackagesDialog : UserControlBase
+{
+ public PythonPackagesDialog()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml.cs
index 3e874a83..f9eb3bf1 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class SelectDataDirectoryDialog : UserControlBase
{
public SelectDataDirectoryDialog()
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs
index 33d2b218..94ac9c31 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
+[Transient]
public partial class UpdateDialog : UserControl
{
public UpdateDialog()
@@ -14,4 +16,4 @@ public partial class UpdateDialog : UserControl
{
AvaloniaXamlLoader.Load(this);
}
-}
\ No newline at end of file
+}
diff --git a/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs
index f02ea254..e3a9cf7f 100644
--- a/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs
@@ -1,13 +1,13 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using Avalonia;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
+[Singleton]
public partial class FirstLaunchSetupWindow : AppWindowBase
{
public ContentDialogResult Result { get; private set; }
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml.cs b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml.cs
index eaa6a883..0e13e9d4 100644
--- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml.cs
@@ -1,7 +1,9 @@
using StabilityMatrix.Avalonia.Controls.Dock;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Inference;
+[Transient]
public partial class InferenceImageUpscaleView : DockUserControlBase
{
public InferenceImageUpscaleView()
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml.cs b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml.cs
index 11784fe4..d9116275 100644
--- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml.cs
@@ -1,7 +1,9 @@
using StabilityMatrix.Avalonia.Controls.Dock;
+using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Inference;
+[Transient]
public partial class InferenceTextToImageView : DockUserControlBase
{
public InferenceTextToImageView()
diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml
index 4dbc644a..74692705 100644
--- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml
+++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml
@@ -49,13 +49,15 @@
+ ToolTip.Tip="Not yet implemented, coming soon!" />
+ ToolTip.Tip="Not yet implemented, coming soon!" />