diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e207ed4..7cefa26e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,8 +6,11 @@ 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.9.0-dev.3
+### Added
+- Added OpenArt.AI workflow browser for ComfyUI workflows
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
+- Fixed output sharing for Stable Diffusion WebUI Forge
## v2.9.0-dev.2
### Added
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 77d5a384..31d10d8d 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
@@ -36,6 +36,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
+using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
@@ -51,6 +52,7 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData;
+[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
@@ -1008,6 +1010,60 @@ The gallery images are often inpainted, but you will get something very similar
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get();
+ public static OpenArtWorkflowViewModel OpenArtWorkflowViewModel =>
+ new()
+ {
+ Workflow = new OpenArtSearchResult
+ {
+ Name = "Test Workflow",
+ Creator = new OpenArtCreator
+ {
+ Name = "Test Creator Name",
+ Username = "Test Creator Username"
+ },
+ Thumbnails =
+ [
+ new OpenArtThumbnail
+ {
+ Url = new Uri(
+ "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
+ )
+ }
+ ],
+ NodesIndex =
+ [
+ "Anything Everywhere",
+ "Reroute",
+ "Note",
+ ".",
+ "ComfyUI's ControlNet Auxiliary Preprocessors",
+ "DWPreprocessor",
+ "PixelPerfectResolution",
+ "AIO_Preprocessor",
+ ",",
+ "ComfyUI",
+ "PreviewImage",
+ "CLIPTextEncode",
+ "EmptyLatentImage",
+ "SplitImageWithAlpha",
+ "ControlNetApplyAdvanced",
+ "JoinImageWithAlpha",
+ "LatentUpscaleBy",
+ "VAEEncode",
+ "LoadImage",
+ "ControlNetLoader",
+ "CLIPVisionLoader",
+ "SaveImage",
+ ",",
+ "ComfyUI Impact Pack",
+ "SAMLoader",
+ "UltralyticsDetectorProvider",
+ "FaceDetailer",
+ ","
+ ]
+ }
+ };
+
public static string CurrentDirectory => Directory.GetCurrentDirectory();
public static Indexer Types { get; } = new();
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
index 01451192..e96ebd24 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
+++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
@@ -374,6 +374,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Open on OpenArt.
+ ///
+ public static string Action_OpenOnOpenArt {
+ get {
+ return ResourceManager.GetString("Action_OpenOnOpenArt", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Open Project....
///
@@ -1688,6 +1697,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Node Details.
+ ///
+ public static string Label_NodeDetails {
+ get {
+ return ResourceManager.GetString("Label_NodeDetails", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to No extensions found..
///
@@ -2435,6 +2453,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Workflow Description.
+ ///
+ public static string Label_WorkflowDescription {
+ get {
+ return ResourceManager.GetString("Label_WorkflowDescription", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Workflows.
///
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx
index e8f7ed3f..91666c7c 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.resx
+++ b/StabilityMatrix.Avalonia/Languages/Resources.resx
@@ -984,4 +984,13 @@
Config
+
+ Open on OpenArt
+
+
+ Node Details
+
+
+ Workflow Description
+
diff --git a/StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs b/StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs
new file mode 100644
index 00000000..b075758d
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace StabilityMatrix.Avalonia.Models;
+
+public class OpenArtCustomNode
+{
+ public required string Title { get; set; }
+ public List Children { get; set; } = [];
+ public bool IsInstalled { get; set; }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
index 148712c0..bcb2ae62 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
@@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
+using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database;
@@ -216,7 +217,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
MaxDialogHeight = 950,
};
- var prunedDescription = PruneDescription(model);
+ var prunedDescription = Utilities.RemoveHtml(model.Description);
var viewModel = dialogFactory.Get();
viewModel.Dialog = dialog;
@@ -263,23 +264,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
await DoImport(model, downloadPath, selectedVersion, selectedFile);
}
- 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}") ?? string.Empty;
- prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
- return prunedDescription;
- }
-
private static async Task SaveCmInfo(
CivitModel model,
CivitModelVersion modelVersion,
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs
new file mode 100644
index 00000000..94ebf773
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.Views.Dialogs;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+using StabilityMatrix.Core.Models.Api.OpenArt;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
+
+[View(typeof(OpenArtWorkflowDialog))]
+[ManagedService]
+[Transient]
+public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase
+{
+ public required OpenArtSearchResult Workflow { get; init; }
+ public string? InstalledComfyPath { get; init; }
+
+ [ObservableProperty]
+ private ObservableCollection customNodes = [];
+
+ [ObservableProperty]
+ private string prunedDescription = string.Empty;
+
+ public override void OnLoaded()
+ {
+ CustomNodes = new ObservableCollection(ParseNodes(Workflow.NodesIndex.ToList()));
+ PrunedDescription = Utilities.RemoveHtml(Workflow.Description);
+ }
+
+ [Localizable(false)]
+ private List ParseNodes(List nodes)
+ {
+ var indexOfFirstDot = nodes.IndexOf(".");
+ if (indexOfFirstDot != -1)
+ {
+ nodes = nodes[(indexOfFirstDot + 1)..];
+ }
+
+ var installedNodes = new List();
+ if (!string.IsNullOrWhiteSpace(InstalledComfyPath))
+ {
+ installedNodes = Directory
+ .EnumerateDirectories(InstalledComfyPath)
+ .Select(
+ x => x.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last()
+ )
+ .Where(x => ComfyNodeMap.Lookup.Values.FirstOrDefault(y => y.EndsWith(x)) != null)
+ .ToList();
+ }
+
+ var sections = new List();
+ OpenArtCustomNode? currentSection = null;
+
+ foreach (var node in nodes)
+ {
+ if (node is "." or ",")
+ {
+ currentSection = null; // End of the current section
+ continue;
+ }
+
+ if (currentSection == null)
+ {
+ currentSection = new OpenArtCustomNode
+ {
+ Title = node,
+ IsInstalled = installedNodes.Contains(node)
+ };
+ sections.Add(currentSection);
+ }
+ else
+ {
+ currentSection.Children.Add(node);
+ }
+ }
+
+ if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null)
+ {
+ sections = sections.Where(x => x.Title != "ComfyUI").ToList();
+ }
+
+ return sections;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
index c983ef94..6897ee23 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -8,14 +9,18 @@ using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Refit;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Processes;
+using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
@@ -23,16 +28,18 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OpenArtBrowserPage))]
[Singleton]
-public partial class OpenArtBrowserViewModel(IOpenArtApi openArtApi, INotificationService notificationService)
- : PageViewModelBase,
- IInfinitelyScroll
+public partial class OpenArtBrowserViewModel(
+ IOpenArtApi openArtApi,
+ INotificationService notificationService,
+ ISettingsManager settingsManager
+) : PageViewModelBase, IInfinitelyScroll
{
private const int PageSize = 20;
- public override string Title => "Workflows";
+ public override string Title => Resources.Label_Workflows;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Whiteboard };
- private SourceCache searchResultsCache = new(x => x.Id);
+ private readonly SourceCache searchResultsCache = new(x => x.Id);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))]
@@ -121,6 +128,36 @@ public partial class OpenArtBrowserViewModel(IOpenArtApi openArtApi, INotificati
await DoSearch();
}
+ [RelayCommand]
+ private async Task OpenWorkflow(OpenArtSearchResult workflow)
+ {
+ var existingComfy = settingsManager.Settings.InstalledPackages.FirstOrDefault(
+ x => x.PackageName is "ComfyUI"
+ );
+
+ var dialog = new BetterContentDialog
+ {
+ IsPrimaryButtonEnabled = true,
+ IsSecondaryButtonEnabled = true,
+ PrimaryButtonText = Resources.Action_Import,
+ SecondaryButtonText = Resources.Action_Cancel,
+ DefaultButton = ContentDialogButton.Primary,
+ IsFooterVisible = true,
+ MaxDialogWidth = 750,
+ MaxDialogHeight = 850,
+ CloseOnClickOutside = true,
+ Content = new OpenArtWorkflowViewModel
+ {
+ Workflow = workflow,
+ InstalledComfyPath = existingComfy is null
+ ? null
+ : Path.Combine(settingsManager.LibraryDir, existingComfy.LibraryPath!, "custom_nodes")
+ },
+ };
+
+ await dialog.ShowAsync();
+ }
+
private async Task DoSearch(int page = 0)
{
IsLoading = true;
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml
new file mode 100644
index 00000000..af014bf1
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml.cs
new file mode 100644
index 00000000..e0ed1d00
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml.cs
@@ -0,0 +1,13 @@
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Core.Attributes;
+
+namespace StabilityMatrix.Avalonia.Views.Dialogs;
+
+[Transient]
+public partial class OpenArtWorkflowDialog : UserControlBase
+{
+ public OpenArtWorkflowDialog()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml
index 8e7a621e..75a6f54c 100644
--- a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml
@@ -66,6 +66,11 @@
+
+
+
-
-
-
+ CornerRadius="8"
+ Command="{StaticResource OpenWorkflowCommand}"
+ CommandParameter="{Binding }">