diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 4a11a99e..3fc6455b 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -932,12 +932,20 @@ The gallery images are often inpainted, but you will get something very similar { new() { - Name = "Test Workflow", - Creator = "Test Creator", - ThumbnailUrls = - [ - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512" - ] + 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" + ) + } + ] + } } }; diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 5dd40cc8..388037ce 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -1319,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!. /// @@ -1355,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. /// @@ -2606,6 +2624,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// 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. /// @@ -2615,6 +2651,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// 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. /// @@ -2705,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 368e2867..4c70a20c 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -1047,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/OpenArtMetadata.cs b/StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs index 72d09658..7620816c 100644 --- a/StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs +++ b/StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs @@ -2,27 +2,21 @@ using System.Linq; using System.Text.Json.Serialization; using Avalonia.Platform.Storage; -using StabilityMatrix.Avalonia; +using StabilityMatrix.Core.Models.Api.OpenArt; -namespace StabilityMatrix.Core.Models.Api.OpenArt; +namespace StabilityMatrix.Avalonia.Models; public class OpenArtMetadata { - [JsonPropertyName("workflow_id")] - public string? Id { get; set; } - - [JsonPropertyName("workflow_name")] - public string? Name { get; set; } - - [JsonPropertyName("creator")] - public string? Creator { get; set; } - - [JsonPropertyName("thumbnails")] - public IEnumerable? ThumbnailUrls { get; set; } + [JsonPropertyName("sm_workflow_data")] + public OpenArtSearchResult? Workflow { get; set; } [JsonIgnore] - public string? FirstThumbnail => ThumbnailUrls?.FirstOrDefault(); + 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/ViewModels/InstalledWorkflowsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs index 8a4c0e87..b765c797 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs @@ -1,28 +1,41 @@ 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) : TabViewModelBase +public partial class InstalledWorkflowsViewModel( + ISettingsManager settingsManager, + INotificationService notificationService +) : TabViewModelBase, IDisposable { - public override string Header => "Installed Workflows"; + public override string Header => Resources.TabLabel_InstalledWorkflows; - private readonly SourceCache workflowsCache = new(x => x.Id); + private readonly SourceCache workflowsCache = + new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString()); [ObservableProperty] private IObservableCollection displayedWorkflows = @@ -38,6 +51,7 @@ public partial class InstalledWorkflowsViewModel(ISettingsManager settingsManage return; await LoadInstalledWorkflowsAsync(); + EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled; } [RelayCommand] @@ -58,12 +72,15 @@ public partial class InstalledWorkflowsViewModel(ISettingsManager settingsManage var json = await File.ReadAllTextAsync(workflowPath); var metadata = JsonSerializer.Deserialize(json); - if (metadata?.Id == null) + if (metadata?.Workflow == null) { metadata = new OpenArtMetadata { - Id = Guid.NewGuid().ToString(), - Name = Path.GetFileNameWithoutExtension(workflowPath) + Workflow = new OpenArtSearchResult + { + Id = Guid.NewGuid().ToString(), + Name = Path.GetFileNameWithoutExtension(workflowPath) + } }; } @@ -76,4 +93,76 @@ public partial class InstalledWorkflowsViewModel(ISettingsManager settingsManage } } } + + [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 index b1dbd39a..1b7ef2f1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -12,6 +12,7 @@ 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; @@ -182,10 +183,7 @@ public partial class OpenArtBrowserViewModel( if (existingComfy == null || comfyPair == null) { - notificationService.Show( - Resources.Label_ComfyRequiredTitle, - "ComfyUI is required to import workflows from OpenArt" - ); + notificationService.Show(Resources.Label_ComfyRequiredTitle, Resources.Label_ComfyRequiredDetail); return; } @@ -210,18 +208,29 @@ public partial class OpenArtBrowserViewModel( var runner = new PackageModificationRunner { ShowDialogOnStart = true, - ModificationCompleteTitle = "Workflow Imported", - ModificationCompleteMessage = "Finished importing workflow and custom nodes" + ModificationCompleteTitle = Resources.Label_WorkflowImported, + ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); notificationService.Show( - "Workflow Imported", - "The workflow and custom nodes have been imported.", + 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) @@ -262,7 +271,7 @@ public partial class OpenArtBrowserViewModel( } catch (ApiException e) { - notificationService.Show("Error retrieving workflows", e.Message); + notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message); } finally { diff --git a/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml index 5c4d3b4b..7e569068 100644 --- a/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml @@ -10,6 +10,11 @@ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:avalonia="https://github.com/projektanker/icons.avalonia" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" + xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" + xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" + xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent" + xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" d:DataContext="{x:Static designData:DesignData.InstalledWorkflowsViewModel}" x:DataType="viewModels:InstalledWorkflowsViewModel" @@ -22,7 +27,7 @@ - + + + + + + - + - + + Margin="8,0" /> + VerticalAlignment="Center" /> @@ -96,7 +113,7 @@ - + + + + + + + + + + + + + + + + + + + + + Text="{Binding Workflow.Name}" + ToolTip.Tip="{Binding Workflow.Name}" /> diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml index 2d377dd1..867d2a62 100644 --- a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -12,6 +12,7 @@ xmlns:avalonia="https://github.com/projektanker/icons.avalonia" xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" x:DataType="viewModels:OpenArtBrowserViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="StabilityMatrix.Avalonia.Views.OpenArtBrowserPage"> @@ -23,7 +24,7 @@ -