From 2998ceee2122c17630e633fc611dec9df8fec6f2 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 10 Feb 2024 22:35:50 -0800 Subject: [PATCH 01/18] Added models & refit interface for openart stuff --- StabilityMatrix.Core/Api/IOpenArtApi.cs | 17 ++++++++ .../Models/Api/OpenArt/NodesCount.cs | 15 +++++++ .../Models/Api/OpenArt/OpenArtCreator.cs | 24 ++++++++++++ .../Api/OpenArt/OpenArtDownloadRequest.cs | 15 +++++++ .../Api/OpenArt/OpenArtDownloadResponse.cs | 12 ++++++ .../Models/Api/OpenArt/OpenArtFeedRequest.cs | 18 +++++++++ .../Api/OpenArt/OpenArtSearchRequest.cs | 18 +++++++++ .../Api/OpenArt/OpenArtSearchResponse.cs | 15 +++++++ .../Models/Api/OpenArt/OpenArtSearchResult.cs | 39 +++++++++++++++++++ .../Models/Api/OpenArt/OpenArtStats.cs | 33 ++++++++++++++++ .../Models/Api/OpenArt/OpenArtThumbnail.cs | 15 +++++++ 11 files changed, 221 insertions(+) create mode 100644 StabilityMatrix.Core/Api/IOpenArtApi.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs diff --git a/StabilityMatrix.Core/Api/IOpenArtApi.cs b/StabilityMatrix.Core/Api/IOpenArtApi.cs new file mode 100644 index 00000000..e13c8623 --- /dev/null +++ b/StabilityMatrix.Core/Api/IOpenArtApi.cs @@ -0,0 +1,17 @@ +using Refit; +using StabilityMatrix.Core.Models.Api.OpenArt; + +namespace StabilityMatrix.Core.Api; + +[Headers("User-Agent: StabilityMatrix")] +public interface IOpenArtApi +{ + [Get("/feed")] + Task GetFeedAsync([Query] OpenArtFeedRequest request); + + [Get("/list")] + Task SearchAsync([Query] OpenArtFeedRequest request); + + [Post("/download")] + Task DownloadWorkflowAsync([Body] OpenArtDownloadRequest request); +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs b/StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs new file mode 100644 index 00000000..2509dcd7 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class NodesCount +{ + [JsonPropertyName("total")] + public long Total { get; set; } + + [JsonPropertyName("primitive")] + public long Primitive { get; set; } + + [JsonPropertyName("custom")] + public long Custom { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs new file mode 100644 index 00000000..1f16dc14 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtCreator +{ + [JsonPropertyName("uid")] + public string Uid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("bio")] + public string Bio { get; set; } + + [JsonPropertyName("avatar")] + public Uri Avatar { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("dev_profile_url")] + public Uri DevProfileUrl { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs new file mode 100644 index 00000000..cdf07d27 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using Refit; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtDownloadRequest +{ + [AliasAs("workflow_id")] + [JsonPropertyName("workflow_id")] + public required string WorkflowId { get; set; } + + [AliasAs("version_tag")] + [JsonPropertyName("version_tag")] + public string VersionTag { get; set; } = "latest"; +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs new file mode 100644 index 00000000..3cb61d79 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtDownloadResponse +{ + [JsonPropertyName("filename")] + public string Filename { get; set; } + + [JsonPropertyName("payload")] + public string Payload { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs new file mode 100644 index 00000000..a3f334d9 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs @@ -0,0 +1,18 @@ +using Refit; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtFeedRequest +{ + [AliasAs("category")] + public string Category { get; set; } + + [AliasAs("sort")] + public string Sort { get; set; } + + [AliasAs("custom_node")] + public string CustomNode { get; set; } + + [AliasAs("cursor")] + public string Cursor { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs new file mode 100644 index 00000000..27d944e3 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs @@ -0,0 +1,18 @@ +using Refit; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtSearchRequest +{ + [AliasAs("keyword")] + public required string Keyword { get; set; } + + [AliasAs("pageSize")] + public int PageSize { get; set; } = 30; + + /// + /// 0-based index of the page to retrieve + /// + [AliasAs("currentPage")] + public int CurrentPage { get; set; } = 0; +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs new file mode 100644 index 00000000..c58e308a --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtSearchResponse +{ + [JsonPropertyName("items")] + public IEnumerable Items { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs new file mode 100644 index 00000000..7db9bb4a --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtSearchResult +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("creator")] + public OpenArtCreator Creator { get; set; } + + [JsonPropertyName("updated_at")] + public DateTimeOffset UpdatedAt { get; set; } + + [JsonPropertyName("stats")] + public OpenArtStats Stats { get; set; } + + [JsonPropertyName("nodes_index")] + public IEnumerable NodesIndex { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("categories")] + public IEnumerable Categories { get; set; } + + [JsonPropertyName("thumbnails")] + public IEnumerable Thumbnails { get; set; } + + [JsonPropertyName("nodes_count")] + public NodesCount NodesCount { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs new file mode 100644 index 00000000..381db723 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtStats +{ + [JsonPropertyName("num_shares")] + public int NumShares { get; set; } + + [JsonPropertyName("num_bookmarks")] + public int NumBookmarks { get; set; } + + [JsonPropertyName("num_reviews")] + public int NumReviews { get; set; } + + [JsonPropertyName("rating")] + public int Rating { get; set; } + + [JsonPropertyName("num_comments")] + public int NumComments { get; set; } + + [JsonPropertyName("num_likes")] + public int NumLikes { get; set; } + + [JsonPropertyName("num_downloads")] + public int NumDownloads { get; set; } + + [JsonPropertyName("num_runs")] + public int NumRuns { get; set; } + + [JsonPropertyName("num_views")] + public int NumViews { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs new file mode 100644 index 00000000..05bfc22f --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtThumbnail +{ + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("url")] + public Uri Url { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } +} From c26960ce1f8457e6ed228dc6e4c687734c03c95c Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 11 Feb 2024 18:35:51 -0800 Subject: [PATCH 02/18] added basic card UI, searching, and infiniscroll --- Avalonia.Gif/Avalonia.Gif.csproj | 2 +- ...tabilityMatrix.Avalonia.Diagnostics.csproj | 4 +- StabilityMatrix.Avalonia/App.axaml.cs | 16 +- .../Models/IInfinitelyScroll.cs | 8 + .../StabilityMatrix.Avalonia.csproj | 15 +- .../ViewModels/OpenArtBrowserViewModel.cs | 175 +++++++++ .../Views/OpenArtBrowserPage.axaml | 340 ++++++++++++++++++ .../Views/OpenArtBrowserPage.axaml.cs | 32 ++ StabilityMatrix.Core/Api/IOpenArtApi.cs | 2 +- .../Models/Api/OpenArt/OpenArtCreator.cs | 2 +- .../Models/Api/OpenArt/OpenArtDateTime.cs | 14 + .../Models/Api/OpenArt/OpenArtFeedRequest.cs | 3 + .../Models/Api/OpenArt/OpenArtSearchResult.cs | 2 +- .../Models/Api/OpenArt/OpenArtStats.cs | 2 +- .../Models/Packages/SDWebForge.cs | 13 + .../StabilityMatrix.UITests.csproj | 2 +- 16 files changed, 614 insertions(+), 18 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs create mode 100644 StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs diff --git a/Avalonia.Gif/Avalonia.Gif.csproj b/Avalonia.Gif/Avalonia.Gif.csproj index 858b974e..6a363589 100644 --- a/Avalonia.Gif/Avalonia.Gif.csproj +++ b/Avalonia.Gif/Avalonia.Gif.csproj @@ -10,7 +10,7 @@ true - + diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj index e635ad2f..71efd8bb 100644 --- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj +++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index f1c10ddd..05e54065 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -333,7 +333,8 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService() }, FooterPages = { provider.GetRequiredService() } } @@ -556,7 +557,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); @@ -565,7 +566,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); @@ -583,6 +584,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/Models/IInfinitelyScroll.cs b/StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs new file mode 100644 index 00000000..59373816 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace StabilityMatrix.Avalonia.Models; + +public interface IInfinitelyScroll +{ + Task LoadNextPageAsync(); +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 1190f686..63e08450 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -39,13 +39,14 @@ - + + - - - + + + - + @@ -65,8 +66,8 @@ - - + + diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs new file mode 100644 index 00000000..c8463037 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using Refit; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models.Api.OpenArt; +using StabilityMatrix.Core.Processes; +using Symbol = FluentIcons.Common.Symbol; +using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(OpenArtBrowserPage))] +[Singleton] +public partial class OpenArtBrowserViewModel(IOpenArtApi openArtApi, INotificationService notificationService) + : PageViewModelBase, + IInfinitelyScroll +{ + private const int PageSize = 20; + + public override string Title => "Workflows"; + public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Whiteboard }; + + private 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 => InternalPageNumber > 0; + + public bool CanGoForward => PageCount > InternalPageNumber + 1; + + public bool CanGoToEnd => PageCount > InternalPageNumber + 1; + + protected override void OnInitialLoaded() + { + searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe(); + } + + public override async Task OnLoadedAsync() + { + if (SearchResults.Any()) + return; + + await DoSearch(); + } + + [RelayCommand] + private async Task FirstPage() + { + DisplayedPageNumber = 1; + await DoSearch(); + } + + [RelayCommand] + private async Task PreviousPage() + { + DisplayedPageNumber--; + await DoSearch(InternalPageNumber); + } + + [RelayCommand] + private async Task NextPage() + { + DisplayedPageNumber++; + await DoSearch(InternalPageNumber); + } + + [RelayCommand] + private async Task LastPage() + { + DisplayedPageNumber = PageCount; + 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; + await DoSearch(); + } + + private async Task DoSearch(int page = 0) + { + IsLoading = true; + + try + { + var response = await openArtApi.SearchAsync( + new OpenArtSearchRequest + { + Keyword = SearchQuery, + PageSize = PageSize, + CurrentPage = page + } + ); + + searchResultsCache.EditDiff(response.Items, (a, b) => a.Id == b.Id); + LatestSearchResponse = response; + } + catch (ApiException e) + { + notificationService.Show("Error retrieving workflows", e.Message); + } + finally + { + IsLoading = false; + } + } + + public async Task LoadNextPageAsync() + { + try + { + DisplayedPageNumber++; + var response = await openArtApi.SearchAsync( + new OpenArtSearchRequest + { + Keyword = SearchQuery, + PageSize = PageSize, + CurrentPage = InternalPageNumber + } + ); + + searchResultsCache.AddOrUpdate(response.Items); + LatestSearchResponse = response; + } + catch (ApiException e) + { + notificationService.Show("Unable to load the next page", e.Message); + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml new file mode 100644 index 00000000..8e7a621e --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs new file mode 100644 index 00000000..aceb5db4 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs @@ -0,0 +1,32 @@ +using System; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class OpenArtBrowserPage : UserControlBase +{ + public OpenArtBrowserPage() + { + 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) < 0.1f; + if (isAtEnd && DataContext is IInfinitelyScroll scroll) + { + scroll.LoadNextPageAsync().SafeFireAndForget(); + } + } +} diff --git a/StabilityMatrix.Core/Api/IOpenArtApi.cs b/StabilityMatrix.Core/Api/IOpenArtApi.cs index e13c8623..f7e9ae19 100644 --- a/StabilityMatrix.Core/Api/IOpenArtApi.cs +++ b/StabilityMatrix.Core/Api/IOpenArtApi.cs @@ -10,7 +10,7 @@ public interface IOpenArtApi Task GetFeedAsync([Query] OpenArtFeedRequest request); [Get("/list")] - Task SearchAsync([Query] OpenArtFeedRequest request); + Task SearchAsync([Query] OpenArtSearchRequest request); [Post("/download")] Task DownloadWorkflowAsync([Body] OpenArtDownloadRequest request); diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs index 1f16dc14..173d1cef 100644 --- a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs @@ -20,5 +20,5 @@ public class OpenArtCreator public string Username { get; set; } [JsonPropertyName("dev_profile_url")] - public Uri DevProfileUrl { get; set; } + public string DevProfileUrl { get; set; } } diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs new file mode 100644 index 00000000..5f45a232 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api.OpenArt; + +public class OpenArtDateTime +{ + [JsonPropertyName("_seconds")] + public long Seconds { get; set; } + + public DateTimeOffset ToDateTimeOffset() + { + return DateTimeOffset.FromUnixTimeSeconds(Seconds); + } +} diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs index a3f334d9..f8dd5255 100644 --- a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs @@ -2,6 +2,9 @@ namespace StabilityMatrix.Core.Models.Api.OpenArt; +/// +/// Note that parameters Category, Custom Node and Sort should be used separately +/// public class OpenArtFeedRequest { [AliasAs("category")] diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs index 7db9bb4a..9b007ced 100644 --- a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs @@ -32,7 +32,7 @@ public class OpenArtSearchResult public IEnumerable Categories { get; set; } [JsonPropertyName("thumbnails")] - public IEnumerable Thumbnails { get; set; } + public List Thumbnails { get; set; } [JsonPropertyName("nodes_count")] public NodesCount NodesCount { get; set; } diff --git a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs index 381db723..4afa6118 100644 --- a/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs +++ b/StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs @@ -14,7 +14,7 @@ public class OpenArtStats public int NumReviews { get; set; } [JsonPropertyName("rating")] - public int Rating { get; set; } + public double Rating { get; set; } [JsonPropertyName("num_comments")] public int NumComments { get; set; } diff --git a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs index 1e6fd16a..b58c7ec8 100644 --- a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs +++ b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs @@ -79,6 +79,19 @@ public class SDWebForge( TorchVersion.Mps }; + public override Dictionary> SharedOutputFolders => + new() + { + [SharedOutputType.Extras] = new[] { "output/extras-images" }, + [SharedOutputType.Saved] = new[] { "log/images" }, + [SharedOutputType.Img2Img] = new[] { "output/img2img-images" }, + [SharedOutputType.Text2Img] = new[] { "output/txt2img-images" }, + [SharedOutputType.Img2ImgGrids] = new[] { "output/img2img-grids" }, + [SharedOutputType.Text2ImgGrids] = new[] { "output/txt2img-grids" } + }; + + public override string OutputFolderName => "output"; + public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, diff --git a/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj b/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj index 90484174..47cee6c9 100644 --- a/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj +++ b/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj @@ -15,7 +15,7 @@ - + From e87b5032beb11e3d2d57cddbe99de5ee7aa5d48c Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 11 Feb 2024 19:24:44 -0800 Subject: [PATCH 03/18] rearrange settings & add System.Drawing.Common reference? --- .../StabilityMatrix.Avalonia.csproj | 1 + .../ViewModels/OpenArtBrowserViewModel.cs | 1 - .../Settings/MainSettingsViewModel.cs | 9 + .../Views/OpenArtBrowserPage.axaml.cs | 2 +- .../Views/Settings/MainSettingsPage.axaml | 188 +++++++++--------- .../Models/Settings/Settings.cs | 1 + .../StabilityMatrix.UITests.csproj | 1 + 7 files changed, 107 insertions(+), 96 deletions(-) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 63e08450..fa070fbd 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -95,6 +95,7 @@ + diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs index c8463037..6f290e70 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index 55b6c896..2a28e275 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -129,6 +129,9 @@ public partial class MainSettingsViewModel : PageViewModelBase [ObservableProperty] private HolidayMode holidayModeSetting; + [ObservableProperty] + private bool infinitelyScrollWorkflowBrowser; + #region System Info private static Lazy> GpuInfosLazy { get; } = @@ -217,6 +220,12 @@ public partial class MainSettingsViewModel : PageViewModelBase settings => settings.HolidayModeSetting ); + settingsManager.RelayPropertyFor( + this, + vm => vm.InfinitelyScrollWorkflowBrowser, + settings => settings.IsWorkflowInfiniteScrollEnabled + ); + DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick; diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs index aceb5db4..c80793c2 100644 --- a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs @@ -23,7 +23,7 @@ public partial class OpenArtBrowserPage : UserControlBase if (scrollViewer.Offset.Y == 0) return; - var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 0.1f; + var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; if (isAtEnd && 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..2b846cc0 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml @@ -45,77 +45,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + 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 index 75a6f54c..5f776195 100644 --- a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -82,7 +82,7 @@ CornerRadius="8"> - + @@ -243,13 +243,8 @@ - - - + Margin="8,8,8,0"> + + diff --git a/StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs new file mode 100644 index 00000000..443070e9 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class WorkflowsPage : UserControlBase +{ + public WorkflowsPage() + { + InitializeComponent(); + } +} diff --git a/StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs b/StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs index dbfa17f9..42553180 100644 --- a/StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs @@ -24,6 +24,14 @@ public class DownloadOpenArtWorkflowStep( var jsonObject = JsonNode.Parse(workflowData.Payload) as JsonObject; jsonObject?.Add("workflow_id", workflow.Id); + jsonObject?.Add("workflow_name", workflow.Name); + jsonObject?.Add("creator", workflow.Creator.Username); + var thumbs = new JsonArray(); + foreach (var thumb in workflow.Thumbnails) + { + thumbs.Add(thumb.Url); + } + jsonObject?.Add("thumbnails", thumbs); await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(jsonObject)).ConfigureAwait(false); From fbc8111952d2960f34253be2276acef27158c3b8 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 23 Feb 2024 00:56:32 -0800 Subject: [PATCH 09/18] call feed endpoint if no search query & added sort for feed --- .../ViewModels/OpenArtBrowserViewModel.cs | 127 +++++++++++++++--- .../Views/InstalledWorkflowsPage.axaml | 9 +- .../Views/OpenArtBrowserPage.axaml | 15 ++- 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs index bb744ff2..e7d54266 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -1,7 +1,9 @@ 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; @@ -69,21 +71,32 @@ public partial class OpenArtBrowserViewModel( Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize))) ); - public bool CanGoBack => InternalPageNumber > 0; + public bool CanGoBack => + string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0; - public bool CanGoForward => PageCount > InternalPageNumber + 1; + public bool CanGoForward => + !string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1; - public bool CanGoToEnd => 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(); } @@ -91,20 +104,32 @@ public partial class OpenArtBrowserViewModel( private async Task PreviousPage() { DisplayedPageNumber--; + searchResultsCache.Clear(); + await DoSearch(InternalPageNumber); } [RelayCommand] private async Task NextPage() { - DisplayedPageNumber++; + if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + DisplayedPageNumber++; + } + + searchResultsCache.Clear(); await DoSearch(InternalPageNumber); } [RelayCommand] private async Task LastPage() { - DisplayedPageNumber = PageCount; + if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) + { + DisplayedPageNumber = PageCount; + } + + searchResultsCache.Clear(); await DoSearch(PageCount - 1); } @@ -119,6 +144,9 @@ public partial class OpenArtBrowserViewModel( private async Task SearchButton() { DisplayedPageNumber = 1; + LatestSearchResponse = null; + searchResultsCache.Clear(); + await DoSearch(); } @@ -202,16 +230,34 @@ public partial class OpenArtBrowserViewModel( try { - var response = await openArtApi.SearchAsync( - new OpenArtSearchRequest + OpenArtSearchResponse? response = null; + if (string.IsNullOrWhiteSpace(SearchQuery)) + { + var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; + if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { - Keyword = SearchQuery, - PageSize = PageSize, - CurrentPage = page + request.Cursor = LatestSearchResponse.NextCursor; } - ); - searchResultsCache.EditDiff(response.Items, (a, b) => a.Id == b.Id); + 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) @@ -224,6 +270,17 @@ public partial class OpenArtBrowserViewModel( } } + 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) @@ -231,17 +288,35 @@ public partial class OpenArtBrowserViewModel( try { - DisplayedPageNumber++; - var response = await openArtApi.SearchAsync( - new OpenArtSearchRequest + OpenArtSearchResponse? response = null; + if (string.IsNullOrWhiteSpace(SearchQuery)) + { + var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; + if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { - Keyword = SearchQuery, - PageSize = PageSize, - CurrentPage = InternalPageNumber + request.Cursor = LatestSearchResponse.NextCursor; } - ); - searchResultsCache.AddOrUpdate(response.Items); + 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) @@ -249,4 +324,16 @@ public partial class OpenArtBrowserViewModel( 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/Views/InstalledWorkflowsPage.axaml b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml index 15eec425..5c4d3b4b 100644 --- a/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml @@ -78,12 +78,11 @@ - + + VerticalAlignment="Center"/> diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml index 5f776195..2d377dd1 100644 --- a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -243,8 +243,8 @@ - + + + + Date: Fri, 23 Feb 2024 00:57:55 -0800 Subject: [PATCH 10/18] remove windows references --- .../ViewModels/InstalledWorkflowsViewModel.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs index ad514b6c..8a4c0e87 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Platform.Storage; -using Avalonia.Xaml.Interactions.DragAndDrop; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; @@ -16,8 +13,6 @@ using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Services; -using Windows.Storage; -using IStorageFile = Avalonia.Platform.Storage.IStorageFile; namespace StabilityMatrix.Avalonia.ViewModels; From 92620dc8940b456551ea21912070f50a424af621 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 27 Feb 2024 15:39:34 -0500 Subject: [PATCH 11/18] Fix duplicate def --- StabilityMatrix.Core/Models/Packages/SDWebForge.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs index 3ff4b3cb..c603532c 100644 --- a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs +++ b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs @@ -104,19 +104,6 @@ public class SDWebForge( TorchVersion.Mps }; - public override Dictionary> SharedOutputFolders => - new() - { - [SharedOutputType.Extras] = new[] { "output/extras-images" }, - [SharedOutputType.Saved] = new[] { "log/images" }, - [SharedOutputType.Img2Img] = new[] { "output/img2img-images" }, - [SharedOutputType.Text2Img] = new[] { "output/txt2img-images" }, - [SharedOutputType.Img2ImgGrids] = new[] { "output/img2img-grids" }, - [SharedOutputType.Text2ImgGrids] = new[] { "output/txt2img-grids" } - }; - - public override string OutputFolderName => "output"; - public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, From df1f188fe60fa06147c8a302b9b25fdda03e52c0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 27 Feb 2024 15:39:51 -0500 Subject: [PATCH 12/18] Add GetInstalledExtensionsLiteAsync to interface --- .../Packages/Extensions/IPackageExtensionManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs b/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs index 76903233..20d37365 100644 --- a/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs +++ b/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs @@ -89,6 +89,14 @@ public interface IPackageExtensionManager CancellationToken cancellationToken = default ); + /// + /// Like , but does not check version. + /// + Task> GetInstalledExtensionsLiteAsync( + InstalledPackage installedPackage, + CancellationToken cancellationToken = default + ); + /// /// Install an extension to the provided package. /// From 0c30b4ba328e5853c2fadfaf3066d07f99380ab6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 27 Feb 2024 15:58:40 -0500 Subject: [PATCH 13/18] Fix openart extension install --- .../Dialogs/OpenArtWorkflowViewModel.cs | 58 ++++++++++++++++--- .../ViewModels/OpenArtBrowserViewModel.cs | 25 ++++---- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs index 869521b2..f565b896 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs @@ -11,6 +11,7 @@ using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.OpenArt; +using StabilityMatrix.Core.Models.Packages.Extensions; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -28,6 +29,8 @@ public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase [ObservableProperty] private string prunedDescription = string.Empty; + public List MissingNodes { get; } = []; + public override async Task OnLoadedAsync() { CustomNodes = new ObservableCollection( @@ -45,16 +48,44 @@ public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase nodes = nodes[(indexOfFirstDot + 1)..]; } - var installedNodes = new List(); - if (InstalledComfy != null) + var installedNodesNames = new HashSet(); + var nameToManifestNodes = new Dictionary(); + + if (InstalledComfy?.BasePackage.ExtensionManager is { } extensionManager) { - installedNodes = ( - await InstalledComfy.BasePackage.ExtensionManager?.GetInstalledExtensionsAsync( - InstalledComfy.InstalledPackage - ) - ) - .Select(x => x.PrimaryPath?.Name) + var installedNodes = ( + await extensionManager.GetInstalledExtensionsLiteAsync(InstalledComfy.InstalledPackage) + ).ToList(); + + var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( + extensionManager.GetManifests(InstalledComfy.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(); @@ -73,8 +104,17 @@ public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase currentSection = new OpenArtCustomNode { Title = node, - IsInstalled = installedNodes.Contains(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 diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs index e7d54266..cf9321c8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -23,7 +23,6 @@ using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Models.PackageModification; -using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using Resources = StabilityMatrix.Avalonia.Languages.Resources; @@ -189,23 +188,21 @@ public partial class OpenArtBrowserViewModel( return; } + if (vm.MissingNodes is not { Count: > 0 } missingNodes) + { + // Skip if no missing nodes + return; + } + var extensionManager = comfyPair.BasePackage.ExtensionManager!; - var extensions = ( - await extensionManager.GetManifestExtensionsAsync( - extensionManager.GetManifests(comfyPair.InstalledPackage) - ) - ).ToList(); - var steps = vm.CustomNodes.Where(x => x.IsInstalled is false) - .Select(node => extensions.FirstOrDefault(x => x.Title == node.Title)) - .OfType() - .Select( + List steps = + [ + new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager), + ..missingNodes.Select( extension => new InstallExtensionStep(extensionManager, comfyPair.InstalledPackage, extension) ) - .Cast() - .ToList(); - - steps.Add(new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager)); + ]; var runner = new PackageModificationRunner { From e62546691af605c368133c9ed42f678a1e633827 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 27 Feb 2024 16:05:25 -0500 Subject: [PATCH 14/18] Fix no download when no missing nodes --- .../ViewModels/OpenArtBrowserViewModel.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs index cf9321c8..7cbbbd39 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -188,22 +188,24 @@ public partial class OpenArtBrowserViewModel( return; } - if (vm.MissingNodes is not { Count: > 0 } missingNodes) - { - // Skip if no missing nodes - return; - } - - var extensionManager = comfyPair.BasePackage.ExtensionManager!; - List steps = [ - new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager), - ..missingNodes.Select( - extension => new InstallExtensionStep(extensionManager, comfyPair.InstalledPackage, extension) - ) + new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) ]; + // Add install steps if missing nodes + if (vm.MissingNodes is { Count: > 0 } missingNodes) + { + var extensionManager = comfyPair.BasePackage.ExtensionManager!; + + steps.AddRange( + missingNodes.Select( + extension => + new InstallExtensionStep(extensionManager, comfyPair.InstalledPackage, extension) + ) + ); + } + var runner = new PackageModificationRunner { ShowDialogOnStart = true, From 1ea83f30f9dc42d5f5014ba6c9b46b445d4fee5f Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 27 Feb 2024 17:46:18 -0500 Subject: [PATCH 15/18] Add install node option and target package selection --- .../DesignData/DesignData.cs | 2 +- .../Dialogs/OpenArtWorkflowViewModel.cs | 51 ++++- .../ViewModels/OpenArtBrowserViewModel.cs | 9 +- .../Views/Dialogs/OpenArtWorkflowDialog.axaml | 210 ++++++++++-------- .../Models/Settings/Settings.cs | 13 ++ 5 files changed, 187 insertions(+), 98 deletions(-) diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 392c2ade..73b1bcba 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -1033,7 +1033,7 @@ The gallery images are often inpainted, but you will get something very similar DialogFactory.Get(); public static OpenArtWorkflowViewModel OpenArtWorkflowViewModel => - new() + new(Services.GetRequiredService(), Services.GetRequiredService()) { Workflow = new OpenArtSearchResult { diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs index f565b896..d6d519bf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs @@ -3,25 +3,30 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; +using AsyncAwaitBestPractices; 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 : ContentDialogViewModelBase +public partial class OpenArtWorkflowViewModel( + ISettingsManager settingsManager, + IPackageFactory packageFactory +) : ContentDialogViewModelBase { public required OpenArtSearchResult Workflow { get; init; } - public PackagePair? InstalledComfy { get; init; } [ObservableProperty] private ObservableCollection customNodes = []; @@ -29,16 +34,50 @@ public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase [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 IEnumerable AvailablePackages => + settingsManager.Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI"); + public List MissingNodes { get; } = []; public override async Task OnLoadedAsync() { + if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage) + { + SelectedPackage = preferredPackage; + } + else + { + SelectedPackage = AvailablePackages.FirstOrDefault(); + } + 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) { @@ -51,14 +90,16 @@ public partial class OpenArtWorkflowViewModel : ContentDialogViewModelBase var installedNodesNames = new HashSet(); var nameToManifestNodes = new Dictionary(); - if (InstalledComfy?.BasePackage.ExtensionManager is { } extensionManager) + var packagePair = SelectedPackagePair; + + if (packagePair?.BasePackage.ExtensionManager is { } extensionManager) { var installedNodes = ( - await extensionManager.GetInstalledExtensionsLiteAsync(InstalledComfy.InstalledPackage) + await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage) ).ToList(); var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( - extensionManager.GetManifests(InstalledComfy.InstalledPackage) + extensionManager.GetManifests(packagePair.InstalledPackage) ); // Add manifestExtensions definition to installedNodes if matching git repository url diff --git a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs index 7cbbbd39..b1dbd39a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs @@ -35,7 +35,8 @@ public partial class OpenArtBrowserViewModel( IOpenArtApi openArtApi, INotificationService notificationService, ISettingsManager settingsManager, - IPackageFactory packageFactory + IPackageFactory packageFactory, + ServiceManager vmFactory ) : TabViewModelBase, IInfinitelyScroll { private const int PageSize = 20; @@ -158,7 +159,7 @@ public partial class OpenArtBrowserViewModel( var comfyPair = packageFactory.GetPackagePair(existingComfy); - var vm = new OpenArtWorkflowViewModel { Workflow = workflow, InstalledComfy = comfyPair }; + var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow }; var dialog = new BetterContentDialog { @@ -193,8 +194,8 @@ public partial class OpenArtBrowserViewModel( new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) ]; - // Add install steps if missing nodes - if (vm.MissingNodes is { Count: > 0 } missingNodes) + // Add install steps if missing nodes and preferred + if (vm is { InstallRequiredNodes: true, MissingNodes: { Count: > 0 } missingNodes }) { var extensionManager = comfyPair.BasePackage.ExtensionManager!; diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml index af014bf1..ec043e94 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/OpenArtWorkflowDialog.axaml @@ -1,99 +1,133 @@ - - - - - - + + + + + + - - - + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index fb72ac43..e8556ba8 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -34,6 +34,19 @@ public class Settings set => ActiveInstalledPackageId = value?.Id; } + [JsonPropertyName("PreferredWorkflowPackage")] + public Guid? PreferredWorkflowPackageId { get; set; } + + [JsonIgnore] + public InstalledPackage? PreferredWorkflowPackage + { + get => + PreferredWorkflowPackageId == null + ? null + : InstalledPackages.FirstOrDefault(x => x.Id == PreferredWorkflowPackageId); + set => PreferredWorkflowPackageId = value?.Id; + } + public bool HasSeenWelcomeNotification { get; set; } public List? PathExtensions { get; set; } public string? WebApiHost { get; set; } From 8d59e1d9f2ac249542d61ffcdb90b48afbd0b4ee Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 24 Mar 2024 14:43:03 -0700 Subject: [PATCH 16/18] fix merge shenanigans --- .../Models/Packages/Extensions/IPackageExtensionManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs b/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs index cdd3e3cf..67131238 100644 --- a/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs +++ b/StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs @@ -95,6 +95,7 @@ public interface IPackageExtensionManager Task> GetInstalledExtensionsLiteAsync( InstalledPackage installedPackage, CancellationToken cancellationToken = default + ); /// /// Get updated info (version) for an installed extension. From c217189ade08317445343f1fb8d3fe3b9d96392a Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 24 Mar 2024 16:30:02 -0700 Subject: [PATCH 17/18] changed how metadata is downloaded & added context menus & localization stuff --- .../DesignData/DesignData.cs | 20 +++- .../Languages/Resources.Designer.cs | 63 ++++++++++ .../Languages/Resources.resx | 21 ++++ .../Models/OpenArtMetadata.cs | 22 ++-- .../ViewModels/InstalledWorkflowsViewModel.cs | 101 +++++++++++++++- .../ViewModels/OpenArtBrowserViewModel.cs | 27 +++-- .../Views/InstalledWorkflowsPage.axaml | 111 ++++++++++++++---- .../Views/OpenArtBrowserPage.axaml | 17 ++- StabilityMatrix.Core/Helper/EventManager.cs | 3 + .../DownloadOpenArtWorkflowStep.cs | 12 +- 10 files changed, 325 insertions(+), 72 deletions(-) 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 @@ -