diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e42023..4363ff89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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.10.0-preview.1 +### Added +- Added OpenArt.AI workflow browser for ComfyUI workflows + ## v2.10.0-dev.3 ### Added - Added support for deep links from the new Stability Matrix Chrome extension @@ -100,6 +105,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added copy image support on linux and macOS for Inference outputs viewer menu ### Fixed - Fixed StableSwarmUI not installing properly on macOS +- Fixed output sharing for Stable Diffusion WebUI Forge - Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS - Fixed default command line args for SDWebUI Forge on macOS - Fixed output paths and output sharing for SDWebUI Forge diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 71af8fde..caae4e88 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -330,7 +330,8 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService() }, FooterPages = { provider.GetRequiredService() } } @@ -553,7 +554,7 @@ public sealed class App : Application .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); - c.Timeout = TimeSpan.FromSeconds(15); + c.Timeout = TimeSpan.FromSeconds(30); }) .AddPolicyHandler(retryPolicy); @@ -562,7 +563,7 @@ public sealed class App : Application .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); - c.Timeout = TimeSpan.FromSeconds(15); + c.Timeout = TimeSpan.FromSeconds(30); }) .AddPolicyHandler(retryPolicy); @@ -580,6 +581,15 @@ public sealed class App : Application new TokenAuthHeaderHandler(serviceProvider.GetRequiredService()) ); + services + .AddRefitClient(defaultRefitSettings) + .ConfigureHttpClient(c => + { + c.BaseAddress = new Uri("https://openart.ai/api/public/workflows"); + c.Timeout = TimeSpan.FromSeconds(30); + }) + .AddPolicyHandler(retryPolicy); + // Add Refit client managers services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); diff --git a/StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs b/StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs new file mode 100644 index 00000000..dc25748a --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs @@ -0,0 +1,208 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactions.DragAndDrop; +using Avalonia.Xaml.Interactivity; + +namespace StabilityMatrix.Avalonia.Controls; + +public class BetterContextDragBehavior : Behavior +{ + private Point _dragStartPoint; + private PointerEventArgs? _triggerEvent; + private bool _lock; + private bool _captured; + + public static readonly StyledProperty ContextProperty = AvaloniaProperty.Register< + ContextDragBehavior, + object? + >(nameof(Context)); + + public static readonly StyledProperty HandlerProperty = AvaloniaProperty.Register< + ContextDragBehavior, + IDragHandler? + >(nameof(Handler)); + + public static readonly StyledProperty HorizontalDragThresholdProperty = AvaloniaProperty.Register< + ContextDragBehavior, + double + >(nameof(HorizontalDragThreshold), 3); + + public static readonly StyledProperty VerticalDragThresholdProperty = AvaloniaProperty.Register< + ContextDragBehavior, + double + >(nameof(VerticalDragThreshold), 3); + + public static readonly StyledProperty DataFormatProperty = AvaloniaProperty.Register< + BetterContextDragBehavior, + string + >("DataFormat"); + + public string DataFormat + { + get => GetValue(DataFormatProperty); + set => SetValue(DataFormatProperty, value); + } + + public object? Context + { + get => GetValue(ContextProperty); + set => SetValue(ContextProperty, value); + } + + public IDragHandler? Handler + { + get => GetValue(HandlerProperty); + set => SetValue(HandlerProperty, value); + } + + public double HorizontalDragThreshold + { + get => GetValue(HorizontalDragThresholdProperty); + set => SetValue(HorizontalDragThresholdProperty, value); + } + + public double VerticalDragThreshold + { + get => GetValue(VerticalDragThresholdProperty); + set => SetValue(VerticalDragThresholdProperty, value); + } + + /// + protected override void OnAttachedToVisualTree() + { + AssociatedObject?.AddHandler( + InputElement.PointerPressedEvent, + AssociatedObject_PointerPressed, + RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble + ); + AssociatedObject?.AddHandler( + InputElement.PointerReleasedEvent, + AssociatedObject_PointerReleased, + RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble + ); + AssociatedObject?.AddHandler( + InputElement.PointerMovedEvent, + AssociatedObject_PointerMoved, + RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble + ); + AssociatedObject?.AddHandler( + InputElement.PointerCaptureLostEvent, + AssociatedObject_CaptureLost, + RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble + ); + } + + /// + protected override void OnDetachedFromVisualTree() + { + AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed); + AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased); + AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved); + AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost); + } + + private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value) + { + var data = new DataObject(); + data.Set(DataFormat, value!); + + var effect = DragDropEffects.None; + + if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt)) + { + effect |= DragDropEffects.Link; + } + else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift)) + { + effect |= DragDropEffects.Move; + } + else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control)) + { + effect |= DragDropEffects.Copy; + } + else + { + effect |= DragDropEffects.Move; + } + + await DragDrop.DoDragDrop(triggerEvent, data, effect); + } + + private void Released() + { + _triggerEvent = null; + _lock = false; + } + + private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (properties.IsLeftButtonPressed) + { + if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext) + { + _dragStartPoint = e.GetPosition(null); + _triggerEvent = e; + _lock = true; + _captured = true; + } + } + } + + private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (_captured) + { + if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { }) + { + Released(); + } + + _captured = false; + } + } + + private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { }) + { + var point = e.GetPosition(null); + var diff = _dragStartPoint - point; + var horizontalDragThreshold = HorizontalDragThreshold; + var verticalDragThreshold = VerticalDragThreshold; + + if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold) + { + if (_lock) + { + _lock = false; + } + else + { + return; + } + + var context = Context ?? AssociatedObject?.DataContext; + + Handler?.BeforeDragDrop(sender, _triggerEvent, context); + + await DoDragDrop(_triggerEvent, context); + + Handler?.AfterDragDrop(sender, _triggerEvent, context); + + _triggerEvent = null; + } + } + } + + private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + Released(); + _captured = false; + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index df122b14..3fc6455b 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 { @@ -921,6 +923,36 @@ The gallery images are often inpainted, but you will get something very similar vm.IsBatchIndexEnabled = true; }); + public static InstalledWorkflowsViewModel InstalledWorkflowsViewModel + { + get + { + var vm = Services.GetRequiredService(); + vm.DisplayedWorkflows = new ObservableCollectionExtended + { + new() + { + Workflow = new() + { + Name = "Test Workflow", + Creator = new OpenArtCreator { Name = "Test Creator" }, + Thumbnails = + [ + new OpenArtThumbnail + { + Url = new Uri( + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512" + ) + } + ] + } + } + }; + + return vm; + } + } + public static IList SampleCompletionData => new List { @@ -1032,6 +1064,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(Services.GetRequiredService(), Services.GetRequiredService()) + { + 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 eb621b57..388037ce 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.... /// @@ -1310,6 +1319,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Error retrieving workflows. + /// + public static string Label_ErrorRetrievingWorkflows { + get { + return ResourceManager.GetString("Label_ErrorRetrievingWorkflows", resourceCulture); + } + } + /// /// Looks up a localized string similar to Everything looks good!. /// @@ -1346,6 +1364,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Finished importing workflow and custom nodes. + /// + public static string Label_FinishedImportingWorkflow { + get { + return ResourceManager.GetString("Label_FinishedImportingWorkflow", resourceCulture); + } + } + /// /// Looks up a localized string similar to First Page. /// @@ -1490,6 +1517,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Infinite Scrolling. + /// + public static string Label_InfiniteScrolling { + get { + return ResourceManager.GetString("Label_InfiniteScrolling", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inner exception. /// @@ -1778,6 +1814,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.. /// @@ -1841,6 +1886,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to OpenArt Browser. + /// + public static string Label_OpenArtBrowser { + get { + return ResourceManager.GetString("Label_OpenArtBrowser", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output Folder. /// @@ -2561,6 +2615,69 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Workflow Browser. + /// + public static string Label_WorkflowBrowser { + get { + return ResourceManager.GetString("Label_WorkflowBrowser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Workflow Deleted. + /// + public static string Label_WorkflowDeleted { + get { + return ResourceManager.GetString("Label_WorkflowDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} deleted successfully. + /// + public static string Label_WorkflowDeletedSuccessfully { + get { + return ResourceManager.GetString("Label_WorkflowDeletedSuccessfully", resourceCulture); + } + } + + /// + /// 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 The workflow and custom nodes have been imported.. + /// + public static string Label_WorkflowImportComplete { + get { + return ResourceManager.GetString("Label_WorkflowImportComplete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Workflow Imported. + /// + public static string Label_WorkflowImported { + get { + return ResourceManager.GetString("Label_WorkflowImported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Workflows. + /// + public static string Label_Workflows { + get { + return ResourceManager.GetString("Label_Workflows", resourceCulture); + } + } + /// /// Looks up a localized string similar to You're up to date. /// @@ -2642,6 +2759,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Installed Workflows. + /// + public static string TabLabel_InstalledWorkflows { + get { + return ResourceManager.GetString("TabLabel_InstalledWorkflows", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add a package to get started!. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 6b0bde49..4c70a20c 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -972,9 +972,30 @@ Select Download Location: + + Workflows + + + Infinite Scrolling + + + Workflow Browser + Config + + Open on OpenArt + + + Node Details + + + Workflow Description + + + OpenArt Browser + Preview Preprocessor @@ -1026,4 +1047,25 @@ Stability Matrix is already running + + {0} deleted successfully + + + Workflow Deleted + + + Error retrieving workflows + + + Installed Workflows + + + Workflow Imported + + + Finished importing workflow and custom nodes + + + The workflow and custom nodes have been imported. + 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/Models/OpenArtMetadata.cs b/StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs new file mode 100644 index 00000000..7620816c --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Avalonia.Platform.Storage; +using StabilityMatrix.Core.Models.Api.OpenArt; + +namespace StabilityMatrix.Avalonia.Models; + +public class OpenArtMetadata +{ + [JsonPropertyName("sm_workflow_data")] + public OpenArtSearchResult? Workflow { get; set; } + + [JsonIgnore] + public string? FirstThumbnail => Workflow?.Thumbnails?.Select(x => x.Url).FirstOrDefault()?.ToString(); + + [JsonIgnore] + public List? FilePath { get; set; } + + [JsonIgnore] + public bool HasMetadata => Workflow?.Creator != null; +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index d1055205..cda7adcd 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -40,6 +40,7 @@ + @@ -197,6 +198,9 @@ NewInstallerDialog.axaml Code + + MSBuild:GenerateCodeFromAttributes + 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..d337b52c --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api.OpenArt; +using StabilityMatrix.Core.Models.Packages.Extensions; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; + +[View(typeof(OpenArtWorkflowDialog))] +[ManagedService] +[Transient] +public partial class OpenArtWorkflowViewModel( + ISettingsManager settingsManager, + IPackageFactory packageFactory +) : ContentDialogViewModelBase +{ + public required OpenArtSearchResult Workflow { get; init; } + + [ObservableProperty] + private ObservableCollection customNodes = []; + + [ObservableProperty] + private string prunedDescription = string.Empty; + + [ObservableProperty] + private bool installRequiredNodes = true; + + [ObservableProperty] + private InstalledPackage? selectedPackage; + + public PackagePair? SelectedPackagePair => + SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null; + + public List AvailablePackages => + settingsManager + .Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI") + .ToList(); + + public List MissingNodes { get; } = []; + + public override async Task OnLoadedAsync() + { + if (Design.IsDesignMode) + return; + + if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage) + { + SelectedPackage = preferredPackage; + } + else + { + SelectedPackage = AvailablePackages.FirstOrDefault(); + } + + if (SelectedPackage == null) + { + InstallRequiredNodes = false; + } + + CustomNodes = new ObservableCollection( + await ParseNodes(Workflow.NodesIndex.ToList()) + ); + PrunedDescription = Utilities.RemoveHtml(Workflow.Description); + } + + partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue) + { + if (oldValue is null) + return; + + settingsManager.Transaction(settings => + { + settings.PreferredWorkflowPackage = newValue; + }); + + OnLoadedAsync().SafeFireAndForget(); + } + + [Localizable(false)] + private async Task> ParseNodes(List nodes) + { + var indexOfFirstDot = nodes.IndexOf("."); + if (indexOfFirstDot != -1) + { + nodes = nodes[(indexOfFirstDot + 1)..]; + } + + var installedNodesNames = new HashSet(); + var nameToManifestNodes = new Dictionary(); + + var packagePair = SelectedPackagePair; + + if (packagePair?.BasePackage.ExtensionManager is { } extensionManager) + { + var installedNodes = ( + await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage) + ).ToList(); + + var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( + extensionManager.GetManifests(packagePair.InstalledPackage) + ); + + // Add manifestExtensions definition to installedNodes if matching git repository url + installedNodes = installedNodes + .Select(installedNode => + { + if ( + installedNode.GitRepositoryUrl is not null + && manifestExtensionsMap.TryGetValue( + installedNode.GitRepositoryUrl, + out var manifestExtension + ) + ) + { + installedNode = installedNode with { Definition = manifestExtension }; + } + + return installedNode; + }) + .ToList(); + + // There may be duplicate titles, deduplicate by using the first one + nameToManifestNodes = manifestExtensionsMap + .GroupBy(x => x.Value.Title) + .ToDictionary(x => x.Key, x => x.First().Value); + + installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet(); + } + + var sections = new List(); + OpenArtCustomNode? currentSection = null; + + foreach (var node in nodes) + { + if (node is "." or ",") + { + currentSection = null; // End of the current section + continue; + } + + if (currentSection == null) + { + currentSection = new OpenArtCustomNode + { + Title = node, + IsInstalled = installedNodesNames.Contains(node) + }; + + // Add missing nodes to the list + if ( + !currentSection.IsInstalled && nameToManifestNodes.TryGetValue(node, out var manifestNode) + ) + { + MissingNodes.Add(manifestNode); + } + + sections.Add(currentSection); + } + else + { + currentSection.Children.Add(node); + } + } + + if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null) + { + sections = sections.Where(x => x.Title != "ComfyUI").ToList(); + } + + return sections; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs new file mode 100644 index 00000000..b765c797 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Api.OpenArt; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(InstalledWorkflowsPage))] +[Singleton] +public partial class InstalledWorkflowsViewModel( + ISettingsManager settingsManager, + INotificationService notificationService +) : TabViewModelBase, IDisposable +{ + public override string Header => Resources.TabLabel_InstalledWorkflows; + + private readonly SourceCache workflowsCache = + new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString()); + + [ObservableProperty] + private IObservableCollection displayedWorkflows = + new ObservableCollectionExtended(); + + protected override async Task OnInitialLoadedAsync() + { + await base.OnInitialLoadedAsync(); + + workflowsCache.Connect().DeferUntilLoaded().Bind(DisplayedWorkflows).Subscribe(); + + if (Design.IsDesignMode) + return; + + await LoadInstalledWorkflowsAsync(); + EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled; + } + + [RelayCommand] + private async Task LoadInstalledWorkflowsAsync() + { + workflowsCache.Clear(); + + foreach ( + var workflowPath in Directory.EnumerateFiles( + settingsManager.WorkflowDirectory, + "*.json", + SearchOption.AllDirectories + ) + ) + { + try + { + var json = await File.ReadAllTextAsync(workflowPath); + var metadata = JsonSerializer.Deserialize(json); + + if (metadata?.Workflow == null) + { + metadata = new OpenArtMetadata + { + Workflow = new OpenArtSearchResult + { + Id = Guid.NewGuid().ToString(), + Name = Path.GetFileNameWithoutExtension(workflowPath) + } + }; + } + + metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)]; + workflowsCache.AddOrUpdate(metadata); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + [RelayCommand] + private async Task OpenInExplorer(OpenArtMetadata metadata) + { + if (metadata.FilePath == null) + return; + + var path = metadata.FilePath.FirstOrDefault()?.Path.ToString(); + if (string.IsNullOrWhiteSpace(path)) + return; + + await ProcessRunner.OpenFileBrowser(path); + } + + [RelayCommand] + private void OpenOnOpenArt(OpenArtMetadata metadata) + { + if (metadata.Workflow == null) + return; + + ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}"); + } + + [RelayCommand] + private async Task DeleteAsync(OpenArtMetadata metadata) + { + var confirmationDialog = new BetterContentDialog + { + Title = Resources.Label_AreYouSure, + Content = Resources.Label_ActionCannotBeUndone, + PrimaryButtonText = Resources.Action_Delete, + SecondaryButtonText = Resources.Action_Cancel, + DefaultButton = ContentDialogButton.Primary, + IsSecondaryButtonEnabled = true, + }; + var dialogResult = await confirmationDialog.ShowAsync(); + if (dialogResult != ContentDialogResult.Primary) + return; + + await using var delay = new MinimumDelay(200, 500); + + var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", ""); + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + await notificationService.TryAsync( + Task.Run(() => File.Delete(path)), + message: "Error deleting workflow" + ); + + var id = metadata?.Workflow?.Id; + if (!string.IsNullOrWhiteSpace(id)) + { + workflowsCache.Remove(id); + } + } + + notificationService.Show( + Resources.Label_WorkflowDeleted, + string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name) + ); + } + + private void OnWorkflowInstalled(object? sender, EventArgs e) + { + LoadInstalledWorkflowsAsync().SafeFireAndForget(); + } + + public void Dispose() + { + workflowsCache.Dispose(); + EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs new file mode 100644 index 00000000..de438ecc --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls.Notifications; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using Refit; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models.Api.OpenArt; +using StabilityMatrix.Core.Models.PackageModification; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; +using Resources = StabilityMatrix.Avalonia.Languages.Resources; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(OpenArtBrowserPage))] +[Singleton] +public partial class OpenArtBrowserViewModel( + IOpenArtApi openArtApi, + INotificationService notificationService, + ISettingsManager settingsManager, + IPackageFactory packageFactory, + ServiceManager vmFactory +) : TabViewModelBase, IInfinitelyScroll +{ + private const int PageSize = 20; + + public override string Header => Resources.Label_OpenArtBrowser; + + private readonly SourceCache searchResultsCache = new(x => x.Id); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))] + private OpenArtSearchResponse? latestSearchResponse; + + [ObservableProperty] + private IObservableCollection searchResults = + new ObservableCollectionExtended(); + + [ObservableProperty] + private string searchQuery = string.Empty; + + [ObservableProperty] + private bool isLoading; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))] + private int displayedPageNumber = 1; + + public int InternalPageNumber => DisplayedPageNumber - 1; + + public int PageCount => + Math.Max( + 1, + Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize))) + ); + + public bool CanGoBack => + string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0; + + public bool CanGoForward => + !string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1; + + public bool CanGoToEnd => + string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1; + + public IEnumerable AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"]; + + [ObservableProperty] + private string? selectedSortMode; + + protected override void OnInitialLoaded() + { + searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe(); + SelectedSortMode = AllSortModes.First(); + } + + [RelayCommand] + private async Task FirstPage() + { + DisplayedPageNumber = 1; + searchResultsCache.Clear(); + + await DoSearch(); + } + + [RelayCommand] + private async Task PreviousPage() + { + DisplayedPageNumber--; + searchResultsCache.Clear(); + + await DoSearch(InternalPageNumber); + } + + [RelayCommand] + private async Task NextPage() + { + if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + DisplayedPageNumber++; + } + + searchResultsCache.Clear(); + await DoSearch(InternalPageNumber); + } + + [RelayCommand] + private async Task LastPage() + { + if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + DisplayedPageNumber = PageCount; + } + + searchResultsCache.Clear(); + await DoSearch(PageCount - 1); + } + + [Localizable(false)] + [RelayCommand] + private void OpenModel(OpenArtSearchResult workflow) + { + ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); + } + + [RelayCommand] + private async Task SearchButton() + { + DisplayedPageNumber = 1; + LatestSearchResponse = null; + searchResultsCache.Clear(); + + await DoSearch(); + } + + [RelayCommand] + private async Task OpenWorkflow(OpenArtSearchResult workflow) + { + var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow }; + + var dialog = new BetterContentDialog + { + IsPrimaryButtonEnabled = true, + IsSecondaryButtonEnabled = true, + PrimaryButtonText = Resources.Action_Import, + SecondaryButtonText = Resources.Action_Cancel, + DefaultButton = ContentDialogButton.Primary, + IsFooterVisible = true, + MaxDialogWidth = 750, + MaxDialogHeight = 850, + CloseOnClickOutside = true, + Content = vm + }; + + var result = await dialog.ShowAsync(); + + if (result != ContentDialogResult.Primary) + return; + + List steps = + [ + new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) + ]; + + // Add install steps if missing nodes and preferred + if ( + vm is + { + InstallRequiredNodes: true, + MissingNodes: { Count: > 0 } missingNodes, + SelectedPackage: not null, + SelectedPackagePair: not null + } + ) + { + var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!; + + steps.AddRange( + missingNodes.Select( + extension => + new InstallExtensionStep( + extensionManager, + vm.SelectedPackagePair.InstalledPackage, + extension + ) + ) + ); + } + + var runner = new PackageModificationRunner + { + ShowDialogOnStart = true, + ModificationCompleteTitle = Resources.Label_WorkflowImported, + ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow + }; + EventManager.Instance.OnPackageInstallProgressAdded(runner); + + await runner.ExecuteSteps(steps); + + notificationService.Show( + Resources.Label_WorkflowImported, + Resources.Label_WorkflowImportComplete, + NotificationType.Success + ); + + EventManager.Instance.OnWorkflowInstalled(); + } + + [RelayCommand] + private void OpenOnOpenArt(OpenArtSearchResult? workflow) + { + if (workflow?.Id == null) + return; + + ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); + } + + private async Task DoSearch(int page = 0) + { + IsLoading = true; + + try + { + OpenArtSearchResponse? response = null; + if (string.IsNullOrWhiteSpace(SearchQuery)) + { + var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; + if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + request.Cursor = LatestSearchResponse.NextCursor; + } + + response = await openArtApi.GetFeedAsync(request); + } + else + { + response = await openArtApi.SearchAsync( + new OpenArtSearchRequest + { + Keyword = SearchQuery, + PageSize = PageSize, + CurrentPage = page + } + ); + } + + foreach (var item in response.Items) + { + searchResultsCache.AddOrUpdate(item); + } + + LatestSearchResponse = response; + } + catch (ApiException e) + { + notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message); + } + finally + { + IsLoading = false; + } + } + + partial void OnSelectedSortModeChanged(string? value) + { + if (value is null || SearchResults.Count == 0) + return; + + searchResultsCache.Clear(); + LatestSearchResponse = null; + + DoSearch().SafeFireAndForget(); + } + + public async Task LoadNextPageAsync() + { + if (!CanGoForward) + return; + + try + { + OpenArtSearchResponse? response = null; + if (string.IsNullOrWhiteSpace(SearchQuery)) + { + var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; + if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + request.Cursor = LatestSearchResponse.NextCursor; + } + + response = await openArtApi.GetFeedAsync(request); + } + else + { + DisplayedPageNumber++; + response = await openArtApi.SearchAsync( + new OpenArtSearchRequest + { + Keyword = SearchQuery, + PageSize = PageSize, + CurrentPage = InternalPageNumber + } + ); + } + + foreach (var item in response.Items) + { + searchResultsCache.AddOrUpdate(item); + } + + LatestSearchResponse = response; + } + catch (ApiException e) + { + notificationService.Show("Unable to load the next page", e.Message); + } + } + + private static string GetSortMode(string? sortMode) + { + return sortMode switch + { + "Trending" => "trending", + "Latest" => "latest", + "Most Downloaded" => "most_downloaded", + "Most Liked" => "most_liked", + _ => "trending" + }; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index 8fdfe91d..0da8ab4c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -130,6 +130,9 @@ public partial class MainSettingsViewModel : PageViewModelBase [ObservableProperty] private HolidayMode holidayModeSetting; + [ObservableProperty] + private bool infinitelyScrollWorkflowBrowser; + #region System Info private static Lazy> GpuInfosLazy { get; } = @@ -218,6 +221,13 @@ public partial class MainSettingsViewModel : PageViewModelBase settings => settings.HolidayModeSetting ); + settingsManager.RelayPropertyFor( + this, + vm => vm.InfinitelyScrollWorkflowBrowser, + settings => settings.IsWorkflowInfiniteScrollEnabled, + true + ); + DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick; diff --git a/StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs new file mode 100644 index 00000000..3db3dabb --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(WorkflowsPage))] +[Singleton] +public partial class WorkflowsPageViewModel : PageViewModelBase +{ + public override string Title => Resources.Label_Workflows; + public override IconSource IconSource => new FASymbolIconSource { Symbol = "fa-solid fa-circle-nodes" }; + + public IReadOnlyList Pages { get; } + + [ObservableProperty] + private TabItem? selectedPage; + + /// + public WorkflowsPageViewModel( + OpenArtBrowserViewModel openArtBrowserViewModel, + InstalledWorkflowsViewModel installedWorkflowsViewModel + ) + { + Pages = new List( + new List([openArtBrowserViewModel, installedWorkflowsViewModel]).Select( + vm => new TabItem { Header = vm.Header, Content = vm } + ) + ); + SelectedPage = Pages.FirstOrDefault(); + } +} diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml new file mode 100644 index 00000000..f8a7d583 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/InstalledWorkflowsPage.axaml b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml new file mode 100644 index 00000000..e913a98a --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs new file mode 100644 index 00000000..191916d2 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class InstalledWorkflowsPage : UserControlBase +{ + public InstalledWorkflowsPage() + { + InitializeComponent(); + } +} diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml new file mode 100644 index 00000000..867d2a62 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -0,0 +1,357 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs new file mode 100644 index 00000000..b6a615f3 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs @@ -0,0 +1,41 @@ +using System; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class OpenArtBrowserPage : UserControlBase +{ + private readonly ISettingsManager settingsManager; + + public OpenArtBrowserPage(ISettingsManager settingsManager) + { + this.settingsManager = settingsManager; + InitializeComponent(); + } + + private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) + { + if (sender is not ScrollViewer scrollViewer) + return; + + if (scrollViewer.Offset.Y == 0) + return; + + var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; + + if ( + isAtEnd + && settingsManager.Settings.IsWorkflowInfiniteScrollEnabled + && DataContext is IInfinitelyScroll scroll + ) + { + scroll.LoadNextPageAsync().SafeFireAndForget(); + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml index 369c3f9a..5fb2d8a4 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml @@ -45,77 +45,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -