diff --git a/.editorconfig b/.editorconfig index 15cae5b1a..53ed69807 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,12 +2,78 @@ root = true [*.cs] max_line_length = 120 -csharp_style_var_for_built_in_types = true +csharp_style_var_for_built_in_types = true:suggestion dotnet_sort_system_directives_first = true # ReSharper properties resharper_csharp_max_line_length = 120 +resharper_place_field_attribute_on_same_line = false # dotnet code quality # noinspection EditorConfigKeyCorrectness -dotnet_code_quality.CA1826.exclude_ordefault_methods = true +dotnet_code_quality.ca1826.exclude_ordefault_methods = true + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, file, static, new, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.private_constants_rule.import_to_resharper = True +dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private) +dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private) +dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c +dotnet_naming_rule.private_instance_fields_rule.resharper_style = aaBb, _ + aaBb +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private) +dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = True +dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private) +dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3 +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix = _ +dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds = constant_field +dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field,readonly_field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = readonly,static +dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field +dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion diff --git a/CHANGELOG.md b/CHANGELOG.md index 420397c6a..e992dd7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,157 @@ 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.11.0 +### Added +#### Packages +- Added new package: [SDFX](https://github.com/sdfxai/sdfx/) by sdfxai +- Added ZLUDA option for SD.Next +- Added more launch options for Forge - [#618](https://github.com/LykosAI/StabilityMatrix/issues/618) +- Added search bar to the Python Packages dialog +#### Inference +- Added Inpainting support for Image To Image projects using the new image mask canvas editor +- Added alternate Lora / LyCORIS drop-down model selection, can be toggled via the model settings button. Allows choosing both CLIP and Model Weights. The existing prompt-based `` method is still available. +- Added optional Recycle Bin mode when deleting images in the Inference image browser, can be disabled in settings (Currently available on Windows and macOS) +#### Model Browsers +- Added PixArt, SDXL Hyper, and SD3 options to the CivitAI Model Browser +- Added XL ControlNets section to HuggingFace model browser +- Added download speed indicator to model downloads in the Downloads tab +#### Output Browser +- Added support for indexing and displaying jpg/jpeg & gif images (in additional to png and webp / animated webp), with metadata parsing and search for compatible formats +#### Settings +- Added setting for locale specific or invariant number formatting +- Added setting for toggling model browser auto-search on load +- Added option in Settings to choose whether to Copy or Move files when dragging and dropping files into the Checkpoint Manager +- Added folder shortcuts in Settings for opening common app and system folders, such as Data Directory and Logs +#### Translations +- Added Brazilian Portuguese language option, thanks to jbostroski for the translation! +### Changed +- Maximized state is now stored on exit and restored on launch +- Drag & drop imports now move files by default instead of copying +- Clicking outside the Select Model Version dialog will now close it +- Changed Package card buttons to better indicate that they are buttons +- Log file storage has been moved from `%AppData%/StabilityMatrix` to a subfolder: `%AppData%/StabilityMatrix/Logs` +- Archived log files now have an increased rolling limit of 9 files, from 2 files previously. Their file names will now be in the format `app.{yyyy-MM-dd HH_mm_ss}.log`. The current session log file remains named `app.log`. +- Updated image controls on Recommended Models dialog to match the rest of the app +- Improved app shutdown clean-up process reliability and speed +- Improved ProcessTracker speed and clean-up safety for faster subprocess and package launching performance +- Updated HuggingFace page so the command bar stays fixed at the top +- Revamped Checkpoints page now shows available model updates and has better drag & drop functionality +- Revamped file deletion confirmation dialog with affected file paths display and recycle bin / permanent delete options (Checkpoint and Output Browsers) (Currently available on Windows and macOS) +### Fixed +- Fixed crash when parsing invalid generated images in Output Browser and Inference image viewer, errors will be logged instead and the image will be skipped +- Fixed missing progress text during package updates +- (Windows) Fixed "Open in Explorer" buttons across the app not opening the correct path on ReFS partitions +- (macOS, Linux) Fixed Subprocesses of packages sometimes not being closed when the app is closed +- Fixed Inference tabs sometimes not being restored from previous sessions +- Fixed multiple log files being archived in a single session, and losing some log entries +- Fixed error when installing certain packages with comments in the requirements file +- Fixed error when deleting Inference browser images in a nested project path with recycle bin mode +- Fixed extra text in positive prompt when loading image parameters in Inference with empty negative prompt value +- Fixed NullReferenceException that sometimes occurred when closing Inference tabs with images due to Avalonia.Bitmap.Size accessor issue +- Fixed [#598](https://github.com/LykosAI/StabilityMatrix/issues/598) - program not exiting after printing help or version text +- Fixed [#630](https://github.com/LykosAI/StabilityMatrix/issues/630) - InvokeAI update hangs forever waiting for input +- Fixed issue where the "installed" state on HuggingFace model browser was not always correct +- Fixed model folders not being created on startup + +### Supporters +#### Visionaries +- Shoutout to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your generous support is appreciated and helps us continue to make Stability Matrix better for everyone! +#### Pioneers +- A big thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support helps us continue to improve Stability Matrix! + +## v2.11.0-pre.2 +### Added +- Added folder shortcuts in Settings for opening common app and system folders, such as Data Directory and Logs. +### Changed +- Log file storage have been moved from `%AppData%/StabilityMatrix` to a subfolder: `%AppData%/StabilityMatrix/Logs` +- Archived log files now have an increased rolling limit of 9 files, from 2 files previously. Their file names will now be in the format `app.{yyyy-MM-dd HH_mm_ss}.log`. The current session log file remains named `app.log`. +- Updated image controls on Recommended Models dialog to match the rest of the app +- Improved app shutdown clean-up process reliability and speed +- Improved ProcessTracker speed and clean-up safety for faster subprocess and package launching performance +### Fixed +- Fixed crash when parsing invalid generated images in Output Browser and Inference image viewer, errors will be logged instead and the image will be skipped +- Fixed issue where blue and red color channels were swapped in the mask editor dialog +- Fixed missing progress text during package updates +- Fixed "Git and Node.js are required" error during SDFX install +- (Windows) Fixed "Open in Explorer" buttons across the app not opening the correct path on ReFS partitions +- (Windows) Fixed Sdfx electron window not closing when stopping the package +- (macOS, Linux) Fixed Subprocesses of packages sometimes not being closed when the app is closed +- Fixed Inference tabs sometimes not being restored from previous sessions +- Fixed multiple log files being archived in a single session, and losing some log entries +- Fixed error when installing certain packages with comments in the requirements file +- Fixed some more missing progress texts during various activities +### Supporters +#### Visionaries +- A heartfelt thank you to our Visionary-tier Patreon supporters, **Scopp Mcdee** and **Waterclouds**! Your generous contributions enable us to keep enhancing Stability Matrix! + +## v2.11.0-pre.1 +### Added +- Added new package: [SDFX](https://github.com/sdfxai/sdfx/) by sdfxai +- Added "Show Nested Models" toggle for new Checkpoints page, allowing users to show or hide models in subfolders of the selected folder +- Added ZLUDA option for SD.Next +- Added PixArt & SDXL Hyper options to the Civitai model browser +- Added release date to model update notification card on the Checkpoints page +- Added option in Settings to choose whether to Copy or Move files when dragging and dropping files into the Checkpoint Manager +- Added more launch options for Forge - [#618](https://github.com/LykosAI/StabilityMatrix/issues/618) +#### Inference +- Added Inpainting support for Image To Image projects using the new image mask canvas editor +### Changed +- Maximized state is now stored on exit and restored on launch +- Clicking outside the Select Model Version dialog will now close it +- Changed Package card buttons to better indicate that they are buttons +### Fixed +- Fixed error when deleting Inference browser images in a nested project path with recycle bin mode +- Fixed extra text in positive prompt when loading image parameters in Inference with empty negative prompt value +- Fixed NullReferenceException that sometimes occured when closing Inference tabs with images due to Avalonia.Bitmap.Size accessor issue +- Fixed package installs not showing any progress messages +- Fixed crash when viewing model details for Unknown model types in the Checkpoint Manager +- Fixed [#598](https://github.com/LykosAI/StabilityMatrix/issues/598) - program not exiting after printing help or version text +- Fixed [#630](https://github.com/LykosAI/StabilityMatrix/issues/630) - InvokeAI update hangs forever waiting for input +### Supporters +#### Visionaries +- Many thanks to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your generous support helps us continue to improve Stability Matrix! + +## v2.11.0-dev.3 +### Added +- Added download speed indicator to model downloads in the Downloads tab +- Added XL ControlNets section to HuggingFace model browser +- Added toggle in Settings for model browser auto-search on load +- Added optional Recycle Bin mode when deleting images in the Inference image browser, can be disabled in settings (Currently on Windows only) +### Changed +- Revamped Checkpoints page now shows available model updates and has better drag & drop functionality +- Updated HuggingFace page so the command bar stays fixed at the top +- Revamped file deletion confirmation dialog with affected file paths display and recycle bin / permanent delete options (Checkpoint and Output Browsers) (Currently on Windows only) +### Fixed +- Fixed issue where the "installed" state on HuggingFace model browser was not always correct +### Supporters +#### Visionaries +- Special shoutout to our first two Visionaries on Patreon, **Scopp Mcdee** and **Waterclouds**! Thank you for your generous support! + +## v2.11.0-dev.2 +### Added +- Added Brazilian Portuguese language option, thanks to jbostroski for the translation! +- Added setting for locale specific or invariant number formatting +- Added support for jpg/jpeg & gif images in the Output Browser +### Changed +- Centered OpenArt browser cards +### Fixed +- Fixed MPS install on macOS for ComfyUI, A1111, SDWebUI Forge, and SDWebUI UX causing torch to be upgraded to dev nightly versions and causing incompatibilities with dependencies. +- Fixed "Auto Scroll to End" not working in some scenarios +- Fixed "Auto Scroll to End" toggle button not scrolling to the end when toggled on +- Fixed/reverted output folder name changes for Automatic1111 +- Fixed xformers being uninstalled with every ComfyUI update +- Fixed Inference Lora menu strength resetting to default if out of slider range (0 to 1) +- Fixed missing progress text during package installs + +## v2.11.0-dev.1 +### Added +- Added search bar to the Python Packages dialog +#### Inference +- Alternate Lora / LyCORIS drop-down model selection, can be toggled via the model settings button. The existing prompt-based Lora / LyCORIS method is still available. +### Fixed +- Fixed crash when failing to parse Python package details + ## v2.10.3 ### Changed - Centered OpenArt browser cards diff --git a/README.md b/README.md index fc9cb21dd..79b4edecc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ [![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] + +[![Latest Stable](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Stable&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release] +[![Latest Preview](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.lykos.ai%2Fupdate-v3.json&query=%24.updates.preview%5B%22win-x64%22%5D.version&prefix=v&label=Latest%20Preview&color=b57400&cacheSeconds=60&link=https%3A%2F%2Flykos.ai%2Fdownloads)](https://lykos.ai/downloads) +[![Latest Dev](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.lykos.ai%2Fupdate-v3.json&query=%24.updates.development%5B%22win-x64%22%5D.version&prefix=v&label=Latest%20Dev&color=880c21&cacheSeconds=60&link=https%3A%2F%2Flykos.ai%2Fdownloads)](https://lykos.ai/downloads) [release]: https://github.com/LykosAI/StabilityMatrix/releases/latest [download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip @@ -11,7 +14,7 @@ [download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg [auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui -[sdwebui-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml +[auto1111-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 @@ -25,6 +28,7 @@ [onetrainer]: https://github.com/Nerogar/OneTrainer [forge]: https://github.com/lllyasviel/stable-diffusion-webui-forge [stable-swarm]: https://github.com/Stability-AI/StableSwarmUI +[sdfx]: https://github.com/sdfxai/sdfx [civitai]: https://civitai.com/ [huggingface]: https://huggingface.co/ @@ -46,6 +50,7 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion - [StableSwarmUI][stable-swarm] - [VoltaML][voltaml] - [InvokeAI][invokeai] + - [SDFX][sdfx] - [Kohya's GUI][kohya-ss] - [OneTrainer][onetrainer] - Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext]) diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj index 7d0762acb..9cb9db876 100644 --- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj +++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj @@ -25,7 +25,7 @@ - + diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index d6db2ffd6..345e026d3 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -82,7 +82,9 @@ + + + diff --git a/StabilityMatrix.Avalonia/Controls/Inference/ExtraNetworkCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/ExtraNetworkCard.axaml.cs new file mode 100644 index 000000000..b5e9d41f2 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Inference/ExtraNetworkCard.axaml.cs @@ -0,0 +1,7 @@ +using Avalonia.Controls.Primitives; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Controls; + +[Transient] +public class ExtraNetworkCard : TemplatedControl; diff --git a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml index 5ffbab8ee..b44d6d516 100644 --- a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml @@ -13,8 +13,8 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" x:DataType="inference:ModelCardViewModel"> - - + + @@ -44,7 +44,7 @@ + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml index 7539cbdee..5370b273c 100644 --- a/StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml @@ -94,7 +94,7 @@ + FontFamily="Cascadia Code,Consolas,Menlo,Monospace,DejaVu Sans Mono,monospace"> + FontFamily="Cascadia Code,Consolas,Menlo,Monospace,DejaVu Sans Mono,monospace"> + FontFamily="Cascadia Code,Consolas,Menlo,Monospace,DejaVu Sans Mono,monospace"> @@ -50,6 +48,23 @@ Source="{Binding ImageSource}" Stretch="Uniform" StretchDirection="Both" /> + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ("PART_BetterAdvancedImage") is not { } image) + if (e.NameScope.Find("PART_BetterAdvancedImage") is not { } imageControl) return; - image + imageControl .WhenPropertyChanged(x => x.CurrentImage) .Subscribe(propertyValue => { - if (propertyValue.Value?.Size is { } size) + if (propertyValue.Value is { } image) { - vm.CurrentBitmapSize = new Size(Convert.ToInt32(size.Width), Convert.ToInt32(size.Height)); - } - else - { - vm.CurrentBitmapSize = Size.Empty; + // Sometimes Avalonia Bitmap.Size getter throws a NullReferenceException depending on skia lifetimes (probably) + // so just catch it and ignore it + Size? size = null; + try + { + size = image.Size; + } + catch (NullReferenceException) { } + + if (size is not null) + { + vm.CurrentBitmapSize = new System.Drawing.Size( + Convert.ToInt32(size.Value.Width), + Convert.ToInt32(size.Value.Height) + ); + return; + } } + + vm.CurrentBitmapSize = System.Drawing.Size.Empty; }); } } diff --git a/StabilityMatrix.Avalonia/Controls/Models/PenPath.cs b/StabilityMatrix.Avalonia/Controls/Models/PenPath.cs new file mode 100644 index 000000000..ba830c7f9 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Models/PenPath.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using SkiaSharp; +using StabilityMatrix.Core.Converters.Json; + +namespace StabilityMatrix.Avalonia.Controls.Models; + +public readonly record struct PenPath() +{ + [JsonConverter(typeof(SKColorJsonConverter))] + public SKColor FillColor { get; init; } + + public bool IsErase { get; init; } + + public List Points { get; init; } = []; + + public SKPath ToSKPath() + { + var skPath = new SKPath(); + + if (Points.Count <= 0) + { + return skPath; + } + + // First move to the first point + skPath.MoveTo(Points[0].X, Points[0].Y); + + // Add the rest of the points + for (var i = 1; i < Points.Count; i++) + { + skPath.LineTo(Points[i].X, Points[i].Y); + } + + return skPath; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/Models/PenPoint.cs b/StabilityMatrix.Avalonia/Controls/Models/PenPoint.cs new file mode 100644 index 000000000..b3f004492 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Models/PenPoint.cs @@ -0,0 +1,30 @@ +using System; +using SkiaSharp; + +namespace StabilityMatrix.Avalonia.Controls.Models; + +public readonly record struct PenPoint(ulong X, ulong Y) +{ + public PenPoint(double x, double y) + : this(Convert.ToUInt64(x), Convert.ToUInt64(y)) { } + + public PenPoint(SKPoint skPoint) + : this(Convert.ToUInt64(skPoint.X), Convert.ToUInt64(skPoint.Y)) { } + + /// + /// Radius of the point. + /// + public double Radius { get; init; } = 1; + + /// + /// Optional pressure of the point. If null, the pressure is unknown. + /// + public double? Pressure { get; init; } + + /// + /// True if the point was created by a pen, false if it was created by a mouse. + /// + public bool IsPen { get; init; } + + public SKPoint ToSKPoint() => new(X, Y); +} diff --git a/StabilityMatrix.Avalonia/Controls/Models/SKLayer.cs b/StabilityMatrix.Avalonia/Controls/Models/SKLayer.cs new file mode 100644 index 000000000..809a1a138 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Models/SKLayer.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; +using SkiaSharp; + +namespace StabilityMatrix.Avalonia.Controls.Models; + +public class SKLayer +{ + /// + /// Surface from Canvas that contains the layer. + /// + public SKSurface? Surface { get; set; } + + /// + /// Optional bitmaps that will be drawn on the layer, in order. + /// (Last index will be drawn on top over previous ones) + /// + public ImmutableList Bitmaps { get; set; } = []; +} diff --git a/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml new file mode 100644 index 000000000..cfd62d17a --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml @@ -0,0 +1,166 @@ + + + + + + + + + + + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs new file mode 100644 index 000000000..daae6fa44 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs @@ -0,0 +1,441 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using DynamicData.Binding; +using SkiaSharp; +using StabilityMatrix.Avalonia.Controls.Models; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.ViewModels.Controls; + +namespace StabilityMatrix.Avalonia.Controls; + +public class PaintCanvas : TemplatedControl +{ + private ConcurrentDictionary TemporaryPaths => ViewModel!.TemporaryPaths; + + private ImmutableList Paths + { + get => ViewModel!.Paths; + set => ViewModel!.Paths = value; + } + + private IDisposable? viewModelSubscription; + + private bool isPenDown; + + private PaintCanvasViewModel? ViewModel { get; set; } + + private SkiaCustomCanvas? MainCanvas { get; set; } + + static PaintCanvas() + { + AffectsRender(BoundsProperty); + } + + public void RefreshCanvas() + { + Dispatcher.UIThread.Post(() => MainCanvas?.InvalidateVisual(), DispatcherPriority.Render); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + MainCanvas = e.NameScope.Find("PART_MainCanvas"); + + Debug.Assert(MainCanvas != null); + + if (MainCanvas is not null) + { + // If we already have a BackgroundBitmap, scale MainCanvas to match + if (DataContext is PaintCanvasViewModel { BackgroundImage: { } backgroundBitmap }) + { + MainCanvas.Width = backgroundBitmap.Width; + MainCanvas.Height = backgroundBitmap.Height; + } + + MainCanvas.RenderSkia += OnRenderSkia; + MainCanvas.PointerEntered += MainCanvas_OnPointerEntered; + MainCanvas.PointerExited += MainCanvas_OnPointerExited; + } + + var zoomBorder = e.NameScope.Find("PART_ZoomBorder"); + if (zoomBorder is not null) + { + zoomBorder.ZoomChanged += (_, zoomEventArgs) => + { + if (ViewModel is not null) + { + ViewModel.CurrentZoom = zoomEventArgs.ZoomX; + + UpdateCanvasCursor(); + } + }; + + if (ViewModel is not null) + { + ViewModel.CurrentZoom = zoomBorder.ZoomX; + + UpdateCanvasCursor(); + } + } + + OnDataContextChanged(EventArgs.Empty); + } + + /// + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (DataContext is PaintCanvasViewModel viewModel) + { + // Set the remote actions + viewModel.RefreshCanvas = RefreshCanvas; + + viewModelSubscription?.Dispose(); + viewModelSubscription = viewModel + .WhenPropertyChanged(vm => vm.BackgroundImage) + .Subscribe(change => + { + if (MainCanvas is not null && change.Value is not null) + { + MainCanvas.Width = change.Value.Width; + MainCanvas.Height = change.Value.Height; + MainCanvas.InvalidateVisual(); + } + }); + + ViewModel = viewModel; + } + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsEnabledProperty) + { + var newIsEnabled = change.GetNewValue(); + + if (!newIsEnabled) + { + isPenDown = false; + } + + // On any enabled change, flush temporary paths + if (!TemporaryPaths.IsEmpty) + { + Paths = Paths.AddRange(TemporaryPaths.Values); + TemporaryPaths.Clear(); + } + } + } + + /// + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + UpdateMainCanvasBounds(); + } + + private void HandlePointerEvent(PointerEventArgs e) + { + // Ignore if disabled + if (!IsEnabled) + { + return; + } + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + TemporaryPaths.TryRemove(e.Pointer.Id, out _); + return; + } + + e.Handled = true; + + // Must have this or stylus inputs lost after a while + // https://github.com/AvaloniaUI/Avalonia/issues/12289#issuecomment-1695620412 + e.PreventGestureRecognition(); + + if (DataContext is not PaintCanvasViewModel viewModel) + { + return; + } + + var currentPoint = e.GetCurrentPoint(this); + + if (e.RoutedEvent == PointerPressedEvent) + { + // Ignore if mouse and not left button + if (e.Pointer.Type == PointerType.Mouse && !currentPoint.Properties.IsLeftButtonPressed) + { + return; + } + + isPenDown = true; + + HandlePointerMoved(e); + } + else if (e.RoutedEvent == PointerReleasedEvent) + { + if (isPenDown) + { + HandlePointerMoved(e); + + isPenDown = false; + } + + if (TemporaryPaths.TryGetValue(e.Pointer.Id, out var path)) + { + Paths = Paths.Add(path); + } + + TemporaryPaths.TryRemove(e.Pointer.Id, out _); + } + else + { + // Moved event + if (!isPenDown || currentPoint.Properties.Pressure == 0) + { + return; + } + + HandlePointerMoved(e); + } + + Dispatcher.UIThread.Post(() => MainCanvas?.InvalidateVisual(), DispatcherPriority.Render); + } + + private void HandlePointerMoved(PointerEventArgs e) + { + if (DataContext is not PaintCanvasViewModel viewModel) + { + return; + } + + // Use intermediate points to include past events we missed + var points = e.GetIntermediatePoints(MainCanvas); + + Debug.WriteLine($"Points: {string.Join(",", points.Select(p => p.Position.ToString()))}"); + + if (points.Count == 0) + { + return; + } + + viewModel.CurrentPenPressure = points.FirstOrDefault().Properties.Pressure; + + // Get or create a temp path + if (!TemporaryPaths.TryGetValue(e.Pointer.Id, out var penPath)) + { + penPath = new PenPath + { + FillColor = viewModel.PaintBrushSKColor.WithAlpha((byte)(viewModel.PaintBrushAlpha * 255)), + IsErase = viewModel.SelectedTool == PaintCanvasTool.Eraser + }; + TemporaryPaths[e.Pointer.Id] = penPath; + } + + // Add line for path + // var cursorPosition = e.GetPosition(MainCanvas); + // penPath.Path.LineTo(cursorPosition.ToSKPoint()); + + // Get bounds for discarding invalid points + var canvasBounds = MainCanvas?.Bounds ?? new Rect(); + + // Add points + foreach (var point in points) + { + // Discard invalid points + if (!canvasBounds.Contains(point.Position) || point.Position.X < 0 || point.Position.Y < 0) + { + continue; + } + + var penPoint = new PenPoint(point.Position.X, point.Position.Y) + { + Pressure = point.Pointer.Type == PointerType.Mouse ? null : point.Properties.Pressure, + Radius = viewModel.PaintBrushSize, + IsPen = point.Pointer.Type == PointerType.Pen + }; + + penPath.Points.Add(penPoint); + } + } + + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + HandlePointerEvent(e); + base.OnPointerPressed(e); + } + + /// + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandlePointerEvent(e); + base.OnPointerReleased(e); + } + + /// + protected override void OnPointerMoved(PointerEventArgs e) + { + HandlePointerEvent(e); + base.OnPointerMoved(e); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Escape) + { + e.Handled = true; + } + } + + /// + /// Update the bounds of the main canvas to match the background image + /// + private void UpdateMainCanvasBounds() + { + if ( + MainCanvas is null + || DataContext is not PaintCanvasViewModel { BackgroundImage: { } backgroundBitmap } + ) + { + return; + } + + // Set size if mismatch + if ( + Math.Abs(MainCanvas.Width - backgroundBitmap.Width) > 0.1 + || Math.Abs(MainCanvas.Height - backgroundBitmap.Height) > 0.1 + ) + { + MainCanvas.Width = backgroundBitmap.Width; + MainCanvas.Height = backgroundBitmap.Height; + MainCanvas.InvalidateVisual(); + } + } + + private int lastCanvasCursorRadius; + private Cursor? lastCanvasCursor; + + private void UpdateCanvasCursor() + { + if (MainCanvas is not { } canvas) + { + return; + } + + var currentZoom = ViewModel?.CurrentZoom ?? 1; + + // Get brush size + var currentBrushSize = Math.Max((ViewModel?.PaintBrushSize ?? 1) - 2, 1); + var brushRadius = (int)Math.Ceiling(currentBrushSize * 2 * currentZoom); + + // Only update cursor if brush size has changed + if (brushRadius == lastCanvasCursorRadius) + { + canvas.Cursor = lastCanvasCursor; + return; + } + + lastCanvasCursorRadius = brushRadius; + + var brushDiameter = brushRadius * 2; + + const int padding = 4; + + var canvasCenter = brushRadius + padding; + var canvasSize = brushDiameter + padding * 2; + + using var cursorBitmap = new SKBitmap(canvasSize, canvasSize); + + using var cursorCanvas = new SKCanvas(cursorBitmap); + cursorCanvas.Clear(SKColors.Transparent); + cursorCanvas.DrawCircle( + brushRadius + padding, + brushRadius + padding, + brushRadius, + new SKPaint + { + Color = SKColors.Black, + Style = SKPaintStyle.Stroke, + StrokeWidth = 1.5f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + IsDither = true, + IsAntialias = true + } + ); + cursorCanvas.Flush(); + + using var data = cursorBitmap.Encode(SKEncodedImageFormat.Png, 100); + using var stream = data.AsStream(); + + var bitmap = WriteableBitmap.Decode(stream); + + canvas.Cursor = new Cursor(bitmap, new PixelPoint(canvasCenter, canvasCenter)); + + lastCanvasCursor?.Dispose(); + lastCanvasCursor = canvas.Cursor; + } + + private void MainCanvas_OnPointerEntered(object? sender, PointerEventArgs e) + { + UpdateCanvasCursor(); + } + + private void MainCanvas_OnPointerExited(object? sender, PointerEventArgs e) + { + if (sender is SkiaCustomCanvas canvas) + { + canvas.Cursor = new Cursor(StandardCursorType.Arrow); + } + } + + private Point GetRelativePosition(Point pt, Visual? relativeTo) + { + if (VisualRoot is not Visual visualRoot) + return default; + if (relativeTo == null) + return pt; + + return pt * visualRoot.TransformToVisual(relativeTo) ?? default; + } + + public AsyncRelayCommand ClearCanvasCommand => new(ClearCanvasAsync); + + public async Task ClearCanvasAsync() + { + Paths = ImmutableList.Empty; + TemporaryPaths.Clear(); + + await Dispatcher.UIThread.InvokeAsync(() => MainCanvas?.InvalidateVisual()); + } + + private void OnRenderSkia(SKSurface surface) + { + ViewModel?.RenderToSurface(surface, renderBackgroundFill: true, renderBackgroundImage: true); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml b/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml new file mode 100644 index 000000000..03f0bc853 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml @@ -0,0 +1,7 @@ + + diff --git a/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml.cs b/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml.cs new file mode 100644 index 000000000..c47ce6e76 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/SkiaCustomCanvas.axaml.cs @@ -0,0 +1,71 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using SkiaSharp; + +namespace StabilityMatrix.Avalonia.Controls; + +public partial class SkiaCustomCanvas : UserControl +{ + private readonly RenderingLogic renderingLogic = new(); + + public event Action? RenderSkia; + + public SkiaCustomCanvas() + { + InitializeComponent(); + + Background = Brushes.Transparent; + + renderingLogic.RenderCall += surface => RenderSkia?.Invoke(surface); + } + + public override void Render(DrawingContext context) + { + renderingLogic.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height); + + context.Custom(renderingLogic); + } + + private class RenderingLogic : ICustomDrawOperation + { + public Action? RenderCall; + + public Rect Bounds { get; set; } + + public void Dispose() { } + + public bool Equals(ICustomDrawOperation? other) + { + return other == this; + } + + /// + public bool HitTest(Point p) + { + return false; + } + + /// + public void Render(ImmediateDrawingContext context) + { + var skia = context.TryGetFeature(); + + using var lease = skia?.Lease(); + + if (lease?.SkSurface is { } skSurface) + { + Render(skSurface); + } + } + + private void Render(SKSurface surface) + { + RenderCall?.Invoke(surface); + } + } +} diff --git a/StabilityMatrix.Avalonia/Converters/EnumToValuesConverter.cs b/StabilityMatrix.Avalonia/Converters/EnumToValuesConverter.cs new file mode 100644 index 000000000..76c3428a4 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/EnumToValuesConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +/// +/// Converts an enum value to an array of values +/// +public class EnumToValuesConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value == null) + { + return null; + } + + var type = value.GetType(); + if (!type.IsEnum) + { + return null; + } + + return Enum.GetValues(type); + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/NumberFormatModeSampleConverter.cs b/StabilityMatrix.Avalonia/Converters/NumberFormatModeSampleConverter.cs new file mode 100644 index 000000000..df2ce2f8d --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/NumberFormatModeSampleConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using StabilityMatrix.Core.Models.Settings; + +namespace StabilityMatrix.Avalonia.Converters; + +/// +/// Converts a to a sample number string +/// +public class NumberFormatModeSampleConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not NumberFormatMode mode) + return null; + + const double sample = 12345.67; + + // Format the sample number based on the number format mode + return mode switch + { + NumberFormatMode.Default => sample.ToString("N2", culture), + NumberFormatMode.CurrentCulture => sample.ToString("N2", culture), + NumberFormatMode.InvariantCulture => sample.ToString("N2", CultureInfo.InvariantCulture), + _ => throw new ArgumentOutOfRangeException() + }; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index c61d19ba2..89b561bb3 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -8,7 +8,6 @@ using System.Net.Http; using System.Text; using AvaloniaEdit.Utils; -using DynamicData; using DynamicData.Binding; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -21,6 +20,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; +using StabilityMatrix.Avalonia.ViewModels.Controls; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference.Video; @@ -49,6 +49,7 @@ using StabilityMatrix.Core.Updater; using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.HuggingFacePageViewModel; +using MainPackageManagerViewModel = StabilityMatrix.Avalonia.ViewModels.PackageManager.MainPackageManagerViewModel; namespace StabilityMatrix.Avalonia.DesignData; @@ -211,8 +212,8 @@ public static void Initialize() packageFactory ); - ObservableCacheEx.AddOrUpdate( - CheckpointsPageViewModel.CheckpointFoldersCache, + /*ObservableCacheEx.AddOrUpdate( + OldCheckpointsPageViewModel.CheckpointFoldersCache, new CheckpointFolder[] { new(settingsManager, downloadService, modelFinder, notificationService, modelImportService) @@ -256,76 +257,7 @@ public static void Initialize() } } } - ); - - /*// Checkpoints page - CheckpointsPageViewModel.CheckpointFolders = - new CheckpointFolder[] - { - new(settingsManager, downloadService, modelFinder, notificationService) - { - Title = "StableDiffusion", - DirectoryPath = "Models/StableDiffusion", - CheckpointFiles = CheckpointFile[] - { - new() - { - FilePath = "~/Models/StableDiffusion/electricity-light.safetensors", - Title = "Auroral Background", - PreviewImagePath = - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" - + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", - ConnectedModel = new ConnectedModelInfo - { - VersionName = "Lightning Auroral", - BaseModel = "SD 1.5", - ModelName = "Auroral Background", - ModelType = CivitModelType.Model, - FileMetadata = new CivitFileMetadata - { - Format = CivitModelFormat.SafeTensor, - Fp = CivitModelFpType.fp16, - Size = CivitModelSize.pruned, - } - } - }, - new() - { - FilePath = "~/Models/Lora/model.safetensors", - Title = "Some model" - }, - }, - }, - new(settingsManager, downloadService, modelFinder, notificationService) - { - Title = "Lora", - DirectoryPath = "Packages/Lora", - SubFolders = CheckpointFolder[] - { - new(settingsManager, downloadService, modelFinder, notificationService) - { - Title = "StableDiffusion", - DirectoryPath = "Packages/Lora/Subfolder", - }, - new(settingsManager, downloadService, modelFinder, notificationService) - { - Title = "Lora", - DirectoryPath = "Packages/StableDiffusion/Subfolder", - } - }, - CheckpointFiles = new AdvancedObservableList - { - new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", } - } - } - }; - - foreach (var folder in CheckpointsPageViewModel.CheckpointFolders) - { - folder.DisplayedCheckpointFiles = new AdvancedObservableList( - folder.CheckpointFiles - ); - }*/ + );*/ CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended { @@ -377,30 +309,59 @@ public static void Initialize() }) }; - NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection + CheckpointsPageViewModel.Categories = new ObservableCollectionExtended { new() { - FilePath = "~/Models/StableDiffusion/electricity-light.safetensors", - Title = "Auroral Background", - PreviewImagePath = - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" - + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", - ConnectedModel = new ConnectedModelInfo + Name = "Category 1", + Path = "path1", + SubDirectories = [new CheckpointCategory { Name = "SubCategory 1", Path = "path3" }] + }, + new() { Name = "Category 2", Path = "path2" } + }; + + CheckpointsPageViewModel.Models = new ObservableCollectionExtended() + { + new( + settingsManager, + new MockModelIndexService(), + notificationService, + dialogFactory, + new LocalModelFile { - VersionName = "Lightning Auroral", - BaseModel = "SD 1.5", - ModelName = "Auroral Background", - ModelType = CivitModelType.Model, - FileMetadata = new CivitFileMetadata + SharedFolderType = SharedFolderType.StableDiffusion, + RelativePath = "~/Models/StableDiffusion/electricity-light.safetensors", + PreviewImageFullPath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", + HasUpdate = true, + ConnectedModelInfo = new ConnectedModelInfo { - Format = CivitModelFormat.SafeTensor, - Fp = CivitModelFpType.fp16, - Size = CivitModelSize.pruned, + VersionName = "Lightning Auroral", + BaseModel = "SD 1.5", + ModelName = "Auroral Background", + ModelType = CivitModelType.Model, + FileMetadata = new CivitFileMetadata + { + Format = CivitModelFormat.SafeTensor, + Fp = CivitModelFpType.fp16, + Size = CivitModelSize.pruned, + }, + TrainedWords = ["aurora", "lightning"] } } - }, - new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" } + ), + new( + settingsManager, + new MockModelIndexService(), + notificationService, + dialogFactory, + new LocalModelFile + { + RelativePath = "~/Models/Lora/model.safetensors", + SharedFolderType = SharedFolderType.StableDiffusion + } + ), }; ProgressManagerViewModel.ProgressItems.AddRange( @@ -523,13 +484,13 @@ public static OutputsPageViewModel OutputsPageViewModel } ) }; - vm.Categories = new ObservableCollectionExtended + vm.Categories = new ObservableCollectionExtended { new() { Name = "Category 1", Path = "path1", - SubDirectories = [new PackageOutputCategory { Name = "SubCategory 1", Path = "path3" }] + SubDirectories = [new TreeViewDirectory { Name = "SubCategory 1", Path = "path3" }] }, new() { Name = "Category 2", Path = "path2" } }; @@ -537,12 +498,12 @@ public static OutputsPageViewModel OutputsPageViewModel } } - public static PackageManagerViewModel PackageManagerViewModel + public static MainPackageManagerViewModel MainPackageManagerViewModel { get { var settings = Services.GetRequiredService(); - var vm = Services.GetRequiredService(); + var vm = Services.GetRequiredService(); vm.SetPackages(settings.Settings.InstalledPackages); vm.SetUnknownPackages( @@ -594,13 +555,10 @@ public static PackageManagerViewModel PackageManagerViewModel public static CheckpointsPageViewModel CheckpointsPageViewModel => Services.GetRequiredService(); - public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel => - Services.GetRequiredService(); - public static SettingsViewModel SettingsViewModel => Services.GetRequiredService(); - public static NewPackageManagerViewModel NewPackageManagerViewModel => - Services.GetRequiredService(); + public static PackageManagerViewModel PackageManagerViewModel => + Services.GetRequiredService(); public static InferenceSettingsViewModel InferenceSettingsViewModel => Services.GetRequiredService(); @@ -694,7 +652,7 @@ public static UpdateSettingsViewModel UpdateSettingsViewModel } }; var sampleViewModel = new ModelVersionViewModel( - new HashSet { "ABCD" }, + Services.GetRequiredService(), sampleCivitVersions[0] ); @@ -762,6 +720,8 @@ public static UpdateSettingsViewModel UpdateSettingsViewModel + "redirect_uri=http://localhost:5022/api/oauth/patreon/callback"; }); + public static MaskEditorViewModel MaskEditorViewModel => DialogFactory.Get(); + public static InferenceTextToImageViewModel InferenceTextToImageViewModel => DialogFactory.Get(vm => { @@ -925,7 +885,10 @@ public static UpdateSettingsViewModel UpdateSettingsViewModel public static LayerDiffuseCardViewModel LayerDiffuseCardViewModel => DialogFactory.Get(); - + + public static ExtraNetworkCardViewModel ExtraNetworkCardViewModel => + DialogFactory.Get(); + public static InstalledWorkflowsViewModel InstalledWorkflowsViewModel { get @@ -1054,6 +1017,8 @@ public static CompletionList SampleCompletionList ); }); + public static PaintCanvasViewModel PaintCanvasViewModel => DialogFactory.Get(); + public static ImageSource SampleImageSource => new( new Uri( @@ -1067,6 +1032,16 @@ public static CompletionList SampleCompletionList public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get(); + public static ConfirmDeleteDialogViewModel ConfirmDeleteDialogViewModel => + DialogFactory.Get(vm => + { + vm.IsRecycleBinAvailable = true; + vm.PathsToDelete = Enumerable + .Range(1, 64) + .Select(i => $"C:/Users/ExampleUser/Data/ExampleFile{i}.txt") + .ToArray(); + }); + public static OpenArtWorkflowViewModel OpenArtWorkflowViewModel => new(Services.GetRequiredService(), Services.GetRequiredService()) { diff --git a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs b/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs index 34540a8f8..65abc05c5 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs @@ -27,6 +27,9 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl public IObservableCollection ControlNetModels { get; } = new ObservableCollectionExtended(); + public IObservableCollection LoraModels { get; } = + new ObservableCollectionExtended(); + public IObservableCollection PromptExpansionModels { get; } = new ObservableCollectionExtended(); diff --git a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs index 49458c6da..0cda9486a 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Nito.Disposables.Internals; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Services; @@ -13,6 +14,10 @@ public class MockModelIndexService : IModelIndexService /// public Dictionary> ModelIndex { get; } = new(); + /// + public IReadOnlySet ModelIndexBlake3Hashes => + ModelIndex.Values.SelectMany(x => x).Select(x => x.HashBlake3).WhereNotNull().ToHashSet(); + /// public Task RefreshIndex() { @@ -20,13 +25,19 @@ public Task RefreshIndex() } /// - public IEnumerable GetFromModelIndex(SharedFolderType types) + public IEnumerable FindByModelType(SharedFolderType types) { return Array.Empty(); } /// - public Task> FindAsync(SharedFolderType type) + public Task> FindAllFolders() + { + return Task.FromResult(new Dictionary()); + } + + /// + public Task> FindByModelTypeAsync(SharedFolderType type) { return Task.FromResult(Enumerable.Empty()); } @@ -43,7 +54,12 @@ public Task RemoveModelAsync(LocalModelFile model) return Task.FromResult(false); } - public Task CheckModelsForUpdates() + public Task RemoveModelsAsync(IEnumerable models) + { + return Task.FromResult(false); + } + + public Task CheckModelsForUpdateAsync() { return Task.CompletedTask; } diff --git a/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs b/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs index e2c1c8f08..76f56383b 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs @@ -1,10 +1,11 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.DesignData; -public class MockSettingsManager : SettingsManager +public class MockSettingsManager() : SettingsManager(NullLogger.Instance) { protected override void LoadSettings(CancellationToken cancellationToken = default) { } diff --git a/StabilityMatrix.Avalonia/Extensions/BitmapExtensions.cs b/StabilityMatrix.Avalonia/Extensions/BitmapExtensions.cs new file mode 100644 index 000000000..961c76108 --- /dev/null +++ b/StabilityMatrix.Avalonia/Extensions/BitmapExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using SkiaSharp; + +namespace StabilityMatrix.Avalonia.Extensions; + +public static class BitmapExtensions +{ + /// + /// Converts an Avalonia to a SkiaSharp . + /// + /// The Avalonia bitmap to convert. + /// The SkiaSharp bitmap. + public static SKBitmap ToSKBitmap(this Bitmap bitmap) + { + if (bitmap.Format != PixelFormat.Rgba8888 && bitmap.Format != PixelFormat.Bgra8888) + { + throw new NotSupportedException($"Unknown pixel format {bitmap.Format}"); + } + + var skColorType = SKColorType.Bgra8888; + if (bitmap.Format == PixelFormat.Rgba8888) + { + skColorType = SKColorType.Rgba8888; + } + + var skAlphaType = bitmap.AlphaFormat switch + { + AlphaFormat.Premul => SKAlphaType.Premul, + AlphaFormat.Unpremul => SKAlphaType.Unpremul, + AlphaFormat.Opaque => SKAlphaType.Opaque, + _ => SKAlphaType.Premul + }; + + var skBitmap = new SKBitmap( + bitmap.PixelSize.Width, + bitmap.PixelSize.Height, + skColorType, + skAlphaType + ); + + var stride = skBitmap.RowBytes; + var bufferSize = stride * skBitmap.Height; + var sourceRect = new PixelRect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height); + + bitmap.CopyPixels(sourceRect, skBitmap.GetPixels(), bufferSize, stride); + + return skBitmap; + } + + // Convert to byte array + public static byte[] ToByteArray(this Bitmap bitmap) + { + var pixelRect = new PixelRect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height); + var stride = bitmap.PixelSize.Width * 4; + + var bufferSize = bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4; + var buffer = new byte[bufferSize]; + + var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned); + + try + { + bitmap.CopyPixels(pixelRect, pinnedBuffer.AddrOfPinnedObject(), bufferSize, stride); + } + finally + { + pinnedBuffer.Free(); + } + + return buffer; + } +} diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index 64a522c5f..77d4ddf34 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -1,12 +1,8 @@ using System; -using System.ComponentModel.DataAnnotations; using System.Drawing; using System.IO; using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.Models.Inference; -using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; -using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.Extensions; @@ -72,6 +68,105 @@ public static void SetupImagePrimarySource( builder.Connections.Primary = loadImage.Output1; builder.Connections.PrimarySize = imageSize; + // Add batch if selected + if (builder.Connections.BatchSize > 1) + { + builder.Connections.Primary = builder + .Nodes.AddTypedNode( + new ComfyNodeBuilder.RepeatLatentBatch + { + Name = builder.Nodes.GetUniqueName("RepeatLatentBatch"), + Samples = builder.GetPrimaryAsLatent(), + Amount = builder.Connections.BatchSize + } + ) + .Output; + } + + // If batch index is selected, add a LatentFromBatch + if (batchIndex is not null) + { + builder.Connections.Primary = builder + .Nodes.AddTypedNode( + new ComfyNodeBuilder.LatentFromBatch + { + Name = "LatentFromBatch", + Samples = builder.GetPrimaryAsLatent(), + // remote expects a 0-based index, vm is 1-based + BatchIndex = batchIndex.Value - 1, + Length = 1 + } + ) + .Output; + } + } + + /// + /// Setup an image as the connection + /// + public static void SetupImagePrimarySourceWithMask( + this ComfyNodeBuilder builder, + ImageSource image, + Size imageSize, + ImageSource mask, + Size maskSize, + int? batchIndex = null + ) + { + // Get image paths + var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached()); + var maskImageRelativePath = Path.Combine("Inference", mask.GetHashGuidFileNameCached()); + + // Load image + var loadImage = builder.Nodes.AddTypedNode( + new ComfyNodeBuilder.LoadImage + { + Name = builder.Nodes.GetUniqueName("LoadImage"), + Image = sourceImageRelativePath + } + ); + + // Load mask for alpha channel + var loadMask = builder.Nodes.AddTypedNode( + new ComfyNodeBuilder.LoadImageMask + { + Name = builder.Nodes.GetUniqueName("LoadMask"), + Image = maskImageRelativePath, + Channel = "red" + } + ); + + builder.Connections.Primary = loadImage.Output1; + builder.Connections.PrimarySize = imageSize; + + // Encode VAE to latent with mask, and replace primary + builder.Connections.Primary = builder + .Nodes.AddTypedNode( + new ComfyNodeBuilder.VAEEncodeForInpaint + { + Name = builder.Nodes.GetUniqueName("VAEEncode"), + Pixels = loadImage.Output1, + Mask = loadMask.Output, + Vae = builder.Connections.GetDefaultVAE() + } + ) + .Output; + + // Add batch if selected + if (builder.Connections.BatchSize > 1) + { + builder.Connections.Primary = builder + .Nodes.AddTypedNode( + new ComfyNodeBuilder.RepeatLatentBatch + { + Name = builder.Nodes.GetUniqueName("RepeatLatentBatch"), + Samples = builder.GetPrimaryAsLatent(), + Amount = builder.Connections.BatchSize + } + ) + .Output; + } + // If batch index is selected, add a LatentFromBatch if (batchIndex is not null) { diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs b/StabilityMatrix.Avalonia/Extensions/InferenceProjectTypeExtensions.cs similarity index 81% rename from StabilityMatrix.Avalonia/Models/InferenceProjectType.cs rename to StabilityMatrix.Avalonia/Extensions/InferenceProjectTypeExtensions.cs index 1b4bb0178..f859414bb 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs +++ b/StabilityMatrix.Avalonia/Extensions/InferenceProjectTypeExtensions.cs @@ -1,18 +1,8 @@ using System; using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Models.Inference; -namespace StabilityMatrix.Avalonia.Models; - -public enum InferenceProjectType -{ - Unknown, - TextToImage, - ImageToImage, - Inpainting, - Upscale, - ImageToVideo -} - +namespace StabilityMatrix.Avalonia.Extensions; public static class InferenceProjectTypeExtensions { public static Type? ToViewModelType(this InferenceProjectType type) diff --git a/StabilityMatrix.Avalonia/Extensions/SkiaExtensions.cs b/StabilityMatrix.Avalonia/Extensions/SkiaExtensions.cs new file mode 100644 index 000000000..1ddcc3061 --- /dev/null +++ b/StabilityMatrix.Avalonia/Extensions/SkiaExtensions.cs @@ -0,0 +1,173 @@ +using System; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using SkiaSharp; + +namespace StabilityMatrix.Avalonia.Extensions; + +public static class SkiaExtensions +{ + private record class SKBitmapDrawOperation : ICustomDrawOperation + { + public Rect Bounds { get; set; } + + public SKBitmap? Bitmap { get; init; } + + public void Dispose() + { + //nop + } + + public bool Equals(ICustomDrawOperation? other) => false; + + public bool HitTest(Point p) => Bounds.Contains(p); + + public void Render(ImmediateDrawingContext context) + { + if ( + Bitmap is { } bitmap + && context.PlatformImpl.GetFeature() is { } leaseFeature + ) + { + var lease = leaseFeature.Lease(); + using (lease) + { + lease.SkCanvas.DrawBitmap( + bitmap, + SKRect.Create( + (float)Bounds.X, + (float)Bounds.Y, + (float)Bounds.Width, + (float)Bounds.Height + ) + ); + } + } + } + } + + private class AvaloniaImage : IImage, IDisposable + { + private readonly SKBitmap? _source; + SKBitmapDrawOperation? _drawImageOperation; + + public AvaloniaImage(SKBitmap? source) + { + _source = source; + if (source?.Info.Size is { } size) + { + Size = new Size(size.Width, size.Height); + } + } + + public Size Size { get; } + + public void Dispose() => _source?.Dispose(); + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect) + { + if (_drawImageOperation is null) + { + _drawImageOperation = new SKBitmapDrawOperation { Bitmap = _source, }; + } + + _drawImageOperation.Bounds = sourceRect; + context.Custom(_drawImageOperation); + } + } + + public static SKBitmap? ToSKBitmap(this System.IO.Stream? stream) + { + if (stream == null) + return null; + return SKBitmap.Decode(stream); + } + + public static IImage? ToAvaloniaImage(this SKBitmap? bitmap) + { + if (bitmap is not null) + { + return new AvaloniaImage(bitmap); + } + return default; + } + + public static Bitmap ToAvaloniaBitmap(this SKBitmap bitmap) + { + return ToAvaloniaBitmap(bitmap, new Vector(96, 96)); + } + + public static Bitmap ToAvaloniaBitmap(this SKBitmap bitmap, Vector dpi) + { + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + var avaloniaColorFormat = bitmap.ColorType switch + { + SKColorType.Rgba8888 => PixelFormat.Rgba8888, + SKColorType.Bgra8888 => PixelFormat.Bgra8888, + _ => throw new NotSupportedException($"Unsupported SKColorType: {bitmap.ColorType}") + }; + + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + var avaloniaAlphaFormat = bitmap.AlphaType switch + { + SKAlphaType.Opaque => AlphaFormat.Opaque, + SKAlphaType.Premul => AlphaFormat.Premul, + SKAlphaType.Unpremul => AlphaFormat.Unpremul, + _ => throw new NotSupportedException($"Unsupported SKAlphaType: {bitmap.AlphaType}") + }; + + var dataPointer = bitmap.GetPixels(); + + return new Bitmap( + avaloniaColorFormat, + avaloniaAlphaFormat, + dataPointer, + new PixelSize(bitmap.Width, bitmap.Height), + dpi, + bitmap.RowBytes + ); + } + + public static Bitmap ToAvaloniaBitmap(this SKImage image) + { + return ToAvaloniaBitmap(image, new Vector(96, 96)); + } + + public static Bitmap ToAvaloniaBitmap(this SKImage image, Vector dpi) + { + ArgumentNullException.ThrowIfNull(image, nameof(image)); + + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + var avaloniaColorFormat = image.ColorType switch + { + SKColorType.Rgba8888 => PixelFormat.Rgba8888, + SKColorType.Bgra8888 => PixelFormat.Bgra8888, + _ => throw new NotSupportedException($"Unsupported SKColorType: {image.ColorType}") + }; + + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + var avaloniaAlphaFormat = image.AlphaType switch + { + SKAlphaType.Opaque => AlphaFormat.Opaque, + SKAlphaType.Premul => AlphaFormat.Premul, + SKAlphaType.Unpremul => AlphaFormat.Unpremul, + _ => throw new NotSupportedException($"Unsupported SKAlphaType: {image.AlphaType}") + }; + + var pixmap = image.PeekPixels(); + var dataPointer = pixmap.GetPixels(); + + return new Bitmap( + avaloniaColorFormat, + avaloniaAlphaFormat, + dataPointer, + new PixelSize(image.Width, image.Height), + dpi, + pixmap.RowBytes + ); + } +} diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index 11105cd15..bc1887b81 100644 --- a/StabilityMatrix.Avalonia/Languages/Cultures.cs +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -1,8 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Threading; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Avalonia.Languages; @@ -13,23 +16,24 @@ public static class Cultures public static CultureInfo? Current => Resources.Culture; - public static readonly Dictionary SupportedCulturesByCode = new Dictionary< - string, - CultureInfo - > - { - ["en-US"] = Default, - ["ja-JP"] = new("ja-JP"), - ["zh-Hans"] = new("zh-Hans"), - ["zh-Hant"] = new("zh-Hant"), - ["it-IT"] = new("it-IT"), - ["fr-FR"] = new("fr-FR"), - ["es"] = new("es"), - ["ru-RU"] = new("ru-RU"), - ["tr-TR"] = new("tr-TR"), - ["de"] = new("de"), - ["pt-PT"] = new("pt-PT") - }; + public static NumberFormatInfo CurrentNumberFormat => Thread.CurrentThread.CurrentCulture.NumberFormat; + + public static readonly Dictionary SupportedCulturesByCode = + new() + { + ["en-US"] = Default, + ["ja-JP"] = new CultureInfo("ja-JP"), + ["zh-Hans"] = new CultureInfo("zh-Hans"), + ["zh-Hant"] = new CultureInfo("zh-Hant"), + ["it-IT"] = new CultureInfo("it-IT"), + ["fr-FR"] = new CultureInfo("fr-FR"), + ["es"] = new CultureInfo("es"), + ["ru-RU"] = new CultureInfo("ru-RU"), + ["tr-TR"] = new CultureInfo("tr-TR"), + ["de"] = new CultureInfo("de"), + ["pt-PT"] = new CultureInfo("pt-PT"), + ["pt-BR"] = new CultureInfo("pt-BR") + }; public static IReadOnlyList SupportedCultures => SupportedCulturesByCode.Values.ToImmutableList(); @@ -44,11 +48,19 @@ public static CultureInfo GetSupportedCultureOrDefault(string? cultureCode) return culture; } - public static void SetSupportedCultureOrDefault(string? cultureCode) + public static void SetSupportedCultureOrDefault(string? cultureCode, NumberFormatInfo numberFormat) + { + if (!TrySetSupportedCulture(cultureCode, numberFormat)) + { + TrySetSupportedCulture(Default, numberFormat); + } + } + + public static void SetSupportedCultureOrDefault(string? cultureCode, NumberFormatMode numberFormatMode) { - if (!TrySetSupportedCulture(cultureCode)) + if (!TrySetSupportedCulture(cultureCode, numberFormatMode)) { - TrySetSupportedCulture(Default); + TrySetSupportedCulture(Default, numberFormatMode); } } @@ -62,6 +74,59 @@ public static bool TrySetSupportedCulture(string? cultureCode) if (Current?.Name != culture.Name) { Resources.Culture = culture; + + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + + EventManager.Instance.OnCultureChanged(culture); + } + + return true; + } + + public static bool TrySetSupportedCulture(string? cultureCode, NumberFormatInfo numberFormat) + { + if (cultureCode is null || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) + { + return false; + } + + if (Current?.Name != culture.Name || CurrentNumberFormat != numberFormat) + { + Resources.Culture = culture; + + var cultureInfo = GetCultureInfoWithNumberFormat(culture, numberFormat); + Thread.CurrentThread.CurrentCulture = cultureInfo; + Thread.CurrentThread.CurrentUICulture = cultureInfo; + + EventManager.Instance.OnCultureChanged(culture); + } + + return true; + } + + public static bool TrySetSupportedCulture(string? cultureCode, NumberFormatMode numberFormatMode) + { + if (cultureCode is null || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) + { + return false; + } + + var numberFormat = numberFormatMode switch + { + NumberFormatMode.CurrentCulture => culture.NumberFormat, + NumberFormatMode.InvariantCulture => CultureInfo.InvariantCulture.NumberFormat, + _ => culture.NumberFormat, + }; + + if (Current?.Name != culture.Name || CurrentNumberFormat != numberFormat) + { + Resources.Culture = culture; + + var cultureInfo = GetCultureInfoWithNumberFormat(culture, numberFormat); + Thread.CurrentThread.CurrentCulture = cultureInfo; + Thread.CurrentThread.CurrentUICulture = cultureInfo; + EventManager.Instance.OnCultureChanged(culture); } @@ -72,4 +137,27 @@ public static bool TrySetSupportedCulture(CultureInfo? cultureInfo) { return cultureInfo is not null && TrySetSupportedCulture(cultureInfo.Name); } + + public static bool TrySetSupportedCulture(CultureInfo? cultureInfo, NumberFormatInfo numberFormat) + { + return cultureInfo is not null && TrySetSupportedCulture(cultureInfo.Name, numberFormat); + } + + public static bool TrySetSupportedCulture(CultureInfo? cultureInfo, NumberFormatMode numberFormatMode) + { + return cultureInfo is not null && TrySetSupportedCulture(cultureInfo.Name, numberFormatMode); + } + + // ReSharper disable once SuggestBaseTypeForParameter + private static CultureInfo GetCultureInfoWithNumberFormat( + CultureInfo culture, + NumberFormatInfo numberFormat + ) + { + ArgumentNullException.ThrowIfNull(culture); + + var cultureInfo = (CultureInfo)culture.Clone(); + cultureInfo.NumberFormat = numberFormat; + return cultureInfo; + } } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index e38cda209..9998a7e3e 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -293,6 +293,15 @@ public static string Action_Login { } } + /// + /// Looks up a localized string similar to Move to Trash. + /// + public static string Action_MoveToTrash { + get { + return ResourceManager.GetString("Action_MoveToTrash", resourceCulture); + } + } + /// /// Looks up a localized string similar to New. /// @@ -617,6 +626,15 @@ public static string Action_Stop { } } + /// + /// Looks up a localized string similar to Toggle Visibility. + /// + public static string Action_ToggleVisibility { + get { + return ResourceManager.GetString("Action_ToggleVisibility", resourceCulture); + } + } + /// /// Looks up a localized string similar to Uninstall. /// @@ -761,6 +779,15 @@ public static string Label_ApiKey { } } + /// + /// Looks up a localized string similar to App Data. + /// + public static string Label_AppData { + get { + return ResourceManager.GetString("Label_AppData", resourceCulture); + } + } + /// /// Looks up a localized string similar to Appearance. /// @@ -770,6 +797,15 @@ public static string Label_Appearance { } } + /// + /// Looks up a localized string similar to App Folders. + /// + public static string Label_AppFolders { + get { + return ResourceManager.GetString("Label_AppFolders", resourceCulture); + } + } + /// /// Looks up a localized string similar to Are you sure?. /// @@ -788,6 +824,15 @@ public static string Label_AreYouSureDeleteImages { } } + /// + /// Looks up a localized string similar to Are you sure you want to delete {0} models?. + /// + public static string Label_AreYouSureDeleteModels { + get { + return ResourceManager.GetString("Label_AreYouSureDeleteModels", resourceCulture); + } + } + /// /// Looks up a localized string similar to Augmentation Level. /// @@ -815,6 +860,24 @@ public static string Label_AutoScrollToEnd { } } + /// + /// Looks up a localized string similar to Auto-Search on Load. + /// + public static string Label_AutoSearchOnLoad { + get { + return ResourceManager.GetString("Label_AutoSearchOnLoad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically initiate a search when the model browser page is loaded. + /// + public static string Label_AutoSearchOnLoad_Description { + get { + return ResourceManager.GetString("Label_AutoSearchOnLoad_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Auto Updates. /// @@ -941,6 +1004,15 @@ public static string Label_CivitAiLoginRequired { } } + /// + /// Looks up a localized string similar to Clipping Mask. + /// + public static string Label_ClippingMask { + get { + return ResourceManager.GetString("Label_ClippingMask", resourceCulture); + } + } + /// /// Looks up a localized string similar to CLIP Skip. /// @@ -950,6 +1022,15 @@ public static string Label_CLIPSkip { } } + /// + /// Looks up a localized string similar to CLIP Strength. + /// + public static string Label_CLIPStrength { + get { + return ResourceManager.GetString("Label_CLIPStrength", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close dialog when finished. /// @@ -1175,6 +1256,15 @@ public static string Label_Deemphasis { } } + /// + /// Looks up a localized string similar to Delete Permanently. + /// + public static string Label_DeletePermanently { + get { + return ResourceManager.GetString("Label_DeletePermanently", resourceCulture); + } + } + /// /// Looks up a localized string similar to Denoising Strength. /// @@ -1337,6 +1427,15 @@ public static string Label_EverythingLooksGood { } } + /// + /// Looks up a localized string similar to Extra Networks (Lora / LyCORIS). + /// + public static string Label_ExtraNetworks { + get { + return ResourceManager.GetString("Label_ExtraNetworks", resourceCulture); + } + } + /// /// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.. /// @@ -1679,6 +1778,15 @@ public static string Label_LocalModel { } } + /// + /// Looks up a localized string similar to Logs. + /// + public static string Label_Logs { + get { + return ResourceManager.GetString("Label_Logs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Lossless. /// @@ -1850,6 +1958,15 @@ public static string Label_Notifications { } } + /// + /// Looks up a localized string similar to Number Format. + /// + public static string Label_NumberFormat { + get { + return ResourceManager.GetString("Label_NumberFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} images selected. /// @@ -2831,6 +2948,15 @@ public static string Text_AppWillRelaunchAfterUpdate { } } + /// + /// Looks up a localized string similar to You are about to delete the following items:. + /// + public static string Text_DeleteFollowingItems { + get { + return ResourceManager.GetString("Text_DeleteFollowingItems", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose your preferred interface to get started. /// @@ -2885,6 +3011,15 @@ public static string Text_WelcomeToStabilityMatrix { } } + /// + /// Looks up a localized string similar to You are about to delete the following {0} items:. + /// + public static string TextTemplate_DeleteFollowingCountItems { + get { + return ResourceManager.GetString("TextTemplate_DeleteFollowingCountItems", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error updating {0}. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.de.resx b/StabilityMatrix.Avalonia/Languages/Resources.de.resx index 1a2ef15fc..b65fa5527 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.de.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.de.resx @@ -934,4 +934,7 @@ Qualität + + CLIP Stärke + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.es.resx b/StabilityMatrix.Avalonia/Languages/Resources.es.resx index 0bc23e712..ee7e0cce0 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.es.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.es.resx @@ -1025,6 +1025,9 @@ Navegador de Modelos + + CLIP Fortaleza + Workflows diff --git a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx index f205b67c4..b2b9939f3 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx @@ -737,7 +737,7 @@ Cela déplacera toutes les images générées des packages sélectionnés vers le répertoire consolidé du dossier de sorties partagées. Cette action ne peut pas être annulée. - Rafraichir + Actualiser Passer à la version supérieure @@ -978,4 +978,55 @@ Nous recommandons un GPU avec prise en charge CUDA pour une meilleure expérience. Vous pouvez continuer sans en avoir un, mais certains packages peuvent ne pas fonctionner et l'inférence peut être plus lente. + + Explorateur de modèle + + + Workflows + + + Scroll infini + + + Explorateur de workflow + + + Ouvrir sur OpenArt + + + Plus de détails + + + Description du workflow + + + Explorateur OpenArt + + + Une autre instance de Stability Matrix est déjà lancée. Merci de la quitter avant d'en démarrer une nouvelle. + + + Stability Matrix est déjà lancé + + + {0} suppressions réalisées + + + Workflow supprimé + + + Erreur lors de l'obtention des workflows + + + Workflows installés + + + Workflows importé + + + L'import du workflow et des custom nodes est terminé + + + Le workflow et les custom nodes ont été importés. + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx b/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx new file mode 100644 index 000000000..def1a1e0c --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Iniciar + + + Sair + + + Salvar + + + Cancelar + + + Idioma + + + É necessário reiniciar para aplicar o novo idioma + + + Reiniciar + + + Reiniciar mais tarde + + + Reinício Necessário + + + Pacote Desconhecido + + + Importar + + + Pacote Tipo + + + Versão + + + Tipo de Versão + + + Lançamentos + + + Branches + + + Araste & Solte os checkpoints aqui para importar + + + Ênfase + + + Diminuir Ênfase + + + Inversão Textual / Embeddings + + + Redes (Lora / LyCORIS) + + + Comentários + + + Exibir grade de pixel em altos níveis de zoom + + + Etapas + + + Etapas - Base + + + Etapas - Aprimorar + + + Escala CFG + + + Intensidade da Redução de Ruído + + + Largura + + + Altura + + + Aprimorar + + + VAE + + + Modelo + + + Conectar + + + Conectando... + + + Fechar + + + Aguardando para conectar... + + + Atualização Disponível + + + Torne-se um Apoiador + + + Entrar no Servidor do Discord + + + Downloads + + + Instalar + + + Pular a configuração inicial + + + Ocorreu um erro inesperado + + + Fechar a Aplicação + + + Nome de Exibição + Can I get some context on this? + + + Uma instalação com esse nome já existe. + + + Por favor escolha um nome diferente ou defina outro local de instalação. + + + Opções Avançadas + + + Commit + + + Organização da Pasta de Modelos Compartilhados + Please, Can I get some context? + + + PyTorch Versão + + + Fechar a caixa de diálogo ao finalizar + + + Diretório de Dados + + + Aqui é onde os dados da aplicação (checkpoints do Modelo, interface Web, etc.) serão instalados. + + + Você pode encontrar erros ao utilizar uma unidade FAT32 ou exFAT. Escolha uma unidade diferente para uma experiência fluída. + + + Modo Portátil + + + No Modo Portátil, todos os dados e configurações serão armazenados no mesmo diretório da aplicação. Você poderá mover a aplicação com sua pasta 'Dados' para outro local ou computador diferente. + + + Continuar + + + Imagem Anterior + + + Próxima Imagem + + + Descrição do Modelo + + + Uma nova versão do Stability Matrix está disponível! + + + Importar Mais Recente - + + + Todas as Versões + + + Procure modelos, #tags, ou @usuários + + + Pesquisar + + + Ordenar + + + Período + + + Tipo de Modelo + + + Modelo Base + + + Exibir Conteúdo Adulto + + + Dados disponibilizados por CivitAI + + + Página + + + Primeira Página + + + Página Anterior + + + Próxima Página + + + Última Página + + + Renomear + + + Apagar + + + Abrir no CivitAI + + + Conectar Modelo + + + Modelo Local + + + Exibir no Explorador de Arquivos + + + Novo + + + Pasta + + + Solte aqui o arquivo para importar + + + Importar com Metadados + + + Pesquisar por metadados conectados em novas importações locais + + + Indexando... + + + Pasta dos Modelos + + + Categorias + + + Vamos começar + + + Li e concordo com o + + + Contrato de Licença. + + + Buscar Metadados Conectados + + + Exibir Imagens do Modelo + + + Aparência + + + Tema + + + Gerenciador de Checkpoint + + + Remover links simbólicos dos diretórios de checkpoints compartilhados ao desligar + + + Escolha essa opção caso você esteja enfrentando problemas ao mover o Stability Matrix para outra unidade + + + Redefinir o Cache dos Checkpoints + + + Reconstrói o cache de checkpoints instalados. Use caso os checkpoints estejam nomeados incorretamente no Navegador de Modelos + + + Ambiente de Pacote + + + Editar + + + Variáveis de Ambiente + + + Python Incorporado + + + Verificar Versão + + + Integrações + + + Discord Status / Rich Presence + + + Sistema + + + Adicionar Stability Matrix no Menu Iniciar + + + Utiliza o diretório atual da aplicação, você pode executá-lo novamente se mover o aplicativo + + + Disponível apenas para Windows + + + Adicionar para o Usuário Atual + + + Adicionar para Todos os Usuários + + + Selecione o novo diretório de dados + + + Não move dados existentes + + + Selecione o Diretório + + + Sobre + + + Stability Matrix + + + Avisos de licença e Código Aberto + + + Clique em Iniciar para Começar! + + + Parar + + + Enviar Entrada + + + Entrada + + + Enviar + + + Entrada Necessária + + + Confirmar? + + + Sim + + + Não + + + Abrir Interface Web + + + Bem-Vindo ao Stability Matrix! + + + Escolha sua interface preferida para começar + + + Instalando + + + Prosseguindo para a Página Iniciar + + + Baixando o pacote... + + + Download Concluído + + + Instalação Concluída + + + Instalando pré-requisitos... + + + Instalando dependências do pacote... + + + Abrir no Explorador de Arquivos + + + Abrir no Finder + + + Desinstalar + + + Procurar Atualizações + + + Atualizar + + + Adicionar Pacote + + + Adicione um pacote para começar! + + + Nome + + + Valor + + + Remover + + + Detalhes + + + Callstack + Referring to the term in the programming, no translation is necessary, as the word Callstack is widely known + + + Erro interno + + + Procurar... + + + OK + + + Repetir + + + Informações da Versão do Python + + + Reiniciar + + + Confirmar Exclusão + + + Isso excluirá a pasta do pacote e todo o seu conteúdo, incluindo quaisquer imagens geradas e arquivos que você possa ter adicionado. + + + Desinstalando pacote... + + + Pacote desinstalado + + + Alguns arquivos não puderam ser excluídos. Feche todos os arquivos abertos no diretório do pacote e tente novamente. + + + Tipo de Pacote Inválido + + + Atualizando {0} + + + Atualização Concluída + + + {0} foi atualizado + + + Erro ao atualizar {0} + + + Atualização falhou + + + Abrir no Navegador + + + Erro ao instalar o pacote + + + Branch + + + Rolar automaticamente até a saída final do terminal + + + Licença + + + Compartilhamento de modelo + + + Selecione o diretório de dados + + + Nome da Pasta de Dados + + + Diretório atual: + + + O aplicativo será reiniciado após a atualização + + + Lembrar-me Depois + + + Instalar Agora + + + Notas de Versão + + + Abrir Projeto... + + + Salvar Como... + + + Restaurar Layout Padrão + + + Usar Pasta de Saída Compartilhada + + + Batch Índice + Batch is also a word useded in PT-BR tech community + + + Copiar + + + Abrir no Visualizador de Imagem + + + {0} imagens selecionadas + + + Pasta de Saída + + + Tipo de Saída + + + Limpar Seleção + + + Selecionar Tudo + + + Enviar para Inferência + + + Texto para Imagem + + + Imagem para Imagem + + + Inpainting + There is no word that directly translates 'Inpainting' into PT-BR, the closest would be 'image reconstruction'. So I decided to keep the original term. + + + Upscale + There is no word that directly translates 'upscale' into PT-BR, the closest would be 'Enhance Image'. So I decided to keep the original term + + + Navegador de Saídas + + + 1 imagem seleciona + + + Pacotes Python + + + Consolidar + + + Você tem certeza? + + + Isso irá mover todas as imagens geradas dos pacotes selecionados para o diretório Consolidado da pasta de saídas compartilhadas. Essa ação não pode ser desfeita. + + + Atualizar + + + Atualizar + + + Reverter Versão + + + Abrir no GitHub + + + Conectado + + + Desconectar + + + E-mail + + + Usuário + + + Senha + + + Login + + + Cadastrar + + + Confirmar Senha + + + Chave API + + + Contas + + + Pré-processador + + + Força + + + Ajustes do Peso + + + Ajustes dos Passos + + + Você precisar estar logado para baixar esse checkpoint. Insira a chave de API da CivitAI nas configurações. + + + O Download Falhou + + + Atualizações Automáticas + + + Para usuários antecipados. As compilações de preview são mais confiáveis do que as do canal de Desenvolvimento e serão disponibilizadas próximas das versões estáveis. Seu feedback nos ajudará muito a descobrir problemas e aprimorar elementos de design. + + + Para usuários técnicos. Seja o primeiro a acessar nossas builds de Desenvolvimento de futuras branches assim que forem disponibilizadas. Poderá haver algumas imperfeições e bugs à medida que experimentamos novos recursos. + + + Atualizações + + + Você está atualizado + + + Última verificação: {0} + + + Copiar Palavras de Gatilho + + + Palavras de Gatilho: + + + Pastas adicionais, como 'IPAdapters' e InversãoTextual (embeddings), podem ser ativadas aqui + + + Abrir no Hugging Face + + + Atualizar Metadados Existentes + + + Geral + A general settings category + + + Inferência + The Inference feature page + + + Prompt + A settings category for Inference generation prompts + + + Saída de Arquivos de Imagem + + + Visualizador de Imagem + + + Preenchimento Automático + + + Substituir sublinhados por espaços no preenchimento automático + + + Tags do Prompt + Tags for image generation prompts + + + Importar Tags do Prompt + + + Arquivo de tags a ser usado no preenchimento automático (Suporta o formato a1111-sd-webui-tagcomplete .csv) + + + Informações de Sistema + + + CivitAI + + + Hugging Face + + + Extensões + Inference Sampler Addons + + + Salvar Imagem Intermediária + Inference module step to save an intermediate image + + + Configurações + + + Selecione o Arquivo + + + Substituir Conteúdo + + + Indisponível no momento + + + O recurso estará disponível em uma atualização futura + + + Arquivo de Imagem Não Encontrado + + + Modo Natalino + Do we have some context? + + + Pular CLIP + + + Imagem para Vídeo + + + Frames Por Segundo + + + CFG Mínimo + + + Lossless + There is no term that translates directly into PT-BR. The word 'lossless' is widely used. + + + Frames + + + Motion Bucket ID + + + Nível de Ampliação + + + Método + + + Qualidade + + + Buscar no Navegador de Modelos + + + Instalado + + + Nenhuma extensão encontrada. + + + Ocultar + + + Copiar Detalhes + + + Download + + + Verifique o progresso das instalações dos pacotes e downloads dos modelos aqui. + + + Modelos Recomendados + + + Enquanto seu pacote é instalado, veja alguns modelos que recomendamos para ajudá-lo a começar. + + + Notificações + + + Nenhum + + + Necessário ComfyUI + + + ComfyUI é necessário para instalar este pacote. Você deseja instalar agora? + + + Por favor selecione um local de download. + + + Selecione o Local de Download: + + + Config + + + Rolagem Automática até o fim + + + Confirmar Saída + + + Você tem certeza que deseja sair? Qualquer pacote em execução será encerrado. + + + Terminal + + + Interface Web + + + Pacotes + + + Esta ação não pode ser desfeita. + + + Você tem certeza que deseja excluir {0} imagens? + + + Estamos checando algumas especificações de hardware para determinar a compatibilidade. + + + Tudo certo! + + + Para melhor experiência recomendamos o uso de GPU com suporte a CUDA. Você pode continuar sem uma, porém alguns pacotes podem não funcionar, e a inferência pode ficar mais lenta. + + + Checkpoints + + + Navegador de Modelos + + + Workflows + + + Rolagem Infinita + + + Navegador de Workflows + + + Abrir no OpenArt + + + Detalhes do Nó + + + Descrição do Workflow + + + Navegador OpenArt + + + Pré-processador de pré-visualização + + + O botão 'Abrir Interface Web' foi movido para a barra de comando + + + Outra instância do Stability Matrix já está em execução. Por favor feche-a antes de iniciar outra. + + + Stability Matrix já está em execução + + + {0} excluído com sucesso + + + Workflow Excluído + + + Erro ao obter os workflows + + + Workflows Instalados + + + Workflow Importado + + + Finalizada a importação do Workflow e dos nós personalizados + + + O workflow e os nós customizados foram importados. + + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx b/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx index c74bd016e..ee79e62c6 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx @@ -934,4 +934,7 @@ Qualidade + + CLIP Força a aplicar + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index ee360cb17..21a267a09 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -1071,4 +1071,49 @@ Click here to review prompt syntax and how to include Lora / Embeddings. + + Extra Networks (Lora / LyCORIS) + + + CLIP Strength + + + Number Format + + + You are about to delete the following items: + + + You are about to delete the following {0} items: + + + Delete Permanently + + + Move to Trash + + + Are you sure you want to delete {0} models? + + + Auto-Search on Load + + + Automatically initiate a search when the model browser page is loaded + + + Toggle Visibility + + + Clipping Mask + + + App Folders + + + Logs + + + App Data + diff --git a/StabilityMatrix.Avalonia/Models/CheckpointCategory.cs b/StabilityMatrix.Avalonia/Models/CheckpointCategory.cs new file mode 100644 index 000000000..0d1096103 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/CheckpointCategory.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace StabilityMatrix.Avalonia.Models; + +public partial class CheckpointCategory : TreeViewDirectory +{ + [ObservableProperty] + private int count; + + public new ObservableCollection SubDirectories { get; set; } = new(); + + public IEnumerable Flatten() + { + yield return this; + + foreach (var subDirectory in SubDirectories) + { + foreach (var nestedSubDirectory in subDirectory.Flatten()) + { + yield return nestedSubDirectory; + } + } + } +} diff --git a/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs index e9237b50c..e816a7d75 100644 --- a/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs +++ b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs @@ -12,14 +12,18 @@ public enum HuggingFaceModelType [ConvertTo(SharedFolderType.StableDiffusion)] BaseModel, - [Description("ControlNets")] + [Description("ControlNets (SD1.5)")] [ConvertTo(SharedFolderType.ControlNet)] ControlNet, - [Description("ControlNets (Diffusers)")] + [Description("ControlNets (Diffusers SD1.5)")] [ConvertTo(SharedFolderType.ControlNet)] DiffusersControlNet, + [Description("ControlNets (SDXL)")] + [ConvertTo(SharedFolderType.ControlNet)] + ControlNetXl, + [Description("IP Adapters")] [ConvertTo(SharedFolderType.IpAdapter)] IpAdapter, diff --git a/StabilityMatrix.Avalonia/Models/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs index 331a9c093..df913b042 100644 --- a/StabilityMatrix.Avalonia/Models/ImageSource.cs +++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs @@ -8,6 +8,7 @@ using Avalonia.Media.Imaging; using Blake3; using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Webp; @@ -117,6 +118,12 @@ private async Task TryRefreshTemplateKeyAsync() return false; } + if (extension.Equals(".gif", StringComparison.OrdinalIgnoreCase)) + { + TemplateKey = ImageSourceTemplateType.WebpAnimation; + return true; + } + TemplateKey = ImageSourceTemplateType.Image; return true; @@ -167,14 +174,20 @@ public async Task GetBlake3HashAsync() return contentHashBlake3.Value; } - // Only available for local files - if (LocalFile is null) + if (LocalFile is not null) { - throw new InvalidOperationException("ImageSource is not a local file"); + var data = await LocalFile.ReadAllBytesAsync(); + contentHashBlake3 = await FileHash.GetBlake3ParallelAsync(data); } + else + { + if (await GetBitmapAsync() is not { } bitmap) + { + throw new InvalidOperationException("GetBitmapAsync returned null"); + } - var data = await LocalFile.ReadAllBytesAsync(); - contentHashBlake3 = await FileHash.GetBlake3ParallelAsync(data); + contentHashBlake3 = await FileHash.GetBlake3ParallelAsync(bitmap.ToByteArray()); + } return contentHashBlake3.Value; } @@ -184,17 +197,20 @@ public async Task GetBlake3HashAsync() /// public async Task GetHashGuidFileNameAsync() { - if (LocalFile is null) + var hash = await GetBlake3HashAsync(); + var guid = hash.ToGuid().ToString(); + + if (LocalFile?.Extension is { } extension) { - throw new InvalidOperationException("ImageSource is not a local file"); + guid += extension; + } + else + { + // Default to PNG if no extension + guid += ".png"; } - var extension = LocalFile.Info.Extension; - - var hash = await GetBlake3HashAsync(); - var guid = hash.ToGuid(); - - return guid + extension; + return guid; } /// @@ -203,32 +219,49 @@ public async Task GetHashGuidFileNameAsync() /// public string GetHashGuidFileNameCached() { - if (LocalFile is null) - { - throw new InvalidOperationException("ImageSource is not a local file"); - } - // Calculate hash if not available if (contentHashBlake3 is null) { - // File must exist - if (!LocalFile.Exists) + // Local file + if (LocalFile is not null) { - throw new FileNotFoundException("Image file does not exist", LocalFile); - } + // File must exist + if (!LocalFile.Exists) + { + throw new FileNotFoundException("Image file does not exist", LocalFile); + } - // Fail in debug since hash should have been pre-calculated - Debug.Fail("Hash has not been calculated when GetHashGuidFileNameCached() was called"); + // Fail in debug since hash should have been pre-calculated + Debug.Fail("Hash has not been calculated when GetHashGuidFileNameCached() was called"); - var data = LocalFile.ReadAllBytes(); - contentHashBlake3 = FileHash.GetBlake3Parallel(data); + var data = LocalFile.ReadAllBytes(); + contentHashBlake3 = FileHash.GetBlake3Parallel(data); + } + // Bitmap + else if (Bitmap is not null) + { + var data = Bitmap.ToByteArray(); + contentHashBlake3 = FileHash.GetBlake3Parallel(data); + } + else + { + throw new InvalidOperationException("ImageSource is not a local file or bitmap"); + } } - var extension = LocalFile.Info.Extension; + var guid = contentHashBlake3.Value.ToGuid().ToString(); - var guid = contentHashBlake3.Value.ToGuid(); + if (LocalFile?.Extension is { } extension) + { + guid += extension; + } + else + { + // Default to PNG if no extension + guid += ".png"; + } - return guid + extension; + return guid; } public string GetHashGuidFileNameCached(string pathPrefix) diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs index ff6905fd8..0cb8650a8 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.Models.Inference; @@ -27,10 +28,7 @@ public partial class FileNameFormatProvider { "seed", () => GenerationParameters?.Seed.ToString() }, { "prompt", () => GenerationParameters?.PositivePrompt }, { "negative_prompt", () => GenerationParameters?.NegativePrompt }, - { - "model_name", - () => Path.GetFileNameWithoutExtension(GenerationParameters?.ModelName) - }, + { "model_name", () => Path.GetFileNameWithoutExtension(GenerationParameters?.ModelName) }, { "model_hash", () => GenerationParameters?.ModelHash }, { "width", () => GenerationParameters?.Width.ToString() }, { "height", () => GenerationParameters?.Height.ToString() }, @@ -114,8 +112,7 @@ public IEnumerable GetParts(string template) } else { - var length = - Math.Min(value.Length, slice.End.Value) - (slice.Start ?? 0); + var length = Math.Min(value.Length, slice.End.Value) - (slice.Start ?? 0); value = value.Substring(slice.Start ?? 0, length); } diff --git a/StabilityMatrix.Avalonia/Models/Inference/InferenceTextToImageModel.cs b/StabilityMatrix.Avalonia/Models/Inference/InferenceTextToImageModel.cs index 5ad6717bb..a45614327 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/InferenceTextToImageModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/InferenceTextToImageModel.cs @@ -1,9 +1,7 @@ using System.Text.Json.Nodes; -using System.Text.Json.Serialization; namespace StabilityMatrix.Avalonia.Models.Inference; -[JsonSerializable(typeof(InferenceTextToImageModel))] public class InferenceTextToImageModel { public string? SelectedModelName { get; init; } diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs index 4c18bd81c..ef900f4d8 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs @@ -82,9 +82,7 @@ public void ValidateExtraNetworks(IModelIndexService indexService) throw new ApplicationException($"Model {network.Name} does not exist in index"); } - var localModel = modelList.FirstOrDefault( - m => m.FileNameWithoutExtension == network.Name - ); + var localModel = modelList.FirstOrDefault(m => m.FileNameWithoutExtension == network.Name); if (localModel == null) { throw new ApplicationException($"Model {network.Name} does not exist in index"); @@ -144,9 +142,7 @@ private int GetSafeEndIndex(int index) { // Normal tags - Push to output outputTokens.Push(currentToken); - outputText.Push( - RawText[currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex)] - ); + outputText.Push(RawText[currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex)]); continue; } @@ -168,9 +164,7 @@ private int GetSafeEndIndex(int index) ); } - var networkType = RawText[ - currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex) - ]; + var networkType = RawText[currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex)]; // Match network type var parsedNetworkType = networkType switch @@ -222,9 +216,7 @@ private int GetSafeEndIndex(int index) ); } - var modelName = RawText[ - currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex) - ]; + var modelName = RawText[currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex)]; // If index service provided, validate model name if (indexService != null) @@ -287,14 +279,10 @@ private int GetSafeEndIndex(int index) ); } - var modelWeight = RawText[ - currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex) - ]; + var modelWeight = RawText[currentToken.StartIndex..GetSafeEndIndex(currentToken.EndIndex)]; // Convert to double - if ( - !double.TryParse(modelWeight, CultureInfo.InvariantCulture, out var weightValue) - ) + if (!double.TryParse(modelWeight, CultureInfo.InvariantCulture, out var weightValue)) { throw PromptValidationError.Network_InvalidWeight( currentToken.StartIndex, @@ -332,9 +320,7 @@ private int GetSafeEndIndex(int index) outputTokens.Push(currentToken); outputText.Push( - weight is null - ? $"embedding:{modelName}" - : $"(embedding:{modelName}:{weight:F2})" + weight is null ? $"embedding:{modelName}" : $"(embedding:{modelName}:{weight:F2})" ); } // Cleanups for separate extra networks @@ -379,8 +365,8 @@ public string GetDebugText() // Format scope var scopeStr = string.Join( ", ", - token.Scopes - .Where(s => s != "source.prompt") + token + .Scopes.Where(s => s != "source.prompt") .Select( s => s.EndsWith(".prompt") @@ -398,7 +384,7 @@ public string GetDebugText() public static Prompt FromRawText(string text, ITokenizerProvider tokenizer) { - using var _ = new CodeTimer(); + using var _ = CodeTimer.StartDebug(); var result = tokenizer.TokenizeLine(text); diff --git a/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs b/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs index 107efbe32..c92f877a0 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs @@ -1,22 +1,19 @@ -using System.Text.Json.Serialization; +namespace StabilityMatrix.Avalonia.Models.Inference; -namespace StabilityMatrix.Avalonia.Models.Inference; - -[JsonSerializable(typeof(SamplerCardModel))] public class SamplerCardModel { public int Steps { get; init; } - + public bool IsDenoiseStrengthEnabled { get; init; } = false; public double DenoiseStrength { get; init; } - + public bool IsCfgScaleEnabled { get; init; } = true; public double CfgScale { get; init; } - + public bool IsDimensionsEnabled { get; init; } public int Width { get; init; } public int Height { get; init; } - + public bool IsSamplerSelectionEnabled { get; init; } = true; public string? SelectedSampler { get; init; } } diff --git a/StabilityMatrix.Avalonia/Models/Inference/StackCardModel.cs b/StabilityMatrix.Avalonia/Models/Inference/StackCardModel.cs index 295adb549..b8aaf6cee 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/StackCardModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/StackCardModel.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; namespace StabilityMatrix.Avalonia.Models.Inference; -[JsonSerializable(typeof(StackCardModel))] public class StackCardModel { public List? Cards { get; init; } diff --git a/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs b/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs index e361207b3..7480c202d 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/StackExpanderModel.cs @@ -1,8 +1,5 @@ -using System.Text.Json.Serialization; +namespace StabilityMatrix.Avalonia.Models.Inference; -namespace StabilityMatrix.Avalonia.Models.Inference; - -[JsonSerializable(typeof(StackExpanderModel))] public class StackExpanderModel : StackCardModel { public string? Title { get; set; } diff --git a/StabilityMatrix.Avalonia/Models/Inference/UpscalerCardModel.cs b/StabilityMatrix.Avalonia/Models/Inference/UpscalerCardModel.cs index b454ca02e..0c11a0b55 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/UpscalerCardModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/UpscalerCardModel.cs @@ -1,9 +1,7 @@ -using System.Text.Json.Serialization; -using StabilityMatrix.Core.Models.Api.Comfy; +using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Avalonia.Models.Inference; -[JsonSerializable(typeof(UpscalerCardModel))] public class UpscalerCardModel { public double Scale { get; init; } = 1; diff --git a/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs b/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs index 689fe1440..ac69ea01f 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/ViewState.cs @@ -1,11 +1,8 @@ -using System.Text.Json.Serialization; - -namespace StabilityMatrix.Avalonia.Models.Inference; +namespace StabilityMatrix.Avalonia.Models.Inference; /// /// Model for view states of inference tabs /// -[JsonSerializable(typeof(ViewState))] public class ViewState { public string? DockLayout { get; set; } diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs index 3cc0355ed..24b35bf92 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs @@ -4,13 +4,13 @@ using System.Text.Json.Serialization; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.Models; /// /// This is the project file for inference tabs /// -[JsonSerializable(typeof(InferenceProjectDocument))] public class InferenceProjectDocument : ICloneable { [JsonIgnore] diff --git a/StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs b/StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs deleted file mode 100644 index 62b699170..000000000 --- a/StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.ObjectModel; - -namespace StabilityMatrix.Avalonia.Models; - -public class PackageOutputCategory -{ - public ObservableCollection SubDirectories { get; set; } = new(); - public required string Name { get; set; } - public required string Path { get; set; } -} diff --git a/StabilityMatrix.Avalonia/Models/PaintCanvasTool.cs b/StabilityMatrix.Avalonia/Models/PaintCanvasTool.cs new file mode 100644 index 000000000..9e0de8af6 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/PaintCanvasTool.cs @@ -0,0 +1,8 @@ +namespace StabilityMatrix.Avalonia.Models; + +public enum PaintCanvasTool +{ + None, + PaintBrush, + Eraser +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index 5b03207d0..eb2acf072 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -110,10 +110,7 @@ private string PrepareInsertionText_Process(ICompletionData data) var text = data.Text; // For tags and if enabled, replace underscores with spaces - if ( - data is TagCompletionData - && settingsManager.Settings.IsCompletionRemoveUnderscoresEnabled - ) + if (data is TagCompletionData && settingsManager.Settings.IsCompletionRemoveUnderscoresEnabled) { // Remove underscores text = text.Replace("_", " "); @@ -315,25 +312,18 @@ int itemsCount var folderTypes = Enum.GetValues(typeof(PromptExtraNetworkType)) .Cast() .Where(f => networkType.HasFlag(f)) - .Select(network => network.ConvertTo()); + .Select(network => network.ConvertTo()) + // Convert back to bit flags + .Aggregate((a, b) => a | b); - var completions = new List(); + var models = modelIndexService.FindByModelType(folderTypes); - foreach (var folderType in folderTypes) - { - // Get from index service - if (modelIndexService.ModelIndex.TryGetValue(folderType, out var localModels)) - { - var results = - from model in localModels - where model.FileName.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase) - select ModelCompletionData.FromLocalModel(model, networkType); - - completions.AddRange(results.Take(itemsCount)); - } - } + var matches = models + .Where(model => model.FileName.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)) + .Select(model => ModelCompletionData.FromLocalModel(model, networkType)) + .Take(itemsCount); - return completions; + return matches; } private IEnumerable GetCompletionNetworkTypes(string searchTerm) @@ -347,19 +337,12 @@ private IEnumerable GetCompletionNetworkTypes(string searchTerm return availableTypes .Where( - type => - type.Item1 - .GetStringValue() - .StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase) + type => type.Item1.GetStringValue().StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase) ) .Select(type => new ModelTypeCompletionData(type.Item2, type.Item1)); } - private IEnumerable GetCompletionTags( - string searchTerm, - int itemsCount, - bool suggest - ) + private IEnumerable GetCompletionTags(string searchTerm, int itemsCount, bool suggest) { if (searcher is null) { @@ -405,11 +388,7 @@ bool suggest } timer.Stop(); - Logger.Trace( - "Completions for {Term} took {Time:F2}ms", - searchTerm, - timer.Elapsed.TotalMilliseconds - ); + Logger.Trace("Completions for {Term} took {Time:F2}ms", searchTerm, timer.Elapsed.TotalMilliseconds); return completions; } diff --git a/StabilityMatrix.Avalonia/Models/TreeViewDirectory.cs b/StabilityMatrix.Avalonia/Models/TreeViewDirectory.cs new file mode 100644 index 000000000..ebf74710c --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TreeViewDirectory.cs @@ -0,0 +1,11 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace StabilityMatrix.Avalonia.Models; + +public partial class TreeViewDirectory : ObservableObject +{ + public ObservableCollection SubDirectories { get; set; } = new(); + public required string Name { get; set; } + public required string Path { get; set; } +} diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index e034c61c8..558914fb6 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; @@ -65,6 +66,16 @@ public static void Main(string[] args) } }); + if ( + parseResult.Errors.Any( + x => x.Tag is ErrorType.HelpRequestedError or ErrorType.VersionRequestedError + ) + ) + { + Environment.Exit(0); + return; + } + Args = parseResult.Value ?? new AppArgs(); if (Args.HomeDirectoryOverride is { } homeDir) diff --git a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs index 75f834472..d33f93b5a 100644 --- a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs @@ -40,6 +40,7 @@ public interface IInferenceClientManager : IDisposable, INotifyPropertyChanged, IObservableCollection Models { get; } IObservableCollection VaeModels { get; } IObservableCollection ControlNetModels { get; } + IObservableCollection LoraModels { get; } IObservableCollection PromptExpansionModels { get; } IObservableCollection Samplers { get; } IObservableCollection Upscalers { get; } diff --git a/StabilityMatrix.Avalonia/Services/IModelImportService.cs b/StabilityMatrix.Avalonia/Services/IModelImportService.cs new file mode 100644 index 000000000..4ac503411 --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/IModelImportService.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Avalonia.Services; + +public interface IModelImportService +{ + /// + /// Saves the preview image to the same directory as the model file + /// + /// + /// + /// The file path of the saved preview image + Task SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath); + + Task DoImport( + CivitModel model, + DirectoryPath downloadFolder, + CivitModelVersion? selectedVersion = null, + CivitFile? selectedFile = null, + IProgress? progress = null, + Func? onImportComplete = null, + Func? onImportFailed = null + ); +} diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 22782abb5..1909aee27 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,8 @@ using DynamicData.Binding; using Microsoft.Extensions.Logging; using SkiaSharp; +using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Core.Api; @@ -75,6 +78,11 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient public IObservableCollection ControlNetModels { get; } = new ObservableCollectionExtended(); + private readonly SourceCache loraModelsSource = new(p => p.GetId()); + + public IObservableCollection LoraModels { get; } = + new ObservableCollectionExtended(); + private readonly SourceCache promptExpansionModelsSource = new(p => p.GetId()); private readonly SourceCache downloadablePromptExpansionModelsSource = @@ -144,6 +152,17 @@ ICompletionProvider completionProvider .Bind(ControlNetModels) .Subscribe(); + loraModelsSource + .Connect() + .Sort( + SortExpressionComparer + .Ascending(f => f.Type) + .ThenByAscending(f => f.ShortDisplayName) + ) + .DeferUntilLoaded() + .Bind(LoraModels) + .Subscribe(); + promptExpansionModelsSource .Connect() .Or(downloadablePromptExpansionModelsSource.Connect()) @@ -227,6 +246,15 @@ await Client.GetNodeOptionNamesAsync("ControlNetLoader", "control_net_name") is ); } + // Get Lora model names + if (await Client.GetNodeOptionNamesAsync("LoraLoader", "lora_name") is { } loraModelNames) + { + loraModelsSource.EditDiff( + loraModelNames.Select(HybridModelFile.FromRemote), + HybridModelFile.Comparer + ); + } + // Prompt Expansion indexing is local only // Fetch sampler names from KSampler node @@ -297,16 +325,14 @@ private void ResetSharedProperties() // Load local models modelsSource.EditDiff( modelIndexService - .GetFromModelIndex(SharedFolderType.StableDiffusion) + .FindByModelType(SharedFolderType.StableDiffusion) .Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); // Load local control net models controlNetModelsSource.EditDiff( - modelIndexService - .GetFromModelIndex(SharedFolderType.ControlNet) - .Select(HybridModelFile.FromLocal), + modelIndexService.FindByModelType(SharedFolderType.ControlNet).Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); @@ -316,10 +342,18 @@ private void ResetSharedProperties() ); downloadableControlNetModelsSource.EditDiff(downloadableControlNets, HybridModelFile.Comparer); + // Load local Lora / LyCORIS models + loraModelsSource.EditDiff( + modelIndexService + .FindByModelType(SharedFolderType.Lora | SharedFolderType.LyCORIS) + .Select(HybridModelFile.FromLocal), + HybridModelFile.Comparer + ); + // Load local prompt expansion models promptExpansionModelsSource.EditDiff( modelIndexService - .GetFromModelIndex(SharedFolderType.PromptExpansion) + .FindByModelType(SharedFolderType.PromptExpansion) .Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); @@ -334,7 +368,7 @@ private void ResetSharedProperties() // Load local VAE models vaeModelsSource.EditDiff( - modelIndexService.GetFromModelIndex(SharedFolderType.VAE).Select(HybridModelFile.FromLocal), + modelIndexService.FindByModelType(SharedFolderType.VAE).Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); @@ -347,7 +381,7 @@ private void ResetSharedProperties() // Load Upscalers modelUpscalersSource.EditDiff( modelIndexService - .GetFromModelIndex( + .FindByModelType( SharedFolderType.ESRGAN | SharedFolderType.RealESRGAN | SharedFolderType.SwinIR ) .Select(m => new ComfyUpscaler(m.FileName, ComfyUpscalerType.ESRGAN)), @@ -369,15 +403,44 @@ public async Task UploadInputImageAsync(ImageSource image, CancellationToken can { EnsureConnected(); - if (image.LocalFile is not { } localFile) + var uploadName = await image.GetHashGuidFileNameAsync(); + + if (image.LocalFile is { } localFile) { - throw new ArgumentException("Image is not a local file", nameof(image)); + logger.LogDebug("Uploading image {FileName} as {UploadName}", localFile.Name, uploadName); + + // For pngs, strip metadata since Pillow can't handle some valid files? + if (localFile.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase)) + { + var bytes = PngDataHelper.RemoveMetadata( + await localFile.ReadAllBytesAsync(cancellationToken) + ); + using var stream = new MemoryStream(bytes); + + await Client.UploadImageAsync(stream, uploadName, cancellationToken); + } + else + { + await using var stream = localFile.Info.OpenRead(); + + await Client.UploadImageAsync(stream, uploadName, cancellationToken); + } } + else + { + logger.LogDebug("Uploading bitmap as {UploadName}", uploadName); - var uploadName = await image.GetHashGuidFileNameAsync(); + if (await image.GetBitmapAsync() is not { } bitmap) + { + throw new InvalidOperationException("Failed to get bitmap from image"); + } - await using var stream = localFile.Info.OpenRead(); - await Client.UploadImageAsync(stream, uploadName, cancellationToken); + await using var ms = new MemoryStream(); + bitmap.Save(ms); + ms.Position = 0; + + await Client.UploadImageAsync(ms, uploadName, cancellationToken); + } } /// diff --git a/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs b/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs index 621458090..e893f30d2 100644 --- a/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs +++ b/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs @@ -18,7 +18,7 @@ namespace StabilityMatrix.Avalonia.Services; -[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))] +[Singleton(typeof(IModelDownloadLinkHandler))] public class ModelDownloadLinkHandler( IDistributedSubscriber uriHandlerSubscriber, ILogger logger, diff --git a/StabilityMatrix.Avalonia/Services/ModelImportService.cs b/StabilityMatrix.Avalonia/Services/ModelImportService.cs new file mode 100644 index 000000000..4d93d96cd --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/ModelImportService.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls.Notifications; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Services; + +[Singleton(typeof(IModelImportService))] +public class ModelImportService( + IDownloadService downloadService, + INotificationService notificationService, + ITrackedDownloadService trackedDownloadService +) : IModelImportService +{ + public static async Task SaveCmInfo( + CivitModel model, + CivitModelVersion modelVersion, + CivitFile modelFile, + DirectoryPath downloadDirectory + ) + { + var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); + var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); + + await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName); + + var jsonName = $"{modelFileName}.cm-info.json"; + return downloadDirectory.JoinFile(jsonName); + } + + /// + /// Saves the preview image to the same directory as the model file + /// + /// + /// + /// The file path of the saved preview image + public async Task SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath) + { + // Skip if model has no images + if (modelVersion.Images == null || modelVersion.Images.Count == 0) + { + return null; + } + + var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image"); + if (image is null) + return null; + + var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); + if (imageExtension is "jpg" or "jpeg" or "png") + { + var imageDownloadPath = modelFilePath.Directory!.JoinFile( + $"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" + ); + + var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); + await notificationService.TryAsync(imageTask, "Could not download preview image"); + + return imageDownloadPath; + } + + return null; + } + + public async Task DoImport( + CivitModel model, + DirectoryPath downloadFolder, + CivitModelVersion? selectedVersion = null, + CivitFile? selectedFile = null, + IProgress? progress = null, + Func? onImportComplete = null, + Func? onImportFailed = null + ) + { + // Get latest version + var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault(); + if (modelVersion is null) + { + notificationService.Show( + new Notification( + "Model has no versions available", + "This model has no versions available for download", + NotificationType.Warning + ) + ); + return; + } + + // Get latest version file + var modelFile = + selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model); + if (modelFile is null) + { + notificationService.Show( + new Notification( + "Model has no files available", + "This model has no files available for download", + NotificationType.Warning + ) + ); + return; + } + + // Folders might be missing if user didn't install any packages yet + downloadFolder.Create(); + + var downloadPath = downloadFolder.JoinFile(modelFile.Name); + + // Download model info and preview first + var cmInfoPath = await SaveCmInfo(model, modelVersion, modelFile, downloadFolder); + var previewImagePath = await SavePreviewImage(modelVersion, downloadPath); + + // Create tracked download + var download = trackedDownloadService.NewDownload(modelFile.DownloadUrl, downloadPath); + + // Add hash info + download.ExpectedHashSha256 = modelFile.Hashes.SHA256; + + // Add files to cleanup list + download.ExtraCleanupFileNames.Add(cmInfoPath); + if (previewImagePath is not null) + { + download.ExtraCleanupFileNames.Add(previewImagePath); + } + + // Attach for progress updates + download.ProgressUpdate += (s, e) => + { + progress?.Report(e); + }; + + download.ProgressStateChanged += (s, e) => + { + if (e == ProgressState.Success) + { + onImportComplete?.Invoke().SafeFireAndForget(); + } + else if (e == ProgressState.Cancelled) + { + // todo? + } + else if (e == ProgressState.Failed) + { + onImportFailed?.Invoke().SafeFireAndForget(); + } + }; + + // Add hash context action + download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(modelFile); + + download.Start(); + } +} diff --git a/StabilityMatrix.Avalonia/Services/NavigationService.cs b/StabilityMatrix.Avalonia/Services/NavigationService.cs index 9e6ac8015..a4c9f50bb 100644 --- a/StabilityMatrix.Avalonia/Services/NavigationService.cs +++ b/StabilityMatrix.Avalonia/Services/NavigationService.cs @@ -8,7 +8,6 @@ using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base; -using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Services; @@ -24,8 +23,8 @@ namespace StabilityMatrix.Avalonia.Services; InterfaceType = typeof(INavigationService) )] [Singleton( - ImplType = typeof(NavigationService), - InterfaceType = typeof(INavigationService) + ImplType = typeof(NavigationService), + InterfaceType = typeof(INavigationService) )] public class NavigationService : INavigationService { diff --git a/StabilityMatrix.Avalonia/Services/RunningPackageService.cs b/StabilityMatrix.Avalonia/Services/RunningPackageService.cs index 38db4dc30..437bc2936 100644 --- a/StabilityMatrix.Avalonia/Services/RunningPackageService.cs +++ b/StabilityMatrix.Avalonia/Services/RunningPackageService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -25,7 +26,7 @@ public partial class RunningPackageService( INotificationService notificationService, ISettingsManager settingsManager, IPyRunner pyRunner -) : ObservableObject +) : ObservableObject, IDisposable { // 🤔 what if we put the ConsoleViewModel inside the BasePackage? 🤔 [ObservableProperty] @@ -137,6 +138,8 @@ public async Task StopPackage(Guid id) var runningPackage = vm.RunningPackage; await runningPackage.BasePackage.WaitForShutdown(); RunningPackages.Remove(id); + + await vm.DisposeAsync(); } } @@ -150,4 +153,28 @@ private static async Task UnpackSiteCustomize(DirectoryPath venvPath) file.Directory?.Create(); await Assets.PyScriptSiteCustomize.ExtractTo(file, true); } + + public void Dispose() + { + var exceptions = new List(); + + foreach (var (_, vm) in RunningPackages) + { + try + { + vm.Dispose(); + } + catch (Exception e) + { + exceptions.Add(e); + } + } + + if (exceptions.Count != 0) + { + throw new AggregateException(exceptions); + } + + GC.SuppressFinalize(this); + } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 096e08c95..639930117 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -17,7 +17,7 @@ app.manifest true ./Assets/Icon.ico - 2.10.0-dev.999 + 2.11.0-dev.999 $(Version) true true @@ -81,8 +81,8 @@ - - + + @@ -92,6 +92,7 @@ + @@ -186,7 +187,7 @@ VideoOutputSettingsCard.axaml Code - + NewPackageManagerPage.axaml Code @@ -201,6 +202,10 @@ MSBuild:GenerateCodeFromAttributes + + MainPackageManagerView.axaml + Code + diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings index 524abe98d..b85eb2738 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings @@ -2,5 +2,8 @@ Yes Pessimistic UI + True True - True + True + True + diff --git a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml index 0b8d75f88..28cf81aa3 100644 --- a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml @@ -58,6 +58,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml.cs b/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml.cs new file mode 100644 index 000000000..d0f03c9af --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using Avalonia.VisualTree; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using MainPackageManagerViewModel = StabilityMatrix.Avalonia.ViewModels.PackageManager.MainPackageManagerViewModel; + +namespace StabilityMatrix.Avalonia.Views.PackageManager; + +[Singleton] +public partial class MainPackageManagerView : UserControlBase +{ + public MainPackageManagerView() + { + InitializeComponent(); + + AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct); + EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; + } + + private void OnOneClickInstallFinished(object? sender, bool skipped) + { + if (skipped) + return; + + Dispatcher.UIThread.Invoke(() => + { + var target = this.FindDescendantOfType() + ?.GetVisualChildren() + .OfType - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - + + + + + diff --git a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs index 3ac19d34f..d4e4c9806 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml.cs @@ -1,60 +1,70 @@ -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; +using System; +using System.ComponentModel; +using System.Linq; using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; using Avalonia.Threading; -using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media.Animation; using FluentAvalonia.UI.Navigation; +using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Core.Attributes; -using StabilityMatrix.Core.Helper; -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Views; [Singleton] -public partial class PackageManagerPage : UserControlBase +public partial class PackageManagerPage : UserControlBase, IHandleNavigation { + private readonly INavigationService packageNavigationService; + + private bool hasLoaded; + + private PackageManagerViewModel ViewModel => (PackageManagerViewModel)DataContext!; + + [DesignOnly(true)] + [Obsolete("For XAML use only", true)] public PackageManagerPage() + : this(App.Services.GetRequiredService>()) { } + + public PackageManagerPage(INavigationService packageNavigationService) { + this.packageNavigationService = packageNavigationService; + InitializeComponent(); AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct); - EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; + + packageNavigationService.SetFrame(FrameView); + packageNavigationService.TypedNavigation += NavigationService_OnTypedNavigation; + FrameView.Navigated += FrameView_Navigated; + BreadcrumbBar.ItemClicked += BreadcrumbBar_ItemClicked; } - private void OnOneClickInstallFinished(object? sender, bool skipped) + /// + protected override void OnLoaded(RoutedEventArgs e) { - if (skipped) - return; + base.OnLoaded(e); - Dispatcher.UIThread.Invoke(() => + if (!hasLoaded) { - var target = this.FindDescendantOfType() - ?.GetVisualChildren() - .OfType