diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aeffd47d..ffad36eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,20 +76,15 @@ jobs: echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - name: Set up .NET 6 (for PupNet) + - name: Set up .NET 8 uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' - + dotnet-version: '8.0.x' + - name: Install PupNet run: | sudo apt-get -y install libfuse2 - dotnet tool install --framework net6.0 -g KuiperZone.PupNet - - - name: Set up .NET 8 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' + dotnet tool install -g KuiperZone.PupNet - name: PupNet Build env: diff --git a/Avalonia.Gif/Avalonia.Gif.csproj b/Avalonia.Gif/Avalonia.Gif.csproj index 6a363589..dbf0defd 100644 --- a/Avalonia.Gif/Avalonia.Gif.csproj +++ b/Avalonia.Gif/Avalonia.Gif.csproj @@ -10,7 +10,7 @@ true - + diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e42023..fa0e8fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ 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-pre.2 +### Added +- Added more metadata to the image dialog info flyout +- Added Restart button to console page +### Changed +- Model download location selector now searches all subfolders +### Fixed +- Fixed Civitai model browser not showing images when "Show NSFW" is disabled +- Fixed crash when Installed Workflows page is opened with no Workflows folder +- Fixed progress bars not displaying properly during package installs & updates + +## v2.10.0-pre.1 +### Added +- Added OpenArt.AI workflow browser for ComfyUI workflows +- Added Output Sharing toggle in Advanced Options during install flow +### Changed +- Changed to a new image control for pages with many images +- Removed Symlink option for InvokeAI due to changes with InvokeAI v4.0+ +- Output sharing is now enabled by default for new installations +- (Internal) Updated to Avalonia 11.0.10 +### Fixed +- Improved performance when deleting many images from the Outputs page +- Fixed ComfyUI torch downgrading to 2.1.2 when updating +- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update +- Fixed "Could not find entry point for InvokeAI" error on InvokeAI v4.0+ + ## v2.10.0-dev.3 ### Added - Added support for deep links from the new Stability Matrix Chrome extension @@ -44,6 +70,17 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ### Removed - Removed the main Launch page, as it is no longer needed with the new Packages page +## v2.9.2 +### Changed +- Due to changes with the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser +- Due to the above API changes, new pages are now loaded via "infinite scrolling" +### Fixed +- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page +- Fixed model download location options for VAEs in the CivitAI Model Browser +- Fixed One-Click install progress dialog not disappearing after completion +- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area +- Fixed no packages being available for one-click install on PCs without a GPU + ## v2.9.1 ### Added - Fixed [#498](https://github.com/LykosAI/StabilityMatrix/issues/498) Added "Pony" category to CivitAI Model Browser @@ -100,6 +137,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/README.md b/README.md index 4070d220..3fd70ec3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml/badge.svg)](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml) [![Discord Server](https://img.shields.io/discord/1115555685476868168?logo=discord&logoColor=white&label=Discord%20Server)](https://discord.com/invite/TUrgfECxHz) +[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release] [release]: https://github.com/LykosAI/StabilityMatrix/releases/latest [download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip @@ -9,7 +10,7 @@ [download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg [auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui -[auto1111-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml +[sdwebui-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml [webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux [comfy]: https://github.com/comfyanonymous/ComfyUI [sdnext]: https://github.com/vladmandic/automatic @@ -21,48 +22,36 @@ [fooocus-controlnet]: https://github.com/fenneishi/Fooocus-ControlNet-SDXL [kohya-ss]: https://github.com/bmaltais/kohya_ss [onetrainer]: https://github.com/Nerogar/OneTrainer +[forge]: https://github.com/lllyasviel/stable-diffusion-webui-forge +[stable-swarm]: https://github.com/Stability-AI/StableSwarmUI [civitai]: https://civitai.com/ [huggingface]: https://huggingface.co/ -Multi-Platform Package Manager and Inference UI for Stable Diffusion +![Header image for Stability Matrix, Multi-Platform Package Manager and Inference UI for Stable Diffusion](https://cdn.lykos.ai/static/sm-banner-rounded.webp) -### ✨ New in 2.5 - [Inference](#inference-A-reimagined-built-in-Stable-Diffusion-experience), a built-in interface for Stable Diffusion powered by ComfyUI +[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64] +[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64] +[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64] + +Multi-Platform Package Manager and Inference UI for Stable Diffusion ### 🖱️ One click install and update for Stable Diffusion Web UI Packages - Supports: - - [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext] + - [Stable Diffusion WebUI Forge][forge], [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext] - [Fooocus][fooocus], [Fooocus MRE][fooocus-mre], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus] - [ComfyUI][comfy] + - [StableSwarmUI][stable-swarm] - [VoltaML][voltaml] - [InvokeAI][invokeai] - [Kohya's GUI][kohya-ss] - [OneTrainer][onetrainer] -- Manage plugins / extensions for supported packages ([Automatic 1111][auto1111], [Comfy UI][comfy]) +- Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext]) - Easily install or update Python dependencies for each package - Embedded Git and Python dependencies, with no need for either to be globally installed - Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time -### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts -- Launch arguments editor with predefined or custom options for each Package install -- Configurable Environment Variables - -### 🗃️ Checkpoint Manager, configured to be shared by all Package installs -- Option to find CivitAI metadata and preview thumbnails for new local imports - -### ☁️ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface] -- Automatically imports to the associated model folder depending on the model type -- Downloads relevant metadata files and preview image - -![header](https://cdn.lykos.ai/static/sm-banner-rounded.webp) - -[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release] - -[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64] -[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64] -[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64] - -### Inference - A reimagined built-in Stable Diffusion experience +### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix - Powerful auto-completion and syntax highlighting using a formal language grammar - Workspaces open in tabs that save and load from `.smproj` project files @@ -76,13 +65,20 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion

-### Searchable launch options +### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts +- Launch arguments editor with predefined or custom options for each Package install +- Configurable Environment Variables +

-### Model browser powered by [Civit AI][civitai] -- Downloads new models, automatically uses the appropriate shared model directory +### 🗃️ Checkpoint Manager, configured to be shared by all Package installs +- Option to find CivitAI metadata and preview thumbnails for new local imports + +### ☁️ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface] +- Automatically imports to the associated model folder depending on the model type +- Downloads relevant metadata files and preview image - Pause and resume downloads, even after closing the app

diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj index 71efd8bb..2fbf2fd4 100644 --- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj +++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj @@ -19,12 +19,12 @@ - - + + - + diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index fa669962..19eb7989 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -4,7 +4,7 @@ xmlns:local="using:StabilityMatrix.Avalonia" xmlns:idcr="using:Dock.Avalonia.Controls.Recycling" xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" - xmlns:controls="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" Name="Stability Matrix" RequestedThemeVariant="Dark"> @@ -81,10 +81,13 @@ - + + 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/Assets/sitecustomize.py b/StabilityMatrix.Avalonia/Assets/sitecustomize.py index b6ebb4c1..d154c13a 100644 --- a/StabilityMatrix.Avalonia/Assets/sitecustomize.py +++ b/StabilityMatrix.Avalonia/Assets/sitecustomize.py @@ -68,6 +68,20 @@ def _patch_rich_console(): pass except Exception as e: print("[sitecustomize error]:", e) + + try: + from pip._vendor.rich import console + + class _Console(console.Console): + @property + def is_terminal(self) -> bool: + return True + + console.Console = _Console + except ImportError: + pass + except Exception as e: + print("[sitecustomize error]:", e) _patch_rich_console() 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/Controls/Inference/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml index 86c40520..67b75973 100644 --- a/StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml @@ -9,6 +9,7 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" x:DataType="vmInference:ImageFolderCardViewModel"> @@ -195,12 +196,11 @@ - + Stretch="UniformToFill" /> @@ -115,12 +116,11 @@ - + Source="{Binding Uri}" + Stretch="Uniform" /> diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index df122b14..471f4a53 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 { @@ -358,7 +360,7 @@ public static class DesignData { new() { - Nsfw = "None", + NsfwLevel = 1, Url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" @@ -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/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs index 973b0bf6..331a9c09 100644 --- a/StabilityMatrix.Avalonia/Models/ImageSource.cs +++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs @@ -58,6 +58,9 @@ public record ImageSource : IDisposable, ITemplateKey Bitmap = bitmap; } + [JsonIgnore] + public Uri? Uri => LocalFile?.FullPath != null ? new Uri(LocalFile.FullPath) : RemoteUrl; + /// public ImageSourceTemplateType TemplateKey { get; private set; } 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/Models/PackageSteps/UnpackSiteCustomizeStep.cs b/StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs new file mode 100644 index 00000000..92e5f413 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.PackageModification; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Python; + +namespace StabilityMatrix.Avalonia.Models.PackageSteps; + +public class UnpackSiteCustomizeStep(DirectoryPath venvPath) : IPackageStep +{ + public async Task ExecuteAsync(IProgress? progress = null) + { + var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath); + var file = sitePackages.JoinFile("sitecustomize.py"); + file.Directory?.Create(); + await Assets.PyScriptSiteCustomize.ExtractTo(file); + } + + public string ProgressTitle => "Unpacking prerequisites..."; +} diff --git a/StabilityMatrix.Avalonia/Services/INavigationService.cs b/StabilityMatrix.Avalonia/Services/INavigationService.cs index f1aa8ba4..68f7f261 100644 --- a/StabilityMatrix.Avalonia/Services/INavigationService.cs +++ b/StabilityMatrix.Avalonia/Services/INavigationService.cs @@ -37,4 +37,6 @@ public interface INavigationService<[SuppressMessage("ReSharper", "UnusedTypePar void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null); bool GoBack(); + + bool CanGoBack { get; } } diff --git a/StabilityMatrix.Avalonia/Services/NavigationService.cs b/StabilityMatrix.Avalonia/Services/NavigationService.cs index c95d3068..8778f7ca 100644 --- a/StabilityMatrix.Avalonia/Services/NavigationService.cs +++ b/StabilityMatrix.Avalonia/Services/NavigationService.cs @@ -188,4 +188,6 @@ public class NavigationService : INavigationService _frame.GoBack(); return true; } + + public bool CanGoBack => _frame?.CanGoBack ?? false; } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index d1055205..b3fba009 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -39,16 +39,17 @@ - - - - - + + + + + + - + - - + + @@ -57,10 +58,10 @@ - - - - + + + + @@ -77,25 +78,25 @@ - + - + - + - - + + - + @@ -197,6 +198,9 @@ NewInstallerDialog.axaml Code + + MSBuild:GenerateCodeFromAttributes + diff --git a/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml index 61d77d82..652a4ade 100644 --- a/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml @@ -6,7 +6,8 @@ xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"> @@ -42,15 +43,14 @@ ColumnSpacing="6" RowSpacing="0"> - + Stretch="UniformToFill"/> - - - - - + Stretch="UniformToFill"/> - - - - - + Stretch="UniformToFill"> + LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains)) - .FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None"); + .FirstOrDefault(image => nsfwEnabled || image.NsfwLevel <= 1); if (image != null) { CardImage = new Uri(image.Url); @@ -216,7 +218,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; @@ -255,7 +257,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel { var subFolder = viewModel?.SelectedInstallLocation - ?? Path.Combine("Models", model.Type.ConvertTo().GetStringValue()); + ?? Path.Combine(@"Models", model.Type.ConvertTo().GetStringValue()); downloadPath = Path.Combine(settingsManager.LibraryDir, subFolder); } @@ -263,23 +265,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/NewOneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs index f6164bd0..425b20e7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs @@ -12,6 +12,7 @@ using DynamicData; using DynamicData.Binding; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Models.PackageSteps; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; @@ -107,18 +108,19 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase { Task.Run(async () => { + var installLocation = Path.Combine( + settingsManager.LibraryDir, + "Packages", + selectedPackage.Name + ); + var steps = new List { new SetPackageInstallingStep(settingsManager, selectedPackage.Name), - new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage) + new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage), }; // get latest version & download & install - var installLocation = Path.Combine( - settingsManager.LibraryDir, - "Packages", - selectedPackage.Name - ); if (Directory.Exists(installLocation)) { var installPath = new DirectoryPath(installLocation); @@ -148,6 +150,11 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase ); steps.Add(downloadStep); + var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep( + Path.Combine(installLocation, "venv") + ); + steps.Add(unpackSiteCustomizeStep); + var installStep = new InstallPackageStep( selectedPackage, torchVersion, @@ -186,7 +193,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase { ShowDialogOnStart = false, HideCloseButton = false, - ModificationCompleteMessage = $"{installedPackage.DisplayName} Install Complete", + ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully" }; runner diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index d815fb12..09a30cf1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -196,7 +196,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase { ShowDialogOnStart = true, HideCloseButton = true, - ModificationCompleteMessage = "Install complete" + ModificationCompleteMessage = $"{SelectedPackage.DisplayName} installed successfully" }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); 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/Dialogs/RecommendedModelsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs index e037408e..a2aa480e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs @@ -75,7 +75,7 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase CivitModels .Connect() .DeferUntilLoaded() - .Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0") + .Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0" || f.ModelVersion.BaseModel == "Pony") .Bind(SdxlModels) .Subscribe(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs index 9ef4c378..338aa20d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs @@ -99,7 +99,7 @@ public partial class SelectModelVersionViewModel( var allImages = value ?.ModelVersion ?.Images - ?.Where(img => nsfwEnabled || img.Nsfw == "None") + ?.Where(img => nsfwEnabled || img.NsfwLevel <= 1) ?.Select(x => new ImageSource(x.Url)) .ToList(); @@ -316,7 +316,9 @@ public partial class SelectModelVersionViewModel( installLocations.Add(downloadDirectory.ToString().Replace(rootModelsDirectory, "Models")); - foreach (var directory in downloadDirectory.EnumerateDirectories()) + foreach ( + var directory in downloadDirectory.EnumerateDirectories(searchOption: SearchOption.AllDirectories) + ) { installLocations.Add(directory.ToString().Replace(rootModelsDirectory, "Models")); } diff --git a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs new file mode 100644 index 00000000..0bc14b2c --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs @@ -0,0 +1,173 @@ +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(); + + if (!Directory.Exists(settingsManager.WorkflowDirectory)) + { + Directory.CreateDirectory(settingsManager.WorkflowDirectory); + } + + 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/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 8dcecc60..da48a2e8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -395,12 +395,13 @@ public partial class OutputsPageViewModel : PageViewModelBase var selected = Outputs.Where(o => o.IsSelected).ToList(); Debug.Assert(selected.Count == NumItemsSelected); + + var imagesToRemove = new List(); foreach (var output in selected) { // Delete the file var imageFile = new FilePath(output.ImageFile.AbsolutePath); var result = await notificationService.TryAsync(imageFile.DeleteAsync()); - if (!result.IsSuccessful) { continue; @@ -413,15 +414,10 @@ public partial class OutputsPageViewModel : PageViewModelBase await notificationService.TryAsync(sideCar.DeleteAsync()); } - OutputsCache.Remove(output.ImageFile); - - // Invalidate cache - if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) - { - loader.RemoveAllNamesFromCache(imageFile.Name); - } + imagesToRemove.Add(output.ImageFile); } + OutputsCache.Remove(imagesToRemove); NumItemsSelected = 0; ClearSelection(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 93f3534a..71d624ec 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -54,7 +54,7 @@ public partial class PackageCardViewModel( private InstalledPackage? package; [ObservableProperty] - private string? cardImageSource; + private Uri? cardImageSource; [ObservableProperty] private bool isUpdateAvailable; @@ -127,7 +127,7 @@ public partial class PackageCardViewModel( ) { IsUnknownPackage = true; - CardImageSource = ""; + CardImageSource = null; InstalledVersion = "Unknown"; } else @@ -135,7 +135,7 @@ public partial class PackageCardViewModel( IsUnknownPackage = false; var basePackage = packageFactory[value.PackageName]; - CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString(); + CardImageSource = basePackage?.PreviewImageUri ?? Assets.NoImage; InstalledVersion = value.Version?.DisplayVersion ?? "Unknown"; CanUseConfigMethod = basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false; diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs index 9172d5c5..6c24efc2 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models.PackageSteps; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; @@ -85,6 +86,9 @@ public partial class PackageInstallDetailViewModel( [ObservableProperty] private GitCommit? selectedCommit; + [ObservableProperty] + private bool isOutputSharingEnabled = true; + [ObservableProperty] private bool canInstall; @@ -97,6 +101,8 @@ public partial class PackageInstallDetailViewModel( OnInstallNameChanged(InstallName); + CanInstall = false; + SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion(); SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; @@ -183,6 +189,7 @@ public partial class PackageInstallDetailViewModel( } var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, SelectedPackage); + var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(Path.Combine(installLocation, "venv")); var downloadOptions = new DownloadPackageVersionOptions(); var installedVersion = new InstalledPackageVersion(); @@ -224,6 +231,8 @@ public partial class PackageInstallDetailViewModel( installLocation ); + var setupOutputSharingStep = new SetupOutputSharingStep(SelectedPackage, installLocation); + var package = new InstalledPackage { DisplayName = InstallName, @@ -234,7 +243,8 @@ public partial class PackageInstallDetailViewModel( LaunchCommand = SelectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, PreferredTorchVersion = SelectedTorchVersion, - PreferredSharedFolderMethod = SelectedSharedFolderMethod + PreferredSharedFolderMethod = SelectedSharedFolderMethod, + UseSharedOutputFolder = IsOutputSharingEnabled }; var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package); @@ -244,11 +254,17 @@ public partial class PackageInstallDetailViewModel( setPackageInstallingStep, prereqStep, downloadStep, + unpackSiteCustomizeStep, installStep, setupModelFoldersStep, addInstalledPackageStep }; + if (IsOutputSharingEnabled) + { + steps.Insert(steps.IndexOf(addInstalledPackageStep), setupOutputSharingStep); + } + var packageName = SelectedPackage.Name; var runner = new PackageModificationRunner diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs index cd4f5f46..fb74fc17 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs @@ -49,10 +49,21 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase Name = packageModificationRunner.CurrentStep?.ProgressTitle; Failed = packageModificationRunner.Failed; - if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Contains("Downloading...")) + if (e.ProcessOutput == null && string.IsNullOrWhiteSpace(e.Message)) return; - Progress.Console.PostLine(e.Message); + if (!string.IsNullOrWhiteSpace(e.Message) && e.Message.Contains("Downloading...")) + return; + + if (e.ProcessOutput != null) + { + Progress.Console.Post(e.ProcessOutput.Value); + } + else + { + Progress.Console.PostLine(e.Message); + } + EventManager.Instance.OnScrollToBottomRequested(); if ( diff --git a/StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs index 38011e04..73f89a8b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs @@ -104,6 +104,14 @@ public partial class RunningPackageViewModel : PageViewModelBase, IDisposable, I } } + [RelayCommand] + private async Task Restart() + { + await Stop(); + await Task.Delay(100); + LaunchPackage(); + } + [RelayCommand] private void LaunchPackage() { @@ -113,10 +121,10 @@ public partial class RunningPackageViewModel : PageViewModelBase, IDisposable, I [RelayCommand] private async Task Stop() { + IsRunning = false; await runningPackageService.StopPackage(RunningPackage.InstalledPackage.Id); Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}"); await Console.StopUpdatesAsync(); - IsRunning = false; } [RelayCommand] 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/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index c2831986..bbb08dae 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -13,6 +13,7 @@ xmlns:api="clr-namespace:StabilityMatrix.Core.Models.Api;assembly=StabilityMatrix.Core" xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" x:CompileBindings="True" x:DataType="vm:CheckpointsPageViewModel" @@ -138,7 +139,7 @@ TextWrapping="WrapWithOverflow" IsVisible="{Binding IsConnectedModel}" /> -
- + - @@ -60,7 +60,7 @@ @@ -103,7 +103,7 @@ CommandParameter="{Binding CivitModel}" IsEnabled="{Binding !IsImporting}"> - --> - - + - + Source="{Binding CivitModel.Creator.Image}"> + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..4b16cf68 --- /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/MainWindow.axaml b/StabilityMatrix.Avalonia/Views/MainWindow.axaml index 75613afc..306a1a37 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml @@ -70,11 +70,13 @@ Grid.RowSpan="2" Name="NavigationView" ItemInvoked="NavigationView_OnItemInvoked" + BackRequested="NavigationView_OnBackRequested" PaneDisplayMode="Left" IsPaneOpen="False" OpenPaneLength="{Binding PaneWidth}" IsSettingsVisible="False" - IsBackEnabled="False" + IsBackEnabled="True" + IsBackButtonVisible="True" MenuItemsSource="{Binding Pages, Mode=OneWay}" FooterMenuItemsSource="{Binding FooterPages, Mode=OneWay}" SelectedItem="{Binding SelectedCategory}"> diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs index c720f37b..eba477f8 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs @@ -446,4 +446,9 @@ public partial class MainWindow : AppWindowBase e.Handled = true; navigationService.GoBack(); } + + private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e) + { + navigationService.GoBack(); + } } diff --git a/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml new file mode 100644 index 00000000..216dcc46 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/PackageManager/PackageInstallBrowserView.axaml b/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml index 68a01735..66040670 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml @@ -34,8 +34,8 @@ diff --git a/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml b/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml index 91b2e671..594b03eb 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" @@ -10,8 +9,8 @@ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" - xmlns:extensions="clr-namespace:StabilityMatrix.Core.Models.Packages.Extensions;assembly=StabilityMatrix.Core" xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="packageManager:PackageInstallDetailViewModel" x:CompileBindings="True" @@ -21,19 +20,21 @@ + Margin="16, 16, 8, 8" + CornerRadius="8" + Source="{Binding SelectedPackage.PreviewImageUri}"> + + - + + Content="{Binding SelectedPackage.LicenseType}" /> - + - - - + + +